Terraform in CI CD Pipelines with GitHub Actions
Running Terraform manually from a developer's laptop is acceptable when learning. In production, every infrastructure change should go through an automated, auditable, repeatable pipeline. This topic shows you how to build a complete Terraform CI/CD workflow using GitHub Actions — the most widely used platform for this purpose.
Why Automate Terraform
Manual Terraform runs have several risks:
- A developer runs
applywithout a team review of the plan - Different machines use different Terraform versions, producing inconsistent results
- Credentials exist on developer laptops — a security risk
- There is no audit trail of who changed what and when
A CI/CD pipeline eliminates all of these problems: every change is reviewed, every run uses the same version, credentials live only in the pipeline, and every action is logged.
The Recommended Terraform Pipeline Flow
Diagram: Pull Request and Merge Workflow
Developer pushes branch
|
v
Pull Request opened
|
v
CI triggers: terraform fmt -check
terraform validate
terraform plan
|
v
Plan output posted as PR comment
|
v
Teammate reviews the plan output in the PR
|
v
PR approved and merged to main
|
v
CD triggers: terraform apply -auto-approve
(applies the exact planned changes)
|
v
Notification sent (Slack, email, etc.)
Apply output stored in run logs
Setting Up GitHub Actions for Terraform
Repository Structure
. ├── .github/ │ └── workflows/ │ └── terraform.yml ├── main.tf ├── variables.tf ├── outputs.tf └── terraform.tfvars
GitHub Actions Workflow File
name: Terraform CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
env:
TF_VERSION: "1.9.0"
jobs:
terraform:
name: Terraform
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- 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: us-east-1
- name: Terraform Init
run: terraform init
- name: Terraform Format Check
run: terraform fmt -check
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
- name: Post Plan to PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const output = `
#### Terraform Plan 📋
\`\`\`
${{ steps.plan.outputs.stdout }}
\`\`\`
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
Storing Credentials Securely in GitHub
Never put AWS credentials in the workflow file or in committed code. Store them in GitHub's encrypted secrets:
- Go to your repository on GitHub
- Click Settings → Secrets and variables → Actions
- Click New repository secret
- Add
AWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY
GitHub injects these as environment variables during the pipeline run. They never appear in logs.
Using OIDC Instead of Long-Lived Credentials
Long-lived AWS access keys are a security risk. A better approach is OpenID Connect (OIDC) — GitHub Actions proves its identity to AWS directly, and AWS grants temporary credentials for that specific run. No long-lived keys to rotate or leak.
- name: Configure AWS Credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
This requires an IAM role with a trust policy that allows GitHub Actions to assume it via OIDC — a one-time setup that eliminates stored credentials entirely.
Pinning the Terraform Version
Always pin the Terraform version in both your pipeline and your configuration to ensure every run uses the same binary.
# In the workflow:
terraform_version: "1.9.0"
# In terraform.tf:
terraform {
required_version = "= 1.9.0"
}
Key Points
- A Terraform CI/CD pipeline runs format checks and validation on pull requests, posts the plan as a PR comment, and applies automatically on merge to main.
- Store cloud credentials in GitHub Secrets — never in code or workflow files.
- Use OIDC-based credential federation to eliminate long-lived AWS access keys from your pipelines entirely.
- Pin Terraform version in both the workflow and the
required_versionconstraint to guarantee consistent behaviour. - Save the plan file with
-out=tfplanand apply it withterraform apply tfplanto ensure the applied changes match exactly what was reviewed.
