Deploy and Monitor an AWS API

Deploy, break, repair, and monitor a Terraform-managed AWS health API.

Introduction

30 Second Summary

The handoff between two tools creates a new point of failure. One missing permission can turn a healthy result into a failed request.

In this project, you will build a public HTTP API on AWS using Terraform. The finished lab proves that you can diagnose a cross-service permission failure from logs before repairing the request path.

What You'll Build

Picture calling your public /hello URL to see healthy JSON from a service whose failure and repair you can explain with evidence.

By the end of this project, you'll have:

  • A public /hello endpoint that returns healthy JSON from AWS Lambda through Amazon API Gateway.
  • A reproducible troubleshooting story where API Gateway access logs expose the missing invocation permission before Amazon CloudWatch confirms the repaired service.
  • A green GitHub Actions workflow that checks Terraform formatting and validity whenever you push your repository.
  • Secret Mission: Add a controlled failure switch to prove the Lambda error alarm reaches ALARM.

Are there any prerequisites?

You'll need a Mac with Visual Studio Code plus AWS and GitHub accounts.

Your configured AWS CLI identity needs permission to create Lambda, API Gateway, IAM, and CloudWatch resources.

The project walks you through installing the remaining command-line tools.

Before We Start

This is your moment to lock in the goal before the hands-on work begins. You are building an AWS health API that proves you can deploy infrastructure, diagnose a deliberate permission failure, repair it, monitor errors, and automate Terraform checks.

Prepare Your Mac

The AWS health API will soon create real cloud resources. A repeatable local toolchain keeps those changes deliberate and traceable.

In this step, Homebrew installs Terraform CLI 1.16.2, Git, plus GitHub CLI. Identity checks then confirm your AWS CLI session plus your GitHub session before any infrastructure is created.

In this step, get ready to:
  • Set up Homebrew on your Mac.
  • Install Terraform CLI 1.16.2, Git, plus GitHub CLI.
  • Verify your AWS CLI identity plus your GitHub CLI authentication.
Install Homebrew

Homebrew gives your Mac one consistent way to install the command-line tools used throughout this lab. Its installer prints shell setup instructions that connect the brew command to future terminal sessions.

  • Press Cmd+Space to open Spotlight.
  • Type Terminal into Spotlight.
  • Press Return to open Terminal.

The installer may pause while it checks your Mac or downloads prerequisites. Leave Terminal open until the process finishes.

  • Start the official Homebrew installer from Terminal by running this command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

What does this command do?

The command downloads Homebrew's official installation script. It immediately runs that script with Bash.

The installer may request permission to make system changes. Follow the prompts shown in Terminal.

  • Follow the shell setup instructions printed at the end of the installer.
  • Run each printed shell setup command in the order shown.
  • Confirm that Homebrew is available by running:
brew

What does this check prove?

Running brew displays Homebrew's command help. That output proves your shell can find the package manager.

That is your first tool in place. Homebrew can now install the rest of the local toolchain.

Command still unavailable?

Return to the final installer output. Run the shell setup commands it printed for your Mac.

Close Terminal after running those commands. Open it again before retrying brew.

help me make the brew command available after installing Homebrew on macOS

Install Terraform, Git, and GitHub CLI

Terraform turns infrastructure definitions into repeatable AWS changes. This project uses Terraform CLI 1.16.2 so your local checks match the version used later in continuous integration.

  • Add HashiCorp's official Homebrew tap plus install Terraform by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

What do these commands do?

  • The first command adds HashiCorp's package source to Homebrew.
  • The second command installs Terraform from that source.
  • Install Git plus GitHub CLI by running these commands:
brew install git
brew install gh

Why install both tools?

  • Git records local versions of the project files you create.
  • GitHub CLI authenticates your GitHub account from Terminal.
  • GitHub CLI publishes the repository later in the project.
  • Check the installed Terraform version by running:
terraform version

What does this check show?

The output identifies the Terraform CLI version your shell runs. Use the matching tab below to finish this installation check.

✔️ I see the required Terraform version

Great. Terraform 1.16.2 is ready for the configuration you create in the next step.

ⓧ I see an older Terraform version

Your shell is finding an older Homebrew installation of Terraform. Upgrade the HashiCorp formula before continuing.

  • Upgrade the installed Terraform formula by running:
brew upgrade hashicorp/tap/terraform

What does this command change?

Homebrew replaces the outdated Terraform formula with the current release from HashiCorp's tap.

  • Check the Terraform version again by running:
terraform version

What should you confirm?

Confirm that the output now reports Terraform 1.16.2.

ⓧ The Terraform command is missing

Your shell cannot find Terraform yet. Install it from HashiCorp's Homebrew tap.

  • Add the tap plus install Terraform by running:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

What do these commands add?

The first command registers HashiCorp's package source. The second command installs Terraform from that source.

  • Check the Terraform version after installation by running:
terraform version

What should you confirm?

Confirm that the output reports Terraform 1.16.2.

Installation fails?

Confirm that brew still displays its command help. A missing Homebrew command means its shell setup remains incomplete.

Retry the installation after confirming your internet connection. Homebrew needs to download each package.

help me debug a Homebrew installation failure for Terraform, Git, or GitHub CLI

Verify AWS and GitHub sessions

A version check proves that a command exists. An identity check proves that the command can reach the correct account.

You need both signals before Terraform creates AWS resources or GitHub CLI publishes your repository.

  • Verify the AWS CLI plus inspect its active identity by running these commands:
aws --version
aws sts get-caller-identity

What do these AWS checks prove?

  • The version command proves that your shell can find AWS CLI v2.
  • The identity command returns the account plus ARN associated with your active credentials.

You should see AWS CLI version information followed by your current account details. The returned ARN identifies the user or role that Terraform uses.

AWS identity unavailable?

Confirm that your existing AWS CLI credentials are still active. Expired or incomplete credentials prevent the identity request from succeeding.

help me diagnose why AWS CLI cannot return my caller identity

  • Check your current GitHub CLI authentication by running:
gh auth status

What does this check prove?

GitHub CLI checks whether it has a stored authentication token for GitHub. A successful status means the CLI can act through your account.

✔️ GitHub authentication is active

Your GitHub CLI session is ready. You can use it to publish the repository later in the project.

ⓧ GitHub authentication is missing

GitHub CLI needs a browser-based sign-in before it can create your repository.

  • Start the GitHub browser authentication flow by running:
gh auth login --web

What happens during sign-in?

GitHub CLI opens a browser authentication flow. It stores the resulting token in your system credential store when one is available.

  • Complete the sign-in prompts in your browser.
  • Return to Terminal after GitHub confirms authentication.
  • Check your GitHub CLI authentication again by running:
gh auth status

What should you confirm?

The status should confirm that GitHub CLI is authenticated. This gives the CLI permission to publish your project later.

Before the final check, which commands do you expect to prove identity instead of command availability?

  • Run the complete toolchain check by using these commands:
terraform version
git --version
gh --version
aws --version
aws sts get-caller-identity
gh auth status

What does the full check cover?

  • The first four commands confirm that Terraform, Git, GitHub CLI, plus AWS CLI are available.
  • The AWS identity command confirms the account plus ARN behind your active credentials.
  • The GitHub status command confirms your authenticated CLI session.

You should see Terraform 1.16.2 plus successful version output from Git, GitHub CLI, and AWS CLI. You should also see your AWS account details plus a successful GitHub authentication status.

One check still failing?

Focus on the first command that fails. Commands after it may still work because each tool has its own installation plus authentication state.

Avoid sharing AWS credentials or GitHub tokens while debugging. Version output plus redacted account details provide enough context.

help me debug the failing command in my Mac toolchain check

Your Mac is ready to manage the full lab. Next up, you will deploy the Lambda function and prove it works through a direct invocation.

Deploy and Invoke the Lambda

Your authenticated toolchain is ready. That gives Terraform a trusted path into AWS for this deployment.

A directly invoked AWS Lambda function gives you a working service before Amazon API Gateway introduces cross-service permissions. This checkpoint isolates the function code from the HTTP layer.

In this step, get ready to:
  • Create the Python handler with its supporting project files.
  • Provision the Lambda function with a seven-day log group.
  • Invoke the function directly to verify its healthy response.
Create the project files

The local files define the function plus every AWS resource it needs. You will keep them in an aws-health-api folder on your Desktop.

  • Press Cmd+Space to open macOS search.
  • Type Visual Studio Code and press Enter.
  • Select Open Folder from the Visual Studio Code welcome screen.
  • Select your Desktop in the file chooser.
  • Create a folder named aws-health-api.
  • Select the aws-health-api folder.
  • Click Open.

You will see the empty aws-health-api folder in the Explorer sidebar.

Why Terraform for this lab?

Terraform records the Lambda function plus its supporting resources as infrastructure as code. The same files can recreate the lab after cleanup.

This repeatability keeps the project focused on deployment plus troubleshooting. Every infrastructure change remains reviewable.

  • Select the new-file icon in the Explorer sidebar.
  • Name the file .gitignore.
  • Add the local exclusions by pasting this content:
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
crash.log
lambda_function.zip
response.json

What does this file protect?

The ignore rules keep generated provider files plus Terraform state out of future Git commits. They also exclude deployment packages plus local response files.

Terraform state can contain details about deployed resources. Keeping it outside the repository avoids publishing local infrastructure data.

  • Save .gitignore.
  • Confirm that .gitignore remains listed in the Explorer sidebar.

Missing the ignore file?

Confirm that the filename starts with a single dot. Make sure Visual Studio Code created it inside aws-health-api.

Ask for help if the file disappears after you save it: help me find a hidden .gitignore file in my Visual Studio Code project

The handler receives an event from Lambda. It returns an HTTP-shaped response that the API layer can reuse later.

  • Select the new-file icon in the Explorer sidebar.
  • Name the file lambda_function.py.
  • Define the health handler by pasting this code:
import json


def lambda_handler(event, context):
    print(json.dumps({"event": "health_check", "status": "healthy"}))

    return {
        "statusCode": 200,
        "headers": {"content-type": "application/json"},
        "body": json.dumps(
            {
                "status": "healthy",
                "service": "my-function",
            }
        ),
    }

What does this handler do?

  • The lambda_handler() function is the entry point that Lambda calls.
  • The log statement records a health-check event for later inspection.
  • The return value includes a successful status code plus a JSON response body.
  • Save lambda_function.py.
  • Confirm that Visual Studio Code shows Python syntax highlighting in the file.

Seeing Python syntax warnings?

Check the indentation inside lambda_handler(). Confirm that every opening brace has a matching closing brace.

Ask for help with the handler if the warning remains: help me compare my Lambda handler with the expected Python structure

The first section of main.tf pins Terraform CLI 1.16.2 plus the two provider versions. It also fixes the deployment region at us-east-1.

  • Select the new-file icon in the Explorer sidebar.
  • Name the file main.tf.
  • Add the Terraform requirements plus project settings by pasting this code:
terraform {
  required_version = "= 1.16.2"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 6.64.0"
    }

    archive = {
      source  = "hashicorp/archive"
      version = "= 2.8.1"
    }
  }
}

variable "aws_region" {
  description = "AWS Region for the lab"
  type        = string
  default     = "us-east-1"
}

provider "aws" {
  region = var.aws_region
}

locals {
  function_name = "my-function"
  api_name      = "cloud-engineer-health-api"
}

What does this configuration establish?

  • The terraform block pins Terraform CLI 1.16.2 plus AWS provider 6.64.0 plus Archive provider 2.8.1.
  • The aws_region variable keeps every resource in us-east-1.
  • The locals block gives the function plus the later API stable names.
  • Save main.tf.
  • Confirm that Visual Studio Code recognizes the file as Terraform configuration.

Does the Terraform file look unformatted?

Confirm that the filename ends with .tf. Keep every nested block inside its matching braces.

Ask for help if the structure remains unclear: help me check the provider and region blocks in my main.tf file

Lambda needs a ZIP deployment package plus an execution role. The archive data source creates the package from lambda_function.py.

  • Place the packaging plus execution-role configuration at the end of main.tf by pasting this code:
data "archive_file" "lambda" {
  type        = "zip"
  source_file = "${path.module}/lambda_function.py"
  output_path = "${path.module}/lambda_function.zip"
}

resource "aws_iam_role" "lambda_exec" {
  name = "cloud-engineer-health-api-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

How does Lambda receive its code and role?

The Archive provider packages the Python file as lambda_function.zip. Terraform can recreate that package whenever the handler changes.

The AWS Identity and Access Management role trusts the Lambda service principal. Lambda assumes this role while the function runs.

  • Save main.tf.
  • Confirm that the file now contains the archive_file.lambda data source plus the aws_iam_role.lambda_exec resource.

Missing the packaging or role blocks?

Make sure you pasted both blocks after the closing brace of locals. Keep one blank line between the two blocks.

Ask for help if braces appear mismatched: help me locate a misplaced brace in my Terraform archive and IAM role configuration

The execution role needs permission to create log streams plus publish log events. An inline policy keeps those permissions attached to this lab role.

  • Place the logging policy at the end of main.tf by pasting this code:
resource "aws_iam_role_policy" "lambda_logs" {
  name = "cloud-engineer-health-api-logs"
  role = aws_iam_role.lambda_exec.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "*"
      }
    ]
  })
}

What does the logging policy allow?

The policy allows Lambda to create a log group plus a log stream. It also permits the function to publish log events.

The policy attaches to aws_iam_role.lambda_exec. The function receives these permissions when it assumes that role.

  • Save main.tf.
  • Confirm that the policy lists three logging actions.

Does the policy block show warnings?

Check the commas between the three logging actions. Confirm that the Statement value remains inside square brackets.

Ask for help if the policy remains invalid: help me debug the jsonencode structure in my Lambda logging policy

A dedicated Amazon CloudWatch log group gives the function a predictable destination. A seven-day retention period keeps this short lab from storing logs indefinitely.

  • Place the log group plus Lambda function configuration at the end of main.tf by pasting this code:
resource "aws_cloudwatch_log_group" "lambda" {
  name              = "/aws/lambda/${local.function_name}"
  retention_in_days = 7
}

resource "aws_lambda_function" "health" {
  filename      = data.archive_file.lambda.output_path
  function_name = local.function_name
  role          = aws_iam_role.lambda_exec.arn
  handler       = "lambda_function.lambda_handler"
  runtime       = "python3.13"
  code_sha256   = data.archive_file.lambda.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.lambda,
    aws_iam_role_policy.lambda_logs
  ]
}

output "lambda_function_name" {
  description = "Lambda function name"
  value       = aws_lambda_function.health.function_name
}

How does Terraform assemble the function?

  • The log group uses the final path /aws/lambda/my-function with seven-day retention.
  • The Lambda resource runs the lambda_function.lambda_handler entry point on Python 3.13.
  • The code_sha256 value lets Terraform detect changes to the deployment package.
  • The dependency list ensures that logging permissions plus the log group exist before the function.
  • The output exposes the deployed function name after the apply completes.
  • Save main.tf.
  • Confirm that the final resource is aws_lambda_function.health.
  • Confirm that the final output is lambda_function_name.

Is the Lambda resource incomplete?

Check that the handler value matches the lambda_function.py filename plus the lambda_handler() function name. Confirm that the runtime value is python3.13.

Ask for help if references remain unresolved: help me trace the references used by my Terraform Lambda resource

✔️ Awesome, I've got everything!

Your three project files are ready. Make sure each file is saved before you initialize Terraform.

ⓧ I'd like to double check the full code

  • Compare .gitignore with this reference:
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
crash.log
lambda_function.zip
response.json
  • Compare lambda_function.py with this reference:
import json


def lambda_handler(event, context):
    print(json.dumps({"event": "health_check", "status": "healthy"}))

    return {
        "statusCode": 200,
        "headers": {"content-type": "application/json"},
        "body": json.dumps(
            {
                "status": "healthy",
                "service": "my-function",
            }
        ),
    }
  • Compare main.tf with this reference:
terraform {
  required_version = "= 1.16.2"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 6.64.0"
    }

    archive = {
      source  = "hashicorp/archive"
      version = "= 2.8.1"
    }
  }
}

variable "aws_region" {
  description = "AWS Region for the lab"
  type        = string
  default     = "us-east-1"
}

provider "aws" {
  region = var.aws_region
}

locals {
  function_name = "my-function"
  api_name      = "cloud-engineer-health-api"
}

data "archive_file" "lambda" {
  type        = "zip"
  source_file = "${path.module}/lambda_function.py"
  output_path = "${path.module}/lambda_function.zip"
}

resource "aws_iam_role" "lambda_exec" {
  name = "cloud-engineer-health-api-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "lambda_logs" {
  name = "cloud-engineer-health-api-logs"
  role = aws_iam_role.lambda_exec.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_cloudwatch_log_group" "lambda" {
  name              = "/aws/lambda/${local.function_name}"
  retention_in_days = 7
}

resource "aws_lambda_function" "health" {
  filename      = data.archive_file.lambda.output_path
  function_name = local.function_name
  role          = aws_iam_role.lambda_exec.arn
  handler       = "lambda_function.lambda_handler"
  runtime       = "python3.13"
  code_sha256   = data.archive_file.lambda.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.lambda,
    aws_iam_role_policy.lambda_logs
  ]
}

output "lambda_function_name" {
  description = "Lambda function name"
  value       = aws_lambda_function.health.function_name
}
Provision the Lambda with Terraform

Terraform now has a complete configuration to initialize plus validate. The apply uses your authenticated AWS CLI credentials to create the resources in us-east-1.

  • Select Terminal from the Visual Studio Code menu bar.
  • Select New Terminal.

The integrated terminal starts inside aws-health-api. The first initialization can take a minute while Terraform downloads the pinned providers.

  • Initialize the Terraform working directory by running this command:
terraform init

What did Terraform initialize?

Terraform downloads hashicorp/aws version 6.64.0 plus hashicorp/archive version 2.8.1. It also prepares the local working directory for planning.

You should see initialization finish successfully. The .terraform directory now holds the provider files.

  • Format the Terraform configuration by running this command:
terraform fmt

What did Terraform format?

Terraform applies its canonical spacing to main.tf. Consistent formatting makes infrastructure changes easier to review.

  • Validate the configuration by running this command:
terraform validate

What did Terraform validate?

Terraform checks the configuration syntax plus provider argument types. You should see a success message confirming that the configuration is valid.

Did initialization or validation fail?

Confirm that the terminal is inside aws-health-api. Check that main.tf plus lambda_function.py appear in the Explorer sidebar.

Ask for help with the exact terminal output: help me diagnose my Terraform initialization or validation failure

A Terraform plan previews the resources before AWS changes. This gives you a review point for the role plus policy plus log group plus function.

  • Preview the infrastructure changes by running this command:
terraform plan

What should the plan contain?

The plan should include the Lambda execution role plus its inline logging policy. It should also include the seven-day log group plus the my-function Lambda function.

Review the resource names before continuing. The plan should contain no API Gateway resources yet.

Before You Approve

The next command creates usage-based AWS resources in your account. The few requests in this lab are expected to remain within applicable free allowances.

Eligibility depends on your AWS account. Cleanup at the end of the project removes the managed resources.

  • Apply the reviewed Terraform configuration by running this command:
terraform apply

What happens during the apply?

Terraform creates a fresh plan plus asks for approval. Approve the plan after confirming that it matches the resources you reviewed.

Terraform packages lambda_function.py plus creates the AWS resources. It finishes by showing the function-name output as my-function.

That is the deployment complete. Your Lambda function now has its code plus execution role plus retained log group.

Did the apply stop before completion?

Check whether the active AWS identity can create Lambda plus IAM plus CloudWatch resources. Confirm that the plan still targets us-east-1.

Ask for help with the resource named in the failure: help me diagnose the failed Terraform apply for my AWS Lambda resources

Invoke the function directly

A direct invocation bypasses the future HTTP integration. This proves that the handler plus execution role work before another AWS service enters the request path.

Before you run the invocation, what response do you expect from a handler that returns a healthy body?

  • Invoke my-function synchronously with an empty JSON payload by running this command:
aws lambda invoke \
  --function-name my-function \
  --payload '{}' \
  --cli-binary-format raw-in-base64-out \
  response.json

What does this invocation do?

  • The command invokes my-function synchronously.
  • The empty JSON object becomes the Lambda event.
  • The raw-in-base64-out format lets AWS CLI v2 pass the JSON payload directly.
  • The function response is written to response.json.

You should see the AWS CLI report a StatusCode of 200. That result proves Lambda completed the synchronous invocation successfully.

  • Select response.json in the Visual Studio Code Explorer sidebar.

The response contains "status": "healthy" plus the service name my-function. Your service now works when invoked directly.

Did the invocation fail?

Confirm that the apply completed before the invocation. Check that the function name in the command matches my-function exactly.

If response.json contains a function error, compare lambda_function.py with the full-code reference. Save the file plus apply the configuration again after correcting it.

Ask for help with the invocation output: help me diagnose why my directly invoked Lambda did not return the healthy response

Your function now works in isolation. Next, you will place an HTTP endpoint in front of it plus uncover the missing cross-service permission.

Expose the Broken HTTP API

Your AWS Lambda function already returns a healthy response when the AWS CLI invokes it directly. The next test puts a public HTTP layer in front of that working function.

An Amazon API Gateway HTTP API receives HTTP requests and hands them to Lambda. This step uses Terraform to create that path so you can test the cross-service boundary that direct invocation never touched.

In this step, get ready to:
  • Define the complete HTTP request path in main.tf.
  • Apply the five new AWS resources with Terraform.
  • Reproduce the cross-service failure through the public endpoint.
Create the HTTP API integration

The HTTP API needs its own access log destination. Those logs give Amazon CloudWatch a record of each request that reaches the integration boundary.

  • In main.tf find the existing output "lambda_function_name" { block.
  • Create the API access log group immediately above that output block by pasting:
resource "aws_cloudwatch_log_group" "api" {
  name              = "/aws/api_gw/${local.api_name}"
  retention_in_days = 7
}

What does this resource do?

  • The log group gives API Gateway a dedicated destination for request records.
  • The seven-day retention period limits how long those records remain stored.
  • Save main.tf.
  • Check the file formatting and configuration by running:
terraform fmt
terraform validate

What do these checks prove?

The first command applies canonical HCL formatting. The second confirms that Terraform can understand the resource and its references.

Terraform should finish both checks successfully. Your API access log group is now valid configuration.

Is the log group invalid?

Check that the resource sits outside every existing block in main.tf. A missing closing brace can make Terraform treat it as part of the Lambda resource.

Ask for help with the exact validation output: help me diagnose this Terraform log group validation error.

The next resource creates the HTTP API itself. Its name comes from the existing api_name local so every related resource uses the same identifier.

  • Create the HTTP API immediately below the API log group by pasting:
resource "aws_apigatewayv2_api" "health" {
  name          = local.api_name
  protocol_type = "HTTP"
}

What does this resource do?

  • The resource creates the API named cloud-engineer-health-api.
  • The protocol type selects the HTTP API feature set.
  • Save main.tf.
  • Check the updated configuration by running:
terraform fmt
terraform validate

What does this check confirm?

Terraform confirms that the HTTP API resource can read the existing api_name local. Successful validation also confirms that the resource is positioned correctly in the file.

You should see a successful validation result with no configuration errors.

Is the API resource failing validation?

Compare local.api_name with the existing local declaration near the top of main.tf. The spelling must match exactly.

Ask for help with the validation details: help me fix this Terraform HTTP API validation error.

An integration tells API Gateway where to send a matched request. The Lambda proxy configuration forwards the request as a payload format 2.0 event.

  • Connect the HTTP API to the existing Lambda function by pasting this block below the API resource:
resource "aws_apigatewayv2_integration" "health" {
  api_id                 = aws_apigatewayv2_api.health.id
  integration_type       = "AWS_PROXY"
  integration_method     = "POST"
  integration_uri        = aws_lambda_function.health.invoke_arn
  payload_format_version = "2.0"
}

How does the integration work?

  • The API identifier attaches this integration to the HTTP API you just defined.
  • The proxy type forwards the request to Lambda without a custom mapping template.
  • The invocation ARN identifies my-function as the integration target.
  • Payload format 2.0 controls the event structure that Lambda receives.
  • Save main.tf.
  • Check the integration references by running:
terraform fmt
terraform validate

What does this check confirm?

Terraform resolves references to both the new HTTP API and the existing Lambda function. A successful result proves that the integration is connected correctly inside the configuration.

You should see a successful validation result with no missing resource references.

Is the integration reference invalid?

Check that aws_apigatewayv2_api.health and aws_lambda_function.health match the resource names already present in main.tf.

Ask for help with the failing reference: help me debug this Terraform API Gateway integration error.

Route requests and deploy the stage

The integration has a target but no public route yet. A route matches an HTTP method and path before sending the request to that integration.

  • Add the GET /hello route below the integration by pasting:
resource "aws_apigatewayv2_route" "health" {
  api_id    = aws_apigatewayv2_api.health.id
  route_key = "GET /hello"
  target    = "integrations/${aws_apigatewayv2_integration.health.id}"
}

How does the route work?

  • The route key matches requests that use the GET method at /hello.
  • The target sends every match to the Lambda proxy integration.
  • Save main.tf.
  • Check the route references by running:
terraform fmt
terraform validate

What does this check confirm?

Terraform confirms that the route belongs to the HTTP API and targets the integration. This catches misspelled resource names before deployment.

You should see a successful validation result for the new route.

Is the route target invalid?

Check the interpolation inside target. It must reference the health integration identifier.

Ask for help comparing the route with the integration: help me fix this Terraform API Gateway route reference.

A stage makes the route callable at an API URL. The $default stage also sends each request record to the API log group.

  • Create the auto-deploy stage below the route by pasting:
resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.health.id
  name        = "$default"
  auto_deploy = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api.arn
    format = jsonencode({
      requestId               = "$context.requestId"
      routeKey                = "$context.routeKey"
      status                  = "$context.status"
      integrationErrorMessage = "$context.integrationErrorMessage"
    })
  }
}

What does the stage record?

  • Automatic deployment publishes route changes without a separate deployment resource.
  • The destination ARN sends access records to the API log group.
  • The log format captures the request identifier and matched route.
  • The log format also captures the response status and integration error details.
  • Save main.tf.
  • Check the stage and log settings by running:
terraform fmt
terraform validate

What does this check confirm?

Terraform verifies the stage references and access log structure. The check also confirms that every context field is valid HCL text.

You should see a successful validation result for the stage configuration.

Are the access logs invalid?

Check the braces around access_log_settings and jsonencode. Each nested block must close before the stage resource closes.

Ask for help with the nested HCL structure: help me fix this Terraform API Gateway access log configuration.

Terraform outputs expose values that you need after deployment. The API URL becomes the entry point for the designed request test.

  • Find the existing output "lambda_function_name" { block near the bottom of main.tf.
  • Add the API URL and log group outputs immediately below that existing output by pasting:
output "base_url" {
  description = "Base URL for the API Gateway HTTP API"
  value       = aws_apigatewayv2_api.health.api_endpoint
}

output "lambda_log_group" {
  description = "Lambda CloudWatch log group"
  value       = aws_cloudwatch_log_group.lambda.name
}

output "api_log_group" {
  description = "API Gateway CloudWatch access log group"
  value       = aws_cloudwatch_log_group.api.name
}

Why expose these outputs?

  • The base URL output gives your terminal the deployed API endpoint.
  • The Lambda log group output identifies the function log destination.
  • The API log group output identifies the request access log destination.
  • Save main.tf.
  • Check the complete configuration by running:
terraform fmt
terraform validate

What does the final check prove?

Terraform formats the full file before validating every new resource and output. A successful result means the configuration is ready for a deployment plan.

You should see a successful validation result with no unresolved output references.

Is an output reference failing?

Check that the output values reference the exact resource names defined above them. Pay close attention to aws_cloudwatch_log_group.api and aws_apigatewayv2_api.health.

Ask for help tracing the failed output: help me debug this Terraform output reference.

A Terraform plan shows the AWS changes before they happen. This checkpoint lets you confirm that the existing Lambda stays in place while the HTTP layer is added.

  • Preview the infrastructure changes by running:
terraform plan

What does the plan show?

Terraform compares the configuration with the deployed stack. The plan should propose five new resources for the API path without replacing the existing Lambda function.

  • Review the plan for the API log group.
  • Review the plan for the HTTP API.
  • Review the plan for the Lambda integration.
  • Review the plan for the route.
  • Review the plan for the default stage.

Does the plan replace the Lambda?

Stop before applying if the plan proposes replacing my-function. Compare the unchanged Lambda block with the version from the previous step.

Ask for help reviewing the unexpected plan: help me understand why Terraform wants to replace my Lambda function.

Applying the plan changes your AWS account. Terraform pauses for confirmation so you have one final checkpoint before it creates the resources.

  • Start the infrastructure deployment by running:
terraform apply

What does this command do?

Terraform creates a fresh execution plan before asking for approval. After approval it creates the five API resources and records them in local state.

  • Compare the fresh plan with the one you reviewed.
  • Approve the plan at Terraform's confirmation prompt.
  • Wait for Terraform to report completion.

You should see five resources added. The output section should now include the API base URL and both log group names.

Did the apply fail?

Check the first reported AWS error because later failures can be consequences of the same cause. Common causes here include insufficient account permissions or an unavailable resource name.

Ask for help with the apply output: help me diagnose this Terraform apply failure.

✔️ Awesome, I've got everything!

Great. Save main.tf before testing the endpoint.

ⓧ I'd like to double check the full code

terraform {
  required_version = "= 1.16.2"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 6.64.0"
    }

    archive = {
      source  = "hashicorp/archive"
      version = "= 2.8.1"
    }
  }
}

variable "aws_region" {
  description = "AWS Region for the lab"
  type        = string
  default     = "us-east-1"
}

provider "aws" {
  region = var.aws_region
}

locals {
  function_name = "my-function"
  api_name      = "cloud-engineer-health-api"
}

data "archive_file" "lambda" {
  type        = "zip"
  source_file = "${path.module}/lambda_function.py"
  output_path = "${path.module}/lambda_function.zip"
}

resource "aws_iam_role" "lambda_exec" {
  name = "cloud-engineer-health-api-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "lambda_logs" {
  name = "cloud-engineer-health-api-logs"
  role = aws_iam_role.lambda_exec.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_cloudwatch_log_group" "lambda" {
  name              = "/aws/lambda/${local.function_name}"
  retention_in_days = 7
}

resource "aws_lambda_function" "health" {
  filename      = data.archive_file.lambda.output_path
  function_name = local.function_name
  role          = aws_iam_role.lambda_exec.arn
  handler       = "lambda_function.lambda_handler"
  runtime       = "python3.13"
  code_sha256   = data.archive_file.lambda.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.lambda,
    aws_iam_role_policy.lambda_logs
  ]
}

resource "aws_cloudwatch_log_group" "api" {
  name              = "/aws/api_gw/${local.api_name}"
  retention_in_days = 7
}

resource "aws_apigatewayv2_api" "health" {
  name          = local.api_name
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_integration" "health" {
  api_id                 = aws_apigatewayv2_api.health.id
  integration_type       = "AWS_PROXY"
  integration_method     = "POST"
  integration_uri        = aws_lambda_function.health.invoke_arn
  payload_format_version = "2.0"
}

resource "aws_apigatewayv2_route" "health" {
  api_id    = aws_apigatewayv2_api.health.id
  route_key = "GET /hello"
  target    = "integrations/${aws_apigatewayv2_integration.health.id}"
}

resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.health.id
  name        = "$default"
  auto_deploy = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api.arn
    format = jsonencode({
      requestId               = "$context.requestId"
      routeKey                = "$context.routeKey"
      status                  = "$context.status"
      integrationErrorMessage = "$context.integrationErrorMessage"
    })
  }
}

output "lambda_function_name" {
  description = "Lambda function name"
  value       = aws_lambda_function.health.function_name
}

output "base_url" {
  description = "Base URL for the API Gateway HTTP API"
  value       = aws_apigatewayv2_api.health.api_endpoint
}

output "lambda_log_group" {
  description = "Lambda CloudWatch log group"
  value       = aws_cloudwatch_log_group.lambda.name
}

output "api_log_group" {
  description = "API Gateway CloudWatch access log group"
  value       = aws_cloudwatch_log_group.api.name
}
Reproduce and diagnose the failure

The public route now exists. The next request tests whether API Gateway can complete the handoff to the working Lambda function.

Before you run this check, do you think the same healthy response from direct invocation will reach the HTTP client?

  • Call the public GET /hello route by running:
curl "$(terraform output -raw base_url)/hello"

What did the request reveal?

You should see {"message":"Internal Server Error"}. This failure is intentional because the HTTP integration cannot yet invoke the function.

The generic client response proves the route is reachable. The access log now holds the specific cause.

Seeing a different response?

If the command cannot read base_url confirm that the apply completed and the output exists in main.tf.

If the request cannot connect check that the stage was created successfully in the apply output.

Ask for help with the response you received: help me diagnose my unexpected API Gateway response.

The client only sees a generic server error. API access logging exposes the integration detail that explains why the handoff failed.

  • Sign in to the AWS Management Console in your browser.
  • Open API Gateway in the AWS Management Console.
  • Select cloud-engineer-health-api.
  • Select Monitor.
  • Select Logging.
  • Confirm that the access log destination is /aws/api_gw/cloud-engineer-health-api.

The destination confirms that the failed request was sent to the log group created by Terraform.

  • Follow the access log destination into CloudWatch.
  • Open the most recent log stream.
  • Expand the newest request event.

The event should contain integrationErrorMessage with a message explaining that API Gateway lacks permission to invoke the Lambda function. This access log turns a generic server error into a specific IAM diagnosis.

You have reproduced a real cross-service permission failure and traced it to its source. Next up, you will repair that boundary and add an alarm that watches the function for errors.

Fix IAM and Add Monitoring

Your API Gateway endpoint now exposes a real cross-service failure. Its access log has shown that API Gateway cannot invoke the Lambda function.

The repair uses a resource-based IAM permission scoped to this HTTP API. You will then add a CloudWatch alarm that watches the function for runtime errors.

In this step, get ready to:
  • Grant API Gateway permission to invoke the Lambda function.
  • Create a CloudWatch alarm for Lambda errors.
  • Confirm the repaired request appears in the function logs.
Grant API Gateway invoke permission

A Lambda resource policy controls which AWS services can invoke the function. Terraform can add the missing permission as part of the same infrastructure stack.

Changing an AWS permission can feel sensitive. The source ARN limits this grant to your HTTP API.

  • Select main.tf in the Explorer sidebar.
  • Place your cursor after the closing brace of aws_apigatewayv2_stage.default.
  • Paste this permission resource at that location:
resource "aws_lambda_permission" "api_gw" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.health.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.health.execution_arn}/*/*"
}

What does this permission do?

  • The aws_lambda_permission.api_gw resource adds an invocation rule to the Lambda function.
  • The apigateway.amazonaws.com principal identifies API Gateway as the allowed caller.
  • The source_arn pattern limits the rule to the execution ARN of this HTTP API.
  • Save main.tf.
  • Apply the permission by running this command. Terraform presents a fresh plan before requesting approval:
terraform apply

What does this command do?

Terraform compares main.tf with the deployed stack. It creates the Lambda permission only after you approve the proposed change.

  • Review the plan for the new aws_lambda_permission.api_gw resource.
  • Enter yes at the approval prompt.

Terraform creates the permission while leaving the existing API resources in place.

  • Predict whether the endpoint now returns healthy JSON or the same server error.
  • Retry the HTTP endpoint by running:
curl "$(terraform output -raw base_url)/hello"

What does this check prove?

The command reads the deployed API base URL from Terraform state. It sends a request to the GET /hello route.

You should see {"status":"healthy","service":"my-function"}. That is the permission failure repaired because API Gateway can now reach the function.

Still seeing an internal server error?

Confirm that aws_lambda_permission.api_gw appears in the completed Terraform apply results.

Check that the permission uses the execution ARN from aws_apigatewayv2_api.health.

Ask for help if the repaired endpoint still fails: help me diagnose why API Gateway still cannot invoke my-function after applying aws_lambda_permission.api_gw.

Create the Lambda error alarm

The repaired endpoint proves that requests can reach Lambda. An error alarm now gives the service a visible signal when an invocation ends in a function or runtime error.

  • Place your cursor after the closing brace of aws_lambda_permission.api_gw.
  • Paste this alarm resource at that location:
resource "aws_cloudwatch_metric_alarm" "lambda_errors" {
  alarm_name          = "myalarm"
  alarm_description   = "Alarm when my-function records at least one error in one minute"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = "Errors"
  namespace           = "AWS/Lambda"
  period              = 60
  statistic           = "Sum"
  threshold           = 1
  treat_missing_data  = "notBreaching"

  dimensions = {
    FunctionName = aws_lambda_function.health.function_name
  }
}

How does the alarm work?

  • The AWS/Lambda namespace supplies the managed Lambda metrics.
  • The Errors metric counts invocations that end in a function or runtime error.
  • The FunctionName dimension scopes the alarm to my-function.
  • The alarm evaluates the one-minute Sum against a threshold of one error.
  • Save main.tf.
  • Validate the expanded Terraform configuration by running:
terraform validate -no-color

What does validation check?

Terraform checks the configuration syntax. It also checks resource arguments against the initialized provider schemas.

Terraform confirms that the configuration is valid before you continue.

  • Scroll to the output "api_log_group" block near the bottom of main.tf.
  • Paste this output directly after that block:
output "alarm_name" {
  description = "CloudWatch Lambda error alarm"
  value       = aws_cloudwatch_metric_alarm.lambda_errors.alarm_name
}

Why add an alarm output?

The alarm_name output exposes the deployed alarm name through Terraform. This keeps the identifier connected to the managed resource.

  • Save main.tf.
  • Create the alarm by running this command. Terraform requests approval after showing the proposed changes:
terraform apply

What changes this time?

Terraform proposes the aws_cloudwatch_metric_alarm.lambda_errors resource. It also records the new alarm_name output.

  • Review the plan for the new CloudWatch alarm.
  • Enter yes at the approval prompt.

Terraform creates an alarm named myalarm for the Lambda error metric.

Alarm creation failed?

Confirm that metric_name is Errors.

Confirm that the FunctionName dimension references aws_lambda_function.health.function_name.

Ask for help with the failed alarm deployment: help me debug why Terraform cannot create aws_cloudwatch_metric_alarm.lambda_errors.

✔️ Awesome, I've got everything!

Great. Double check that main.tf is saved before inspecting the logs.

ⓧ I'd like to double check the full code

Your final main.tf should match this file exactly.

terraform {
  required_version = "= 1.16.2"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 6.64.0"
    }

    archive = {
      source  = "hashicorp/archive"
      version = "= 2.8.1"
    }
  }
}

variable "aws_region" {
  description = "AWS Region for the lab"
  type        = string
  default     = "us-east-1"
}

provider "aws" {
  region = var.aws_region
}

locals {
  function_name = "my-function"
  api_name      = "cloud-engineer-health-api"
}

data "archive_file" "lambda" {
  type        = "zip"
  source_file = "${path.module}/lambda_function.py"
  output_path = "${path.module}/lambda_function.zip"
}

resource "aws_iam_role" "lambda_exec" {
  name = "cloud-engineer-health-api-lambda-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "lambda_logs" {
  name = "cloud-engineer-health-api-logs"
  role = aws_iam_role.lambda_exec.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "*"
      }
    ]
  })
}

resource "aws_cloudwatch_log_group" "lambda" {
  name              = "/aws/lambda/${local.function_name}"
  retention_in_days = 7
}

resource "aws_lambda_function" "health" {
  filename      = data.archive_file.lambda.output_path
  function_name = local.function_name
  role          = aws_iam_role.lambda_exec.arn
  handler       = "lambda_function.lambda_handler"
  runtime       = "python3.13"
  code_sha256   = data.archive_file.lambda.output_base64sha256

  depends_on = [
    aws_cloudwatch_log_group.lambda,
    aws_iam_role_policy.lambda_logs
  ]
}

resource "aws_cloudwatch_log_group" "api" {
  name              = "/aws/api_gw/${local.api_name}"
  retention_in_days = 7
}

resource "aws_apigatewayv2_api" "health" {
  name          = local.api_name
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_integration" "health" {
  api_id                 = aws_apigatewayv2_api.health.id
  integration_type       = "AWS_PROXY"
  integration_method     = "POST"
  integration_uri        = aws_lambda_function.health.invoke_arn
  payload_format_version = "2.0"
}

resource "aws_apigatewayv2_route" "health" {
  api_id    = aws_apigatewayv2_api.health.id
  route_key = "GET /hello"
  target    = "integrations/${aws_apigatewayv2_integration.health.id}"
}

resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.health.id
  name        = "$default"
  auto_deploy = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api.arn
    format = jsonencode({
      requestId               = "$context.requestId"
      routeKey                = "$context.routeKey"
      status                  = "$context.status"
      integrationErrorMessage = "$context.integrationErrorMessage"
    })
  }
}

resource "aws_lambda_permission" "api_gw" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.health.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.health.execution_arn}/*/*"
}

resource "aws_cloudwatch_metric_alarm" "lambda_errors" {
  alarm_name          = "myalarm"
  alarm_description   = "Alarm when my-function records at least one error in one minute"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  metric_name         = "Errors"
  namespace           = "AWS/Lambda"
  period              = 60
  statistic           = "Sum"
  threshold           = 1
  treat_missing_data  = "notBreaching"

  dimensions = {
    FunctionName = aws_lambda_function.health.function_name
  }
}

output "lambda_function_name" {
  description = "Lambda function name"
  value       = aws_lambda_function.health.function_name
}

output "base_url" {
  description = "Base URL for the API Gateway HTTP API"
  value       = aws_apigatewayv2_api.health.api_endpoint
}

output "lambda_log_group" {
  description = "Lambda CloudWatch log group"
  value       = aws_cloudwatch_log_group.lambda.name
}

output "api_log_group" {
  description = "API Gateway CloudWatch access log group"
  value       = aws_cloudwatch_log_group.api.name
}

output "alarm_name" {
  description = "CloudWatch Lambda error alarm"
  value       = aws_cloudwatch_metric_alarm.lambda_errors.alarm_name
}
Inspect the successful invocation

A healthy client response confirms the integration path. The corresponding function log proves that Lambda executed the request.

  • Predict what a successful request should leave behind in the Lambda log group.
  • Generate a fresh invocation by running:
curl "$(terraform output -raw base_url)/hello"

Why invoke the endpoint again?

This request creates a fresh execution to find in the function logs. The response should contain "status":"healthy".

The newest log stream can be a little fiddly to identify when several streams have similar names.

  • Return to CloudWatch in the AWS Management Console from the previous step.
  • Navigate to the log group named /aws/lambda/my-function.
  • Choose the log stream with the most recent activity.
  • Find the log event containing "event": "health_check".
  • Confirm that the same event contains "status": "healthy".

That is the repaired path confirmed from both sides. The client receives healthy JSON while Lambda records the successful invocation.

  • Predict which alarm name should appear in the AWS CLI response.
  • Retrieve the deployed alarm by running:
aws cloudwatch describe-alarms --alarm-names "myalarm"

What does this command verify?

The AWS CLI requests the configuration for the alarm named myalarm. The response proves that CloudWatch can retrieve the deployed alarm.

You should see an alarm named myalarm with the AWS/Lambda namespace. Its metric name should be Errors.

Alarm missing from the response?

Confirm that your AWS CLI session uses the same account as the Terraform deployment.

Confirm that the active AWS region is us-east-1.

Ask for help if the alarm still does not appear: help me find why aws cloudwatch describe-alarms cannot retrieve myalarm after Terraform created it.

Your API is repaired, logged, and monitored. Next, you will add YAML validation with GitHub Actions before publishing the project.

Add YAML CI and Publish to GitHub

Your repaired endpoint now answers healthy requests. Amazon CloudWatch watches its errors.

The repository still relies on manual Terraform checks. GitHub Actions solves that with a YAML workflow that runs the same checks on every push.

In this step, get ready to:
  • Write a YAML workflow that checks Terraform formatting and validity.
  • Document the architecture and repaired permission failure in a README.
  • Publish the project as a public GitHub repository and verify its workflow.
Create the YAML workflow

YAML uses indentation to show which settings belong together. This workflow connects its triggers, permissions, runner, and validation steps through that indentation.

  • Use the file tree in Visual Studio Code to create .github/workflows/terraform-check.yml inside the open project folder.
  • Paste this complete workflow into .github/workflows/terraform-check.yml:
name: Terraform checks

on:
  push:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v4.0.1
        with:
          terraform_version: "1.16.2"

      - name: Check Terraform formatting
        run: terraform fmt -check -recursive

      - name: Initialize providers without a backend
        run: terraform init -backend=false

      - name: Validate Terraform configuration
        run: terraform validate -no-color

How does this workflow fit together?

  • The push trigger runs the workflow when new commits reach GitHub.
  • The workflow_dispatch trigger also lets you start a run manually.
  • The contents: read permission gives the workflow read-only repository access.
  • The checkout action retrieves your files. The setup action installs Terraform 1.16.2.
  • The final three steps check formatting, initialize providers without a backend, and validate the configuration.
  • Save .github/workflows/terraform-check.yml.

Before you run the checks, do you expect all three to pass against the project on your Mac? Take a moment to predict your answer.

  • Check the Terraform formatting by running:
terraform fmt -check -recursive

What should this check show?

A silent return to the prompt means Terraform found no formatting differences. Your existing main.tf already follows canonical formatting.

  • Initialize the providers without accessing a backend by running:
terraform init -backend=false

Why skip the backend?

The workflow only needs provider plugins and modules to validate the configuration. It does not need access to your local Terraform state.

  • Validate the Terraform configuration by running:
terraform validate -no-color

What does validation prove?

Terraform checks the configuration syntax, arguments, attribute names, and types. The command completes without validation errors when the configuration is ready for CI.

Good result. Your workflow's three commands now pass against the same project GitHub will check.

Did a local check fail?

  • Check the indentation in .github/workflows/terraform-check.yml against the reference below.
  • Format main.tf with Terraform before repeating the formatting check.
  • Confirm your internet connection if provider initialization cannot download a required plugin.

Ask for help with the exact check that failed: help me diagnose a failing local Terraform workflow check

✔️ Awesome, I've got everything!

Your saved workflow contains both triggers, read-only permissions, the pinned setup actions, and all three Terraform checks.

ⓧ I'd like to double check the full code

  • Compare .github/workflows/terraform-check.yml with this complete reference:
name: Terraform checks

on:
  push:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository
        uses: actions/checkout@v7.0.1

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v4.0.1
        with:
          terraform_version: "1.16.2"

      - name: Check Terraform formatting
        run: terraform fmt -check -recursive

      - name: Initialize providers without a backend
        run: terraform init -backend=false

      - name: Validate Terraform configuration
        run: terraform validate -no-color

How to use this reference

Every line belongs at the indentation level shown. Matching this file ensures GitHub can connect each step to the validate job.

Document the troubleshooting lab

A README turns the repository into evidence of your engineering process. It records the working architecture, the permission failure you diagnosed, the repair, and the cleanup path.

  • Use the file tree in Visual Studio Code to create README.md inside the open project folder.
  • Paste the project overview, architecture, and demo sections into README.md:
# AWS Health API Troubleshooting Lab

A small AWS cloud engineering project that provisions a serverless health API with Terraform, reproduces a missing API Gateway invocation permission, fixes it, and adds CloudWatch monitoring.

## Architecture

Client request -> API Gateway HTTP API -> AWS Lambda -> CloudWatch Logs and Metrics -> CloudWatch Alarm

Terraform manages the AWS resources. GitHub Actions checks the Terraform configuration whenever code is pushed.

## Demo

Call the API:

```bash
curl "$(terraform output -raw base_url)/hello"
```

Expected response:

```json
{"status":"healthy","service":"my-function"}
```

Inspect the alarm:

```bash
aws cloudwatch describe-alarms --alarm-names "myalarm"
```

What does this section communicate?

The architecture line shows how a request travels through the service. The demo gives readers two concrete checks they can recognize from the project.

The healthy response proves the API works. The alarm command shows that the workload includes operational monitoring.

  • Save README.md.
  • Confirm the editor now shows the architecture flow, healthy response, and alarm check.

Does the README look incomplete?

  • Check that every opening code fence has a matching closing code fence.
  • Confirm the Architecture and Demo headings each begin on their own line.

Ask for help if the sections still run together: help me debug the Markdown structure in my project README

  • Append the failure explanation, YAML CI summary, and cleanup section to README.md:
## What the failure demonstrated

The Lambda function worked when invoked directly, but API Gateway initially returned an internal server error because its service principal did not have resource-based permission to invoke the function. Adding `aws_lambda_permission.api_gw` repaired the integration.

## YAML CI

The workflow at `.github/workflows/terraform-check.yml` checks formatting, installs the pinned providers without accessing a backend, and validates the Terraform configuration. It does not deploy infrastructure and does not require AWS credentials.

## Cleanup

```bash
terraform destroy
```

Why include the failure and cleanup?

The failure section shows that you diagnosed a cross-service permission boundary. Naming aws_lambda_permission.api_gw connects the repair to the infrastructure code.

The cleanup section gives readers a reproducible way to remove the managed resources. The CI section clarifies that validation runs without AWS credentials or deployment access.

  • Save README.md.
  • Confirm the editor now shows the failure explanation, YAML CI section, and cleanup command.

Missing the final sections?

  • Confirm the new content sits below the alarm code fence.
  • Check that the failure, YAML CI, and cleanup headings each start with two hash characters.

Ask for help if the file order differs from the reference: help me compare my README with the required project documentation

✔️ Awesome, I've got everything!

Your README now tells the full story from architecture through cleanup.

ⓧ I'd like to double check the full code

  • Compare README.md with this complete reference:
# AWS Health API Troubleshooting Lab

A small AWS cloud engineering project that provisions a serverless health API with Terraform, reproduces a missing API Gateway invocation permission, fixes it, and adds CloudWatch monitoring.

## Architecture

Client request -> API Gateway HTTP API -> AWS Lambda -> CloudWatch Logs and Metrics -> CloudWatch Alarm

Terraform manages the AWS resources. GitHub Actions checks the Terraform configuration whenever code is pushed.

## Demo

Call the API:

```bash
curl "$(terraform output -raw base_url)/hello"
```

Expected response:

```json
{"status":"healthy","service":"my-function"}
```

Inspect the alarm:

```bash
aws cloudwatch describe-alarms --alarm-names "myalarm"
```

## What the failure demonstrated

The Lambda function worked when invoked directly, but API Gateway initially returned an internal server error because its service principal did not have resource-based permission to invoke the function. Adding `aws_lambda_permission.api_gw` repaired the integration.

## YAML CI

The workflow at `.github/workflows/terraform-check.yml` checks formatting, installs the pinned providers without accessing a backend, and validates the Terraform configuration. It does not deploy infrastructure and does not require AWS credentials.

## Cleanup

```bash
terraform destroy
```

How to use this reference

The complete file keeps each heading and code fence in its final position. Matching it gives repository visitors the same troubleshooting narrative you followed.

Publish the repository and verify CI

Git records the project as a versioned snapshot. GitHub publishes that snapshot and runs the workflow from the committed YAML file.

  • Save every open file in Visual Studio Code.
  • Initialize the repository with main as its branch by running:
git init -b main

What does initialization create?

Git creates repository metadata inside the current project folder. The initial branch is named main from the start.

The terminal confirms that the repository was initialized. Your source files remain unchanged.

  • Stage the project and record its first snapshot by running these commands:
git add .
git commit -m "Build monitored AWS health API"

What do these commands record?

The first command stages the current project tree. The second command records the staged files with a descriptive commit message.

Your existing .gitignore keeps local Terraform state, generated ZIP content, and invocation output out of the snapshot.

The terminal prints a commit summary with the files added to the repository.

Could Git not create the commit?

  • Follow Git's printed identity guidance if it needs your author name or email.
  • Confirm .gitignore exists before staging the project again.

Ask for help with the output you received: help me diagnose why Git could not create my first project commit

Your repository will be public

The next command publishes the committed files where anyone can read them. Your .gitignore excludes local Terraform state and generated artifacts from the commit.

Before you publish, do you expect the push trigger to start the Terraform workflow automatically? Take a moment to predict your answer.

  • Create and push the public aws-health-api repository by running:
gh repo create aws-health-api --public --source=. --remote=origin --push

What does publishing do?

GitHub CLI uses the current folder as the repository source. It creates a public repository named aws-health-api.

The command names the remote origin and pushes the committed main branch. That push triggers the workflow.

The terminal confirms that the repository was created and pushed. It also prints the repository URL.

Could GitHub not create the repository?

  • Return to the authenticated GitHub CLI session from earlier if the command reports an authentication problem.
  • Confirm that aws-health-api is available under your GitHub account.
  • Check your internet connection if the repository cannot be reached.

Ask for help with the exact command result: help me diagnose why GitHub CLI could not create or push my repository

Before you inspect the run, do you expect all three Terraform checks to complete without AWS credentials? Take a moment to predict your answer.

  • Open the aws-health-api repository URL printed in the terminal.
  • Open the Actions page.
  • Open the newest Terraform checks run.
  • Confirm that checkout, Terraform setup, formatting, provider initialization, and validation all completed successfully.

You will see the Terraform checks workflow shown in green. This proves every push now receives an automated Terraform quality check.

Strong finish. Your working AWS health API now has public documentation, version history, and automated Terraform validation.

Secret mission

Trigger the Alarm on Purpose

A healthy endpoint proves the service works. A controlled failure proves your monitoring catches real application errors. In this mission, you will add a safe failure switch and trace its signal from the client response to Lambda logs and the CloudWatch alarm.

Clean Up Your Resources

Clean Up Your Resources

Your deployed AWS resources can continue generating usage-based charges. Decide whether to keep the demo online, pause its cloud usage, or delete every project resource.

Cost warning

AWS Lambda and Amazon API Gateway HTTP APIs charge based on usage. Amazon CloudWatch Logs and alarms can also incur charges beyond applicable allowances.

The few requests in this lab are expected to fit applicable free allowances. Eligibility depends on your AWS account. Choose Pause or Delete if you do not need the endpoint online.

Resources you used:

  • The my-function Lambda function.
  • The cloud-engineer-health-api-lambda-role AWS Identity and Access Management execution role.
  • The cloud-engineer-health-api-logs inline logging policy.
  • The cloud-engineer-health-api API Gateway HTTP API.
  • The GET /hello route with its integration and $default stage.
  • The API Gateway invocation permission represented by aws_lambda_permission.api_gw.
  • The seven-day Lambda log group represented by aws_cloudwatch_log_group.lambda.
  • The seven-day API access log group represented by aws_cloudwatch_log_group.api.
  • The myalarm Lambda error alarm.
  • The local project folder currently open in Visual Studio Code.
  • The public aws-health-api repository on GitHub with its green GitHub Actions workflow.

Keep everything running

No action needed. Choose this if you want the public health endpoint available for future demonstrations.

  • Leave the Terraform-managed AWS stack deployed.
  • Monitor your AWS billing while the endpoint remains available.
  • Keep the aws-health-api repository as the published record of your troubleshooting lab.
  • Use the healthy endpoint occasionally to confirm the demo still responds.

Pause - I'll come back to this later

Shut down the AWS workload to stop new usage from this lab. Your local project folder and GitHub repository stay available for rebuilding the demo.

What Pause Removes

Terraform displays a destruction plan before requesting approval. The plan should contain only resources from this health API lab.

  • Return to the project folder in Visual Studio Code.
  • Open an integrated terminal for that folder.
  • Start removing the Terraform-managed AWS resources by running:
terraform destroy

What Does This Command Do?

Terraform reads the local state to deprovision every managed object. It preserves your local configuration. It also leaves the GitHub repository untouched.

  • Review every resource in the destruction plan.
  • Approve the destruction after confirming that every listed resource belongs to this lab.
  • Confirm that Terraform tracks no remaining resources by running:
terraform state list

What Does This Check Show?

This command lists the resources recorded in local Terraform state. Empty output confirms that no managed AWS resources remain in that state.

Good call. Your live AWS workload is removed while the files needed to recreate it remain available.

Destroy Didn't Complete?

  • Read the first Terraform error to identify the resource that blocked deletion.
  • Confirm that your AWS authentication still uses the account that created the stack.
  • Ask for help diagnosing the failed Terraform destruction.

Delete - I don't want to use this again

Remove the AWS stack first. Then delete the published repository and the local project folder.

Delete in This Order

Permanent deletion is easier while the local Terraform state still exists. Destroying the cloud resources first gives Terraform the information it needs to remove the complete stack.

  • Return to the project folder in Visual Studio Code.
  • Open an integrated terminal for that folder.
  • Start removing the Terraform-managed AWS resources by running:
terraform destroy

What Does This Command Do?

Terraform uses the local state to deprovision the Lambda function, IAM resources, HTTP API, log groups, and alarm. The local files remain available until you remove their folder.

  • Review every resource in the destruction plan.
  • Approve the destruction after confirming that every listed resource belongs to this lab.
  • Verify that the Terraform state contains no managed resources by running:
terraform state list

What Does This Check Show?

Terraform lists every object still recorded in its local state. You should see no output after a successful destruction.

The cloud side is clear. The endpoint, logs, permissions, and alarm from this lab are no longer deployed.

Still See Resources in State?

  • Review the remaining resource addresses to identify which deletion did not complete.
  • Confirm that your AWS authentication still points to the account used for this lab.
  • Ask for help with the remaining Terraform resources.

Repository deletion is permanent. Make sure you no longer need the published workflow history or README before continuing.

  • Open the aws-health-api repository on GitHub.
  • Open its repository settings page.
  • Scroll to the repository deletion area.
  • Select the permanent repository deletion option.
  • Enter aws-health-api when GitHub requests the repository name.
  • Confirm the permanent deletion.

You should no longer find the aws-health-api repository on your GitHub profile.

Deleting the local folder removes the Terraform state and every remaining project file. Keep any notes you want before completing this final cleanup.

  • In the Visual Studio Code Explorer sidebar, right-click the top-level folder that contains main.tf.
  • Choose the option that reveals the folder in Finder.
  • Close the Visual Studio Code window for this project.
  • Move the revealed project folder to Trash.
  • Open Trash in Finder.
  • Select only the project folder.
  • Delete the selected folder permanently.
  • Confirm that the project folder no longer appears in its previous Finder location.

Everything created for the lab is now removed from AWS, GitHub, and your Mac.

Nice Work!

Nice Work!

You did it! Your public health API now runs on AWS Lambda through an API Gateway HTTP API.

You've learned how to:

  • Provisioned the complete API as infrastructure as code with Terraform. A request to GET /hello returns healthy JSON from my-function.
  • Diagnosed a deliberate IAM permission failure through API Gateway access logs. A scoped Lambda permission restored the endpoint.
  • Published an interview-ready repository with a hand-written YAML workflow for GitHub Actions. The green Terraform checks workflow checks formatting on every push. It also validates the Terraform configuration.
  • Completed the Secret Mission by adding a controlled fail=true path. CloudWatch logs captured the synthetic failure. The myalarm alarm reached ALARM after the Lambda error metric arrived.

Ready to quiz yourself?