From cedca9d1ac34de3d1ad69cc496f5eeddcbb53abb Mon Sep 17 00:00:00 2001 From: emmy-github-webdev Date: Sat, 13 Dec 2025 08:47:11 +0100 Subject: [PATCH 1/4] Tested and fixed tfstate store --- terraform/apigw.tf | 37 ++++++++++ terraform/backend-prod.tfvars | 4 ++ terraform/backend-staging.tfvars | 4 ++ terraform/backend.tfvars | 5 -- terraform/iam.tf | 45 +++++++++++++ terraform/lambda.tf | 28 ++++++++ terraform/lambda_package/staging-lambda.zip | Bin 0 -> 745 bytes terraform/main.tf | 71 +++----------------- terraform/modules/api-gateway/main.tf | 62 ----------------- terraform/modules/api-gateway/outputs.tf | 14 ---- terraform/modules/api-gateway/variables.tf | 14 ---- terraform/modules/dynamodb/main.tf | 17 ----- terraform/modules/dynamodb/outputs.tf | 9 --- terraform/modules/dynamodb/variables.tf | 9 --- terraform/modules/iam/main.tf | 58 ---------------- terraform/modules/iam/outputs.tf | 9 --- terraform/modules/iam/variables.tf | 14 ---- terraform/modules/lambda/main.tf | 33 --------- terraform/modules/lambda/outputs.tf | 14 ---- terraform/modules/lambda/variables.tf | 29 -------- terraform/outputs.tf | 19 ++---- terraform/prod.tfvars | 5 +- terraform/provider.tf | 14 ++++ terraform/staging.tfvars | 5 +- terraform/variables.tf | 34 ++++------ 25 files changed, 165 insertions(+), 388 deletions(-) create mode 100644 terraform/apigw.tf create mode 100644 terraform/backend-prod.tfvars create mode 100644 terraform/backend-staging.tfvars delete mode 100644 terraform/backend.tfvars create mode 100644 terraform/iam.tf create mode 100644 terraform/lambda.tf create mode 100644 terraform/lambda_package/staging-lambda.zip delete mode 100644 terraform/modules/api-gateway/main.tf delete mode 100644 terraform/modules/api-gateway/outputs.tf delete mode 100644 terraform/modules/api-gateway/variables.tf delete mode 100644 terraform/modules/dynamodb/main.tf delete mode 100644 terraform/modules/dynamodb/outputs.tf delete mode 100644 terraform/modules/dynamodb/variables.tf delete mode 100644 terraform/modules/iam/main.tf delete mode 100644 terraform/modules/iam/outputs.tf delete mode 100644 terraform/modules/iam/variables.tf delete mode 100644 terraform/modules/lambda/main.tf delete mode 100644 terraform/modules/lambda/outputs.tf delete mode 100644 terraform/modules/lambda/variables.tf create mode 100644 terraform/provider.tf diff --git a/terraform/apigw.tf b/terraform/apigw.tf new file mode 100644 index 0000000..cdab2ce --- /dev/null +++ b/terraform/apigw.tf @@ -0,0 +1,37 @@ +resource "aws_apigatewayv2_api" "http_api" { + name = "${var.env}-health-check-api" + protocol_type = "HTTP" +} + +resource "aws_apigatewayv2_integration" "lambda_integration" { + api_id = aws_apigatewayv2_api.http_api.id + integration_type = "AWS_PROXY" + integration_uri = aws_lambda_function.health_check.arn + payload_format_version = "2.0" +} + +resource "aws_apigatewayv2_route" "health_route" { + api_id = aws_apigatewayv2_api.http_api.id + route_key = "GET /health" + target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" +} + +resource "aws_apigatewayv2_route" "health_route_post" { + api_id = aws_apigatewayv2_api.http_api.id + route_key = "POST /health" + target = "integrations/${aws_apigatewayv2_integration.lambda_integration.id}" +} + +resource "aws_apigatewayv2_stage" "default_stage" { + api_id = aws_apigatewayv2_api.http_api.id + name = "$default" + auto_deploy = true +} + +resource "aws_lambda_permission" "allow_apigw" { + statement_id = "${var.env}-allow-apigw" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.health_check.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_apigatewayv2_api.http_api.execution_arn}/*/*" +} diff --git a/terraform/backend-prod.tfvars b/terraform/backend-prod.tfvars new file mode 100644 index 0000000..15f092c --- /dev/null +++ b/terraform/backend-prod.tfvars @@ -0,0 +1,4 @@ +bucket = "serverlesshealthcheckapi" +key = "serverless-health-check-api/prod/tfstate" +region = "us-east-1" +encrypt = true diff --git a/terraform/backend-staging.tfvars b/terraform/backend-staging.tfvars new file mode 100644 index 0000000..edb423e --- /dev/null +++ b/terraform/backend-staging.tfvars @@ -0,0 +1,4 @@ +bucket = "serverlesshealthcheckapi" +key = "serverless-health-check-api/staging/tfstate" +region = "us-east-1" +encrypt = true diff --git a/terraform/backend.tfvars b/terraform/backend.tfvars deleted file mode 100644 index 71aaf90..0000000 --- a/terraform/backend.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -# bucket = "serverlesshealthcheckapi" -# key = "health-check-app/terraform.tfstate" -# region = "us-east-1" -# encrypt = true -# dynamodb_table = "terraform-locks" \ No newline at end of file diff --git a/terraform/iam.tf b/terraform/iam.tf new file mode 100644 index 0000000..ec28e5b --- /dev/null +++ b/terraform/iam.tf @@ -0,0 +1,45 @@ +data "aws_iam_policy_document" "lambda_assume_role" { + statement { + effect = "Allow" + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + actions = ["sts:AssumeRole"] + } +} + +resource "aws_iam_role" "lambda_role" { + name = "${var.env}-health-check-lambda-role" + assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json + tags = { + Environment = var.env + } +} + +resource "aws_iam_role_policy" "lambda_policy" { + name = "${var.env}-health-check-lambda-policy" + role = aws_iam_role.lambda_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "dynamodb:PutItem" + ] + Resource = aws_dynamodb_table.requests.arn + }, + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "arn:aws:logs:*:*:*" + } + ] + }) +} diff --git a/terraform/lambda.tf b/terraform/lambda.tf new file mode 100644 index 0000000..9c7261f --- /dev/null +++ b/terraform/lambda.tf @@ -0,0 +1,28 @@ +data "archive_file" "lambda_zip" { + type = "zip" + output_path = "${path.module}/lambda_package/${var.env}-lambda.zip" + + source { + content = file("${path.module}/../lambda/lambda_function.py") + filename = "lambda_function.py" + } +} + +resource "aws_lambda_function" "health_check" { + function_name = "${var.env}-health-check-function" + filename = data.archive_file.lambda_zip.output_path + handler = var.lambda_handler + runtime = var.lambda_runtime + role = aws_iam_role.lambda_role.arn + source_code_hash = data.archive_file.lambda_zip.output_base64sha256 + + environment { + variables = { + REQUESTS_TABLE = aws_dynamodb_table.requests.name + } + } + + tags = { + Environment = var.env + } +} diff --git a/terraform/lambda_package/staging-lambda.zip b/terraform/lambda_package/staging-lambda.zip new file mode 100644 index 0000000000000000000000000000000000000000..fade2416ba888ddc40276bc2e72a4927272bebbe GIT binary patch literal 745 zcmWIWW@Zs#-~d8&AOsZ1Nz6@3NsLb`%}XxH%+J#+sGJk*pMBd;V6XHa*|xue5g&`M zT(B2!f-K*`W?PR3tH2dwNj~({iuV){>`RV1a z&pgW>ICYh!@7es4&9m$0o7dZKetuLbx%vo;OKyGC_auGKwOk^c$}jeYF1@Th!>lK$ z|KL?ScgY-AIF(P+`{D5Iv$906JO5=r=Fb&9=|(R%Wja(HkrmsdT~)E`|D}S1 zrXs6iGPmB*TFw1Ms5~Q+Y3Jsa^;1$F3o;z2|Bz4|F15Qc;=+B&^xKxl*3A_uK6CL+ zfO)HA42xl>+mc;+t1s|)Jo>@*^V(u1mj3-&JS;i#>bEK~O?(t5On;MeDqdc7dvUL# zP9j&n?~#6ksFdb=Cl!wBu?3~7Zd$y0WvRZAr*A>_b8lCp#{Z5r;)nO0h)C&O@a)fp z%k^Qq=h;~9=x)1v*QD~(?=|gR%a(=udZ+2hbDY-PuUdQZWUSG)ypT8+w*Kjs0qp9( z5B&WxE7{ec+x}Q&0HeWgX8GNFM82OASt0rNsPv(QZP#pNK797ry8W>Bb9a`YkoM^1 zfhiwezRD_^7H_#%{AL0Fy<2h<8y()yeg5COQ*i}va>mpuQ&(r0r{e0kXrLxwQ%09)C-{pE=LZ18(uq!!`6m zS-o1}y*%HivgciSPq`gk5v*r>`>F8D|0=%?NfBFljiB`Uy7q5 Date: Sat, 13 Dec 2025 08:53:52 +0100 Subject: [PATCH 2/4] Fixed terraform director --- .github/workflows/deploy.yaml | 17 ++++++++++++----- .github/workflows/destroy.yaml | 13 ++++++++++++- README.md | 10 +++++++++- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 7e0ace5..4a9fbb6 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -8,13 +8,14 @@ on: workflow_dispatch: env: - TF_WORKING_DIR: terraform/ + TF_WORKING_DIR: terraform jobs: terraform: name: Terraform (plan & apply) runs-on: ubuntu-latest environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'staging' }} + permissions: contents: read @@ -34,9 +35,15 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ secrets.AWS_REGION }} + # Backend selection based on branch - name: Terraform Init working-directory: ${{ env.TF_WORKING_DIR }} - run: terraform init -input=false + run: | + if [ "${{ github.ref }}" = "refs/heads/staging" ]; then + terraform init -reconfigure -backend-config=backend-staging.tfvars -input=false + else + terraform init -reconfigure -backend-config=backend-prod.tfvars -input=false + fi - name: Terraform Validate & Format working-directory: ${{ env.TF_WORKING_DIR }} @@ -54,17 +61,17 @@ jobs: terraform plan -var-file="prod.tfvars" -out=tfplan fi - - name: Terraform Apply + - name: Terraform Apply (staging) if: github.ref == 'refs/heads/staging' working-directory: ${{ env.TF_WORKING_DIR }} run: terraform apply -input=false -auto-approve tfplan - - name: Terraform Apply (prod) - requires env approval + - name: Terraform Apply (prod) if: github.ref == 'refs/heads/main' working-directory: ${{ env.TF_WORKING_DIR }} run: terraform apply -input=false -auto-approve tfplan - - name: Show outputs + - name: Show Outputs if: success() working-directory: ${{ env.TF_WORKING_DIR }} run: terraform output -json diff --git a/.github/workflows/destroy.yaml b/.github/workflows/destroy.yaml index 6836253..464e2a8 100644 --- a/.github/workflows/destroy.yaml +++ b/.github/workflows/destroy.yaml @@ -16,7 +16,12 @@ env: jobs: destroy: + name: Terraform Destroy runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment }} + + permissions: + contents: read steps: - name: Checkout @@ -34,9 +39,15 @@ jobs: aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ secrets.AWS_REGION }} + # Backend selection based on input - name: Terraform Init working-directory: ${{ env.TF_WORKING_DIR }} - run: terraform init -input=false + run: | + if [ "${{ github.event.inputs.environment }}" = "staging" ]; then + terraform init -reconfigure -backend-config=backend-staging.tfvars -input=false + else + terraform init -reconfigure -backend-config=backend-prod.tfvars -input=false + fi - name: Terraform Destroy working-directory: ${{ env.TF_WORKING_DIR }} diff --git a/README.md b/README.md index 34aae76..27915ba 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,12 @@ Run the python funtion locally using VS Code Run Button - Create the terraform folder structure -- Deploy with: terraform init then terraform apply -var-file="staging.tfvars" (or prod.tfvars) \ No newline at end of file +- Deploy with: terraform init then terraform apply -var-file="staging.tfvars" (or prod.tfvars) + + +endpoint - https://nrbefv9bcj.execute-api.us-east-1.amazonaws.com/health + + +terraform init -backend-config=backend-staging.tfvars for staging environment + +terraform init -backend-config=backend-prod.tfvars for prod environment From 83c7128926680722ddc493637312f6f85b21ece3 Mon Sep 17 00:00:00 2001 From: emmy-github-webdev Date: Sat, 13 Dec 2025 08:55:54 +0100 Subject: [PATCH 3/4] Fixed terraform fmt error --- terraform/apigw.tf | 6 +++--- terraform/backend-prod.tfvars | 8 ++++---- terraform/backend-staging.tfvars | 8 ++++---- terraform/iam.tf | 2 +- terraform/lambda.tf | 10 +++++----- terraform/prod.tfvars | 2 +- terraform/staging.tfvars | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/terraform/apigw.tf b/terraform/apigw.tf index cdab2ce..1e15a77 100644 --- a/terraform/apigw.tf +++ b/terraform/apigw.tf @@ -4,9 +4,9 @@ resource "aws_apigatewayv2_api" "http_api" { } resource "aws_apigatewayv2_integration" "lambda_integration" { - api_id = aws_apigatewayv2_api.http_api.id - integration_type = "AWS_PROXY" - integration_uri = aws_lambda_function.health_check.arn + api_id = aws_apigatewayv2_api.http_api.id + integration_type = "AWS_PROXY" + integration_uri = aws_lambda_function.health_check.arn payload_format_version = "2.0" } diff --git a/terraform/backend-prod.tfvars b/terraform/backend-prod.tfvars index 15f092c..35a5adc 100644 --- a/terraform/backend-prod.tfvars +++ b/terraform/backend-prod.tfvars @@ -1,4 +1,4 @@ -bucket = "serverlesshealthcheckapi" -key = "serverless-health-check-api/prod/tfstate" -region = "us-east-1" -encrypt = true +bucket = "serverlesshealthcheckapi" +key = "serverless-health-check-api/prod/tfstate" +region = "us-east-1" +encrypt = true diff --git a/terraform/backend-staging.tfvars b/terraform/backend-staging.tfvars index edb423e..f306603 100644 --- a/terraform/backend-staging.tfvars +++ b/terraform/backend-staging.tfvars @@ -1,4 +1,4 @@ -bucket = "serverlesshealthcheckapi" -key = "serverless-health-check-api/staging/tfstate" -region = "us-east-1" -encrypt = true +bucket = "serverlesshealthcheckapi" +key = "serverless-health-check-api/staging/tfstate" +region = "us-east-1" +encrypt = true diff --git a/terraform/iam.tf b/terraform/iam.tf index ec28e5b..638aff5 100644 --- a/terraform/iam.tf +++ b/terraform/iam.tf @@ -2,7 +2,7 @@ data "aws_iam_policy_document" "lambda_assume_role" { statement { effect = "Allow" principals { - type = "Service" + type = "Service" identifiers = ["lambda.amazonaws.com"] } actions = ["sts:AssumeRole"] diff --git a/terraform/lambda.tf b/terraform/lambda.tf index 9c7261f..bc4b63c 100644 --- a/terraform/lambda.tf +++ b/terraform/lambda.tf @@ -9,11 +9,11 @@ data "archive_file" "lambda_zip" { } resource "aws_lambda_function" "health_check" { - function_name = "${var.env}-health-check-function" - filename = data.archive_file.lambda_zip.output_path - handler = var.lambda_handler - runtime = var.lambda_runtime - role = aws_iam_role.lambda_role.arn + function_name = "${var.env}-health-check-function" + filename = data.archive_file.lambda_zip.output_path + handler = var.lambda_handler + runtime = var.lambda_runtime + role = aws_iam_role.lambda_role.arn source_code_hash = data.archive_file.lambda_zip.output_base64sha256 environment { diff --git a/terraform/prod.tfvars b/terraform/prod.tfvars index 242682d..012a7f5 100644 --- a/terraform/prod.tfvars +++ b/terraform/prod.tfvars @@ -1,2 +1,2 @@ -env = "prod" +env = "prod" aws_region = "us-east-1" diff --git a/terraform/staging.tfvars b/terraform/staging.tfvars index 47cdc26..da3c370 100644 --- a/terraform/staging.tfvars +++ b/terraform/staging.tfvars @@ -1,2 +1,2 @@ -env = "staging" +env = "staging" aws_region = "us-east-1" From c8d5c3535ac78f7ef7464f9d19a60118c54757ce Mon Sep 17 00:00:00 2001 From: emmy-github-webdev Date: Sat, 13 Dec 2025 09:38:36 +0100 Subject: [PATCH 4/4] Added README content --- .github/workflows/destroy.yaml | 2 +- README.md | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/.github/workflows/destroy.yaml b/.github/workflows/destroy.yaml index 464e2a8..52361b1 100644 --- a/.github/workflows/destroy.yaml +++ b/.github/workflows/destroy.yaml @@ -12,7 +12,7 @@ on: - prod env: - TF_WORKING_DIR: terraform/ + TF_WORKING_DIR: terraform jobs: destroy: diff --git a/README.md b/README.md index 27915ba..658bc54 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,91 @@ # Serverless Health Check API with CI/CD +The goal of this project is to build, configure, and automate the deployment of a simple serverless application on AWS. Created a health check endpoint that logs requests and stores them in a database, with a CI/CD pipeline to manage deployments for both staging and production environments, fully provisioned via Terraform and deployed automatically using GitHub Actions. + +## Architectural desig + +### Core Components + +- Amazon API Gateway (HTTP API) +- AWS Lambda (Python) +- Amazon DynamoDB +- AWS IAM +- Amazon CloudWatch Logs + +Each environment (staging, prod) is isolated by naming convention and Terraform variables. + +### Runtime Request Flow + +1. _Client_: sends a GET or POST request to: + +``` +https://.execute-api..amazonaws.com/health +``` +2. _API Gateway_: +- Matches the /health route +- Forwards the request using AWS_PROXY integration +3. _Lambda Function (env-health-check-function)_: +- Logs the full request event to CloudWatch Logs +- Generates a UUID +- Stores request metadata in DynamoDB (env-requests-db) +- Returns a JSON response +4. _DynamoDB_: +- Stores the request record (ID, timestamp, request payload) + +### Pipeline Flow +1. Developer pushes code +- staging branch → auto deploy +- main branch → production deploy +2. GitHub Actions workflow: The GitHub action workflow contain both terraform deploy and terraform destroy. +- Configures AWS credentials (GitHub Secrets) +- Terraform deploy - deploy.yaml + - Checks out code + - Runs: + - terraform fmt + - terraform validate + - terraform plan + - terraform apply + +- Terraform destroy - destroy.yaml + - On GitHub console, manually trigger the destroy pipeline from the actions + - Runs: + - terraform int + - terraform destroy + +### Environment separation + +| Aspect | Staging | Production | +| -------------- | ------------------------------- | ---------------------------- | +| Branch | `staging` | `main` | +| Terraform vars | `staging.tfvars` | `prod.tfvars` | +| Lambda | `staging-health-check-function` | `prod-health-check-function` | +| DynamoDB | `staging-requests-db` | `prod-requests-db` | +| API Gateway | `staging-health-check-api` | `prod-health-check-api` | +| Approval | None | Required | + + +### Security and IAM Role +Each Lambda function has one dedicated IAM role with: +- _Allowed permissions_ + - dynamodb:PutItem → specific DynamoDB table ARN + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents +- Denied by default + - No read access to DynamoDB + - No access to other AWS services + - No wildcard write permissions +- secrets Handling + - AWS credentials stored in GitHub Secrets + - No credentials committed to repository + + + + + + + + create hello lambda funtion using Python ``` @@ -37,6 +123,7 @@ Run the python funtion locally using VS Code Run Button endpoint - https://nrbefv9bcj.execute-api.us-east-1.amazonaws.com/health + terraform init -backend-config=backend-staging.tfvars for staging environment terraform init -backend-config=backend-prod.tfvars for prod environment