How do you integrate Terraform into a CI/CD pipeline safely?

5 minadvancedterraformci-cdautomation

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

  1. Never auto-apply from an unreviewed PR. plan runs on every PR so reviewers see the intended infrastructure diff alongside the code diff, but apply is gated behind merge (or an explicit manual approval step for production).
  2. Apply the plan you reviewed, not a freshly recomputed one. Saving the plan to a file (-out=tfplan) and running apply tfplan guarantees what gets applied is exactly what was shown in review — re-running plan right before apply risks a subtly different plan if something in the environment changed in between.
  3. 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 from dev).
  4. 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.
  5. 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.
  6. Require explicit approval for production applies — a manual "approve" gate (GitHub Environments, a Slack approval bot, Terraform Cloud's run approval) before the prod apply step 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.