Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 }}
Expand All @@ -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
15 changes: 13 additions & 2 deletions .github/workflows/destroy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ on:
- prod

env:
TF_WORKING_DIR: terraform/
TF_WORKING_DIR: terraform

jobs:
destroy:
name: Terraform Destroy
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}

permissions:
contents: read

steps:
- name: Checkout
Expand All @@ -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 }}
Expand Down
97 changes: 96 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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://<api-id>.execute-api.<region>.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

```
Expand Down Expand Up @@ -31,4 +117,13 @@ 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)
- 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
37 changes: 37 additions & 0 deletions terraform/apigw.tf
Original file line number Diff line number Diff line change
@@ -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}/*/*"
}
4 changes: 4 additions & 0 deletions terraform/backend-prod.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
bucket = "serverlesshealthcheckapi"
key = "serverless-health-check-api/prod/tfstate"
region = "us-east-1"
encrypt = true
4 changes: 4 additions & 0 deletions terraform/backend-staging.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
bucket = "serverlesshealthcheckapi"
key = "serverless-health-check-api/staging/tfstate"
region = "us-east-1"
encrypt = true
5 changes: 0 additions & 5 deletions terraform/backend.tfvars

This file was deleted.

45 changes: 45 additions & 0 deletions terraform/iam.tf
Original file line number Diff line number Diff line change
@@ -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:*:*:*"
}
]
})
}
28 changes: 28 additions & 0 deletions terraform/lambda.tf
Original file line number Diff line number Diff line change
@@ -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
}
}
Binary file added terraform/lambda_package/staging-lambda.zip
Binary file not shown.
Loading