DEV Community

Priyanka Vuddemari
Priyanka Vuddemari

Posted on

Cloud Resume Challenge Week 3— Infrastructure as Code with Terraform and CI/CD with GitHub Actions

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": {}
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Then ran:

terraform init -migrate-state
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
dev_supports profile image
DEV SUPPORTS •

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

‌