Articles

Terraform in a team: state layout, blast radius and reviewed plans

How to split Terraform state, run plans in CI and apply only what was reviewed, so infrastructure changes become routine instead of risky.

The examples are generic and illustrative. They are not taken from any client environment: adapt thresholds and settings to your own system.

The state file is the real boundary

Every terraform apply can change anything in the state it works on. That makes the state, not the module, the unit of risk. With one state for a whole environment, a typo in a DNS record and a change to the production database travel through the same plan — and that plan takes minutes to compute.

Split by lifecycle and blast radius

Group resources that change together and fail together. A common layout separates, for each environment:

  • Foundation: accounts, networks, DNS zones — changed rarely.
  • Data: databases, buckets, queues — changed carefully and protected from deletion.
  • Compute and services: clusters, functions, load balancers — changed often.

Each layer reads the outputs of the layer below and never manages its resources. Environments share the same modules but have separate state and separate credentials, so an apply in staging cannot touch production.

Remote state, locking and versions

  • Keep state in a remote backend with locking and versioning enabled — never in Git and never on a laptop.
  • Commit the dependency lock file (.terraform.lock.hcl) and pin module versions, so a plan today and a plan next month run the same code.
  • Protect stateful resources with prevent_destroy and with the provider's own deletion protection.

Plan on every pull request, apply what was reviewed

Run terraform plan in CI for every change and post the result on the pull request. After approval, apply the saved plan instead of planning again, so production receives exactly what the reviewers read; if the state changed in between, Terraform rejects the stale plan. The pipeline should authenticate with short-lived credentials through OIDC federation, not with long-lived keys stored as secrets.

terraform init -input=false
terraform plan -input=false -lock-timeout=5m -out=tfplan
terraform show -no-color tfplan > plan.txt   # attach to the pull request

# after approval, applying the same saved plan:
terraform apply -input=false tfplan

Detect drift on a schedule

Changes in the console still happen, usually during incidents. A scheduled job running terraform plan -detailed-exitcode finds them: exit code 0 means no changes, 2 means the real infrastructure differs from the code, and 1 means an error. Turn a 2 into a ticket, then either codify the change or revert it.

Policy before apply

Rules such as “no public buckets” or “every resource has an owner tag” are better checked by a machine than by a reviewer at the end of a long day. Policy-as-code tools such as Open Policy Agent, through Conftest, can evaluate the JSON form of the plan (terraform show -json) in CI and block the merge.

Contact

Want a second opinion on your setup?

Send a short description of your system. A senior engineer replies in writing within one business day.

Discuss your infrastructure