Terraform plan is the guardrail between your code and your live infrastructure. Every time you run it, Terraform compares your desired configuration with the current state and shows you exactly whatβs going to change β before anything actually happens.
If you want to avoid destructive changes, catch drift early, and prevent misconfigured variables from sneaking into production, this guide is for you. π
This post covers:
- How the Terraform plan engine works
- How to read plan output (add/change/destroy)
- How to automate plan checks in CI/CD
- Common flags you'll actually use
- Real copy/paste examples
- Team-friendly best practices
- Bonus: risk-aware reviews with ControlMonkey
Letβs get into it.
π§© Terraform Plan Basics
Terraform plan generates an execution plan without changing any resources. It refreshes state (unless disabled), evaluates providers and data sources, compares current vs. desired state, and prints a diff of proposed actions.
π§ Common terraform plan flags you'll actually use
-out=plan.tfplan # Save a binary plan file for apply
-refresh=true|false # Control state refresh before diff
-var / -var-file # Pass inputs consistently
-target=addr # Break-glass-only resource targeting
π Exit codes with -detailed-exitcode
0 β No changes
1 β Error
2 β Changes present
π Official reference (recommended): terraform plan command reference
π Also relevant: How to design a Terraform CI/CD pipeline for AWS
βοΈ How Terraform Plan Works Under the Hood
Terraform loads your state (local or remote), optionally refreshes it using provider APIs, and computes the diff.
Key components involved:
- State β Terraformβs source of truth
- Providers β Define schemas + CRUD operations
- Data sources β Read-only lookups executed during the plan
- Resources β Infrastructure objects that may be created, updated, replaced, or destroyed
A refresh step pulls the actual state of resources from the provider, and Terraform compares it with what your code declares.
π§ How to Read Terraform Plan Output
Terraform uses clear symbols in the diff:
+ create
- destroy
~ update in-place
-/+ replace (destroy + create)
A typical summary looks like:
Plan: X to add, Y to change, Z to destroy.
π¨ Production rule of thumb
Treat any destroy or any replace (-/+ ) as a red-flag that requires a second reviewer.
Small input changes (e.g., variable tweak, module version update) can cascade into unintended replacements β including databases or network resources.
π¦ Working With Plan Files + JSON (Automation-Ready)
A recommended workflow is to save the plan, export it to JSON, and run validations.
Canonical snippet:
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
# Count destroys & replaces
jq '[.resource_changes[] | select(.change.actions|index("delete"))] | length' plan.json
jq '[.resource_changes[] | select(.change.actions|index("replace"))] | length' plan.json
What you can automate with JSON:
- Block destroys in production unless approved
- Enforce mandatory tags/owners
- Fail if predicted cost exceeds budget
- Annotate PRs with risk indicators
π Terraform Plan Examples: Local CLI β CI/CD
Local workflow
terraform init
terraform plan -out=plan.tfplan
terraform show plan.tfplan
Review β approve β apply.
Minimal GitHub Actions gate using -detailed-exitcode
name: terraform-plan
on: pull_request
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.6
- run: terraform init
- id: plan
run: |
set +e
terraform plan -detailed-exitcode -out=plan.tfplan
echo "code=$?" >> $GITHUB_OUTPUT
set -e
- name: Upload Plan Artifact
uses: actions/upload-artifact@v4
with:
name: tfplan
path: plan.tfplan
- name: Fail If Changes Present
if: steps.plan.outputs.code == '2'
run: exit 1
𧨠Troubleshooting & Best Practices for Stable Terraform Plans
Here are proven ways to reduce surprises:
βοΈ Pin provider versions
Run terraform init -upgrade intentionally, not automatically.
βοΈ Use a consistent remote backend
S3 + DynamoDB locking, GCS, or Terraform Cloud.
Avoid local state in team environments.
βοΈ Stabilize your plan
- Avoid volatile data sources
- Use
depends_onwhen needed - Keep var-files consistent across environments
- Align Terraform + provider versions across laptops & CI
A stable plan = fewer loops of βwhy is this resource changing again?β
π¦ Where ControlMonkey Fits In (Optional but Powerful)
ControlMonkey adds context around Terraform plans so teams spot risk instantly:
- Highlights destroys & replacements
- Surfaces drift before running plan
- Enforces org-wide guardrails
- Adds automatic insights during plan review
- Runs across GitHub, GitLab, Bitbucket, Azure DevOps
If your team reviews plans daily, the noise reduction alone is a productivity unlock.
π Related reads:
- IaC Risk Index β https://controlmonkey.io/news/iac-risk-index/
- Atlantis + Plan Guide β https://controlmonkey.io/resource/how-to-use-atlantis-plan/
β Wrap-Up: Review Terraform Plans With Confidence
Terraform plan is the most important checkpoint in IaC. Use it consistently, export JSON for policy checks, and fail PRs when risky changes appear.
If you want faster reviews, automated guardrails, and risk-aware change visibility across teams, ControlMonkey can help β request a demo to see how it works.
π¬ What are your best Terraform plan tips or horror stories? Drop them in the comments β DevOps managers learn best from each other.
Top comments (0)