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
- What is the "meta-layer" problem in a Terraform + CodePipeline setup?
- What does
terraform fmt -checkdo vsterraform fmt? - Why use
terraform apply tfplaninstead ofterraform applyin CI? - What is OIDC and why is it better than storing AWS access keys in GitHub Secrets?
- What condition in the workflow ensures apply only runs on merges to main?