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:YOUR_URL)
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 (2)
Do not follow any external links! DEV.to uses Sloan for automated messages, this is likely phishing.