Terraform in CI/CD Pipelines
Running Terraform from a laptop works for learning, but production infrastructure needs a pipeline that runs the same commands the same way every time, with credentials scoped tightly, state locked against concurrent runs, and a human approval gate before anything destructive happens. A typical Terraform CI/CD pipeline chains together fast static checks, a plan stage that produces a reviewable artifact, an approval gate, and finally an apply stage that consumes the exact plan that was reviewed — never a fresh, potentially different plan.
Cricket analogy: Practicing in the backyard works for learning the game, but international cricket needs the same net session run identically every time, with kit checked, the pitch protected from tampering, and an umpire's approval before anything irreversible like a declaration happens.
The standard pipeline stages
A mature pipeline typically runs: (1) terraform fmt -check and terraform validate for cheap, fast correctness checks; (2) terraform plan -out=tfplan on every pull request, posting the human-readable summary as a PR comment for review; (3) policy-as-code evaluation against the plan; (4) a manual approval gate, often built into the CI tool or Terraform Cloud/Enterprise; and (5) terraform apply tfplan on merge to the main branch, applying the exact saved plan file rather than re-planning, which guarantees the applied changes match what was reviewed.
Cricket analogy: A mature squad selection process runs: fitness checks, then a probable-eleven review posted for selectors to comment on, then a policy check against player workload limits, then a chairman's approval, then fielding the exact eleven that was reviewed — never a fresh, different lineup on the day.
# .github/workflows/terraform.yml (excerpt)
- name: Terraform Init
run: terraform init -input=false
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -input=false -out=tfplan
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -input=false -auto-approve tfplanCredentials, state locking, and isolation
CI runners should authenticate using short-lived, workload-scoped credentials — for example, OIDC federation into an AWS IAM role or an Azure/GCP workload identity — rather than long-lived static access keys stored as CI secrets. A remote backend with state locking (S3+DynamoDB, Terraform Cloud, Azure Storage with a lease) is mandatory in CI, since concurrent pipeline runs (two PRs planning at once, or a re-run overlapping a previous apply) would otherwise corrupt the state file. Separate pipelines, workspaces, or even separate cloud accounts per environment (dev/staging/prod) prevent a mistaken apply from crossing environment boundaries.
Cricket analogy: A touring team should clear customs using short-term match visas scoped to the series, not a permanent residency card kept in a drawer; and a shared, locked team ledger — not two managers' personal notebooks — prevents two people recording conflicting substitutions during a rain-delayed match.
Applying the exact saved plan file (terraform apply tfplan) rather than re-running plan at apply time closes a subtle gap: infrastructure or provider data can change between review and merge, and reusing the saved plan guarantees what gets applied is precisely what a human (or policy engine) already approved.
Never store long-lived cloud access keys as plain CI secrets if avoidable — a leaked static key has no expiration, while OIDC-federated short-lived credentials scoped to a specific pipeline run drastically limit the blast radius of a compromised pipeline.
- A standard pipeline runs fmt/validate, then plan (on PR), then policy checks, then a gated apply (on merge).
- Applying a saved plan file (
terraform apply tfplan) guarantees what's applied matches exactly what was reviewed. - Remote state with locking (S3+DynamoDB, Terraform Cloud, etc.) is mandatory to prevent corruption from concurrent CI runs.
- Short-lived, OIDC-federated credentials are safer in CI than long-lived static access keys stored as secrets.
- Separate pipelines or workspaces per environment (dev/staging/prod) prevent accidental cross-environment applies.
- A manual approval gate between plan and apply keeps a human in the loop before infrastructure changes take effect.
Practice what you learned
1. Why does a mature Terraform CI/CD pipeline apply a saved plan file (`terraform apply tfplan`) rather than re-running `plan` at apply time?
2. Why is a remote backend with state locking considered mandatory in CI pipelines?
3. What is the security advantage of OIDC-federated credentials over long-lived static access keys in a CI pipeline?
4. In the standard pipeline stage order, where does the plan typically run relative to a pull request?
5. Why is environment isolation (separate workspaces, pipelines, or accounts) important in CI/CD for Terraform?
Was this page helpful?
You May Also Like
terraform validate and fmt
Understand two everyday Terraform commands — `terraform fmt` for consistent code style and `terraform validate` for catching syntax and configuration errors before you ever run a plan.
Plan Review and Policy as Code
Explore how teams review `terraform plan` output for correctness and safety, and how policy-as-code tools like Sentinel and OPA automatically enforce organizational rules on every plan.
Remote State Backends
Remote backends store Terraform's state file outside the local disk, enabling team collaboration, durability, and integration with locking mechanisms.
State Locking
State locking prevents two Terraform operations from writing to the same state file simultaneously, protecting against corruption and lost updates.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics