TerraformXXII · CI/CD for Production TerraformCI
CI for Terraform: The Production Pipeline
What you'll learn
- Build a CI pipeline for Terraform
- Choose the right gates and the right checks
- Use saved plans for reviewable applies
- Recognise the production risks of skipping CI
Prerequisites
Verified against Terraform CLI 1.9.x · OpenTofu 1.7.x · HCL 2.0 · bpg/proxmox provider 0.66+ · hashicorp/local provider 2.5+ · hashicorp/null provider 3.2+ · hashicorp/random provider 3.6+ · hashicorp/http provider 3.4+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12
A CI pipeline is the production control for Terraform changes. The pipeline runs the validation, the plan, the policy, and the apply. The pipeline is the safety net. The lesson teaches the production-grade CI pipeline.
The pipeline stages
A production CI pipeline has these stages:
- Lint.
terraform fmt -check -recursiveandtflint. - Validate.
terraform init -backend=falseandterraform validate. - Security scan.
trivy config,tfsec, orcheckov. - Plan.
terraform plan -out=tfplan. - Review. The plan is uploaded as an artifact. The PR is reviewed.
- Apply.
terraform apply tfplan(after manual approval). - Notify. The on-call engineer is notified.
Each stage is a gate. A failure in any stage stops the pipeline.
The GitHub Actions example
name: terraform
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
# 1. Lint
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.x
- run: terraform fmt -check -recursive
- uses: terraform-linters/setup-tflint@v4
with:
tflint_version: latest
- run: tflint --recursive
# 2. Validate (no backend)
- run: terraform init -backend=false
- run: terraform validate
# 3. Security scan
- uses: aquasecurity/trivy-action@v0.20.0
with:
args: config --severity HIGH,CRITICAL
- run: trivy config --severity HIGH,CRITICAL .
# 4. Plan
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- run: terraform init
- run: terraform plan -out=tfplan -no-color
- uses: actions/upload-artifact@v4
with:
name: tfplan
path: tfplan
- uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = JSON.parse(
exec('terraform show -json tfplan').stdout
);
const counts = plan.resource_changes.reduce((acc, r) => {
acc[r.change.actions[0]] = (acc[r.change.actions[0]] || 0) + 1;
return acc;
}, {});
const body = `### Terraform plan\n\n` +
`| Action | Count |\n|--------|-------|\n` +
Object.entries(counts).map(([k, v]) => `| ${k} | ${v} |`).join('\n');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body,
});
# 6. Apply (only on main, after manual approval)
- if: github.ref == 'refs/heads/main'
uses: hashicorp/tf-actions@v1
with:
tf-actions-version: latest
command: apply
working-directory: .
terraform_version: 1.9.x
plan_artifact: tfplan
The pipeline:
- Runs on every PR and on every push to main.
- Runs the lint, validate, and security scan on every PR.
- Runs the plan on every PR.
- Comments the plan on the PR.
- Applies the saved plan only on main, after manual approval.
The gates
The pipeline gates (in order):
- Lint. Format errors. The fix is
terraform fmt. - Validate. Configuration errors. The fix is to fix the configuration.
- Security scan. Security issues. The fix is to fix the configuration.
- Plan. The plan is reviewed. The fix is to fix the plan or accept the plan.
- Apply. The apply is gated by manual approval.
Each gate is a production control. A failure in any gate stops the pipeline.
The plan review
The plan is uploaded as an artifact. The PR is reviewed by a second engineer. The review checks:
- The summary line matches the change ticket.
- The action types are expected.
- The replacements are expected.
- The destructions are expected.
- The drift is expected.
The review is the production control for the apply.
The apply
The apply is gated by manual approval:
# GitHub Actions with environment approval
jobs:
terraform-apply:
runs-on: ubuntu-latest
environment: production # requires manual approval
# ...
The apply uses the saved plan from the artifact:
- uses: hashicorp/tf-actions@v1
with:
command: apply
plan_artifact: tfplan
The apply is the same plan that was reviewed. The audit trail is the artifact.
The notification
The on-call engineer is notified when the apply completes:
- name: Notify on apply
if: always()
run: |
curl -fsSL -X POST "$SLACK_WEBHOOK" \
-d "Terraform apply ${{ github.run_id }} ${{ job.status }}"
The notification is the production control for the post-apply monitoring.
What comes next
The next lesson is CD / Terraform apply — the production deployment patterns.
Verification
Knowledge check · 7 questions
Q1. What is the role of the CI pipeline in Terraform?
Q2. What is the role of plan artifacts in CI/CD?
Q3. terraform apply -auto-approve in CI/CD is appropriate for production.
Q4. What is the role of OIDC for CI/CD?
Q5. Which of the following are part of a production CI/CD pipeline? (Select all that apply.)
Q6. What is the role of environment-specific CI/CD?
Q7. A CI/CD pipeline applies to production. The state is corrupted in dev. What is the impact?
Passing score: 75%. Answers are checked in this browser.