How do you integrate Terraform into a CI/CD pipeline safely?
Quick Answer
A typical pipeline runs `terraform fmt -check` and `terraform validate` on every PR, then `terraform plan` and posts the plan as a PR comment for human review — never auto-applying straight from a PR. `apply` runs only on merge to the main branch (or via a manual approval gate), using a service principal/role with least-privilege access and a remote backend with locking so concurrent pipeline runs can't race. Pin the Terraform CLI and provider versions in CI to match local dev, and store the plan file as a build artifact so the exact reviewed plan is what gets applied ("plan then apply the same plan"), not a re-computed one.
Detailed Answer
Running Terraform safely from CI/CD is less about the YAML pipeline syntax and more about enforcing a review gate before anything actually touches infrastructure.
A typical pipeline shape
On every pull request:
- run: terraform fmt -check
- run: terraform init
- run: terraform validate
- run: terraform plan -out=tfplan
- run: <post tfplan output as a PR comment for human review>
On merge to main (or after manual approval):
- run: terraform init
- run: terraform apply tfplan # apply the *exact* plan that was reviewed
Key practices
- Never auto-apply from an unreviewed PR.
planruns on every PR so reviewers see the intended infrastructure diff alongside the code diff, butapplyis gated behind merge (or an explicit manual approval step for production). - Apply the plan you reviewed, not a freshly recomputed one. Saving the plan to a file (
-out=tfplan) and runningapply tfplanguarantees what gets applied is exactly what was shown in review — re-runningplanright beforeapplyrisks a subtly different plan if something in the environment changed in between. - Use a dedicated, least-privilege CI identity. The pipeline's AWS/Azure/GCP role should have only the permissions needed for the resources it manages, distinct from any individual engineer's broad access, and ideally distinct per environment (a
prod-scoped role separate fromdev). - Remote backend with locking is mandatory, since multiple pipeline runs (or a pipeline run overlapping with a manual
apply) could otherwise race on the same state. - Pin the Terraform CLI and provider versions in CI to match what's used locally — a version mismatch between a developer's laptop and the CI runner is a classic source of "plan looks different in CI" surprises.
- Require explicit approval for production applies — a manual "approve" gate (GitHub Environments, a Slack approval bot, Terraform Cloud's run approval) before the prod
applystep executes, even after the PR is merged.
Why this matters
The whole point of putting Terraform in CI is to make infrastructure changes go through the same rigor as application code changes: a visible diff, required review, and a controlled, auditable execution — never a human running apply -auto-approve from their laptop against production.