Chunk 3 is complete — and this one pushed me the furthest outside my comfort zone. Infrastructure as Code, remote state management, and a fully automated CI/CD pipeline are all now in place.
In this post I'm documenting exactly what I built, the decisions I made, and the bugs I hit along the way.
What is Chunk 3 about?
The goal is to stop clicking through the AWS Console and start defining infrastructure as code — and then automate deployments so every push to GitHub triggers a full deploy automatically.
The tools involved:
- Terraform — define all AWS resources as code
- GitHub Actions — automate the deployment pipeline
- S3 remote backend — store Terraform state so CI/CD and local machines share the same view of infrastructure
- Dev Container — reproducible development environment in GitHub Codespaces
Why Terraform over AWS SAM?
The challenge recommends AWS SAM for IaC, but I chose Terraform instead. Here is why:
- Terraform is cloud agnostic — the same skills work on AWS, Azure, and GCP
- It is by far the most in-demand IaC tool in the industry right now
- HCL (HashiCorp Configuration Language) is clean and readable
- The Terraform ecosystem and community are excellent
Setting Up the Dev Container
Since I am working in GitHub Codespaces, the first thing I did was create a dev container so every time the environment starts, all tools are pre-installed automatically.
I created .devcontainer/devcontainer.json:
{
"name": "Cloud Resume Challenge",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/devcontainers/features/aws-cli:1": {},
"ghcr.io/devcontainers/features/terraform:1": {},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.12"
},
"ghcr.io/devcontainers/features/github-cli:1": {}
}
}
This installs AWS CLI, Terraform, Python, and GitHub CLI automatically on every Codespace rebuild. No more manual setup.
Terraform Project Structure
terraform/
├── main.tf ← all AWS resources
├── variables.tf ← input variables
├── outputs.tf ← output values
└── providers.tf ← AWS provider + backend config
The resources defined in Terraform:
- S3 bucket with static website hosting and public access policy
- DynamoDB table for the visitor counter
- IAM role and policies for Lambda
- Lambda function
- API Gateway HTTP API with GET /count route
- Lambda permission for API Gateway
Remote Backend — The Most Important Step
This is the step most beginners skip and then wonder why CI/CD fails.
By default Terraform stores its state file locally on your machine. When GitHub Actions runs Terraform it has no idea what already exists in AWS — so it tries to create everything from scratch and hits conflicts.
The fix is a remote backend — store the state file in S3 so both your local machine and GitHub Actions share the same state.
I added this to providers.tf:
backend "s3" {
bucket = "priyanka-terraform-state"
key = "cloud-resume/terraform.tfstate"
region = "ap-south-2"
}
Then ran:
terraform init -migrate-state
This moved the local state file to S3. From that point on both local and CI/CD shared the same state.
Key lesson: Always set up remote state before setting up CI/CD. If you do it the other way around you will hit resource conflict errors on every pipeline run.
Bugs I Hit Along the Way
Bug 1 — Resource conflicts on first apply
Since I had already created S3, DynamoDB, Lambda, and API Gateway manually earlier in the challenge, Terraform tried to create them again and hit 409 conflict errors.
Fix: import existing resources into Terraform state:
terraform import aws_s3_bucket.resume priyanka-cloud-resume-challenge
terraform import aws_lambda_function.counter cloud-resume-counter
terraform import aws_dynamodb_table.counter cloud-resume-counter
Bug 2 — DynamoDB table item import not supported
Terraform does not support importing aws_dynamodb_table_item resources. The item already existed from manual setup so Terraform kept trying to create it and failing.
Fix: removed the aws_dynamodb_table_item resource from main.tf entirely. The item already exists and Lambda manages the views count dynamically — Terraform does not need to manage it.
Bug 3 — GitHub Actions had no Terraform state
The pipeline kept hitting resource conflict errors because the state file was local and not shared with GitHub Actions.
Fix: set up the S3 remote backend as described above. Once state was in S3, both local and CI/CD shared the same view of infrastructure and conflicts stopped.
Bug 4 — String concatenation error in outputs.tf
Used + operator to concatenate strings in Terraform — which is a number operator, not a string operator.
Fix: use string interpolation instead:
value = "${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "/")}/count"
The GitHub Actions Pipeline
The pipeline runs automatically on every push to main and also supports manual triggers via workflow_dispatch.
name: Deploy Cloud Resume
on:
push:
branches:
- main
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-south-2
- name: Sync files to S3
run: |
aws s3 sync . s3://priyanka-cloud-resume-challenge \
--exclude "*" \
--include "index.html" \
--include "styles.css" \
--delete
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
--paths "/*"
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Init
working-directory: ./terraform
run: terraform init
- name: Terraform Apply
working-directory: ./terraform
run: terraform apply -auto-approve
- name: Smoke test
run: |
response=$(curl -s https://40g9acq651.execute-api.ap-south-2.amazonaws.com/count)
if echo "$response" | grep -q "views"; then
echo "Smoke test passed!"
else
echo "Smoke test failed!"
exit 1
fi
What the Pipeline Does
Push to GitHub
↓
Checkout code
↓
Configure AWS credentials
↓
Sync index.html + styles.css → S3
↓
Invalidate CloudFront cache
↓
Terraform init + apply
↓
Smoke test → verify API returns views count
Key Takeaways
- Remote state is not optional for CI/CD — without it every pipeline run hits resource conflicts
- Import existing resources before applying — if you created resources manually first, import them before letting Terraform manage them
-
Terraform string interpolation — use
${}not+for string concatenation - Dev containers save time — define your environment once, never set it up again
- workflow_dispatch is underrated — being able to trigger the pipeline manually without a dummy commit is very useful
What's Next
Next up is writing tests for the Lambda function and then tackling the challenge mods — AI Engineer Mod and DevOps multi-stage pipeline.
Part of my Cloud Resume Challenge series. Read here
Top comments (1)
Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support