Skip to main content

Module 16, Terraform in CI/CD

The Meta-Layer Problem

CodePipeline deploys your application. Terraform creates CodePipeline. Who runs Terraform?

GitHub Actions (runs Terraform)
↓ creates
CodePipeline (deploys your application)
↓ triggers on push
Elastic Beanstalk (runs your application)

Answer: a separate GitHub Actions workflow runs Terraform, separate from the application pipeline.

Complete GitHub Actions Workflow

# .github/workflows/terraform.yml
name: Terraform

on:
push:
branches: [main]
paths: ['infrastructure/**']
pull_request:
paths: ['infrastructure/**']

permissions:
id-token: write # required for OIDC
contents: read

jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT:role/github-actions-terraform
aws-region: us-east-1

- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "~1.5"

- name: Terraform Format Check
run: terraform fmt -check -recursive
working-directory: infrastructure

- name: Terraform Init
run: terraform init
working-directory: infrastructure

- name: Terraform Validate
run: terraform validate
working-directory: infrastructure

- name: Terraform Plan
run: terraform plan -out=tfplan
working-directory: infrastructure

- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply tfplan
working-directory: infrastructure

OIDC, Keyless AWS Authentication

Instead of storing long-lived AWS access keys as GitHub secrets, use OIDC to let GitHub Actions assume an IAM role directly:

resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

resource "aws_iam_role" "github_actions" {
name = "github-actions-terraform"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Federated = aws_iam_openid_connect_provider.github.arn }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:YOUR_ORG/YOUR_REPO:*"
}
}
}]
})
}
tip

OIDC means zero long-lived credentials stored anywhere. The GitHub Actions JWT is exchanged for short-lived AWS credentials at runtime.

Knowledge Check

  1. What is the "meta-layer" problem in a Terraform + CodePipeline setup?
  2. What does terraform fmt -check do vs terraform fmt?
  3. Why use terraform apply tfplan instead of terraform apply in CI?
  4. What is OIDC and why is it better than storing AWS access keys in GitHub Secrets?
  5. What condition in the workflow ensures apply only runs on merges to main?