Terraform plan is the guardrail between your code and live infrastructure. It compares the desired configuration with the current state and previews the exact actions Terraform will take. You should use ‘Terraform plan’ before every apply to catch destructive changes, environment drift, and misconfigured variables.

This post breaks down how the plan engine works, how to read the output, and how to automate checks in your CI/CD pipeline. We’ll finish with copy/paste examples and practical tips to keep plans stable in teams.

Terraform Plan Basics

Terraform plan creates an execution plan without changing resources. It refreshes state (unless told not to), reads provider data, compares desired vs. current state, and prints the diff with 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 across runs
  • target=addr: focus planning on specific resources only for break-glass fixes; don’t use it for normal workflows because it can bypass dependencies

Exit handling with -detailed-exitcode:

  • 0: no changes
  • 1: error
  • 2: changes present

See the official command reference for exact behavior and flags: terraform plan command reference. [Link to Hashicorp plan command]

Tip: If you’re building a pipeline, read our quick guide on designing a Terraform CI/CD pipeline for AWS for end-to-end context.

How Terraform Plan Works Under the Hood

State is the main building block that anchors the comparison. Whether stored locally or in a remote backend, Terraform loads the state file, optionally refreshes it from providers, and computes the diff.

The refresh action calls the provider’s API to fetch the latest configuration of the actual resources. Terraform then compares this real-world state with the desired state defined in the code.

Each Terraform Provider defines the schemas and CRUD operations for Resources and Data Sources.

Data sources perform read operations during the plan phase to fetch information that is often needed by other resources. Resources, on the other hand, manage real infrastructure objects and are associated with actions such as create, update (in-place), replace, or destroy.

Reading Terraform Plan Output: Add, Change, Destroy

The key symbols:

  • + create
  • – destroy
  • ~ update in-place
  • -/+ replace (destroy then create)

After running ‘plan’, Terraform prints “Plan: X to add, Y to change, Z to destroy.” You should treat destroys in production with extra scrutiny and always ask for a second reviewer.

Modifying a variable or changing a module version can trigger a chain reaction: even a small input change can cascade into a resource replacement, potentially impacting production functionality.

For example, changing a default parameter might result in replacing a production database. By carefully reviewing the plan output – and watching for indicators like “-/+ replace” – you can often prevent unnecessary downtime.

Replaces vs. in-place updates matter. Any change forcing new resource creation (e.g., immutable attributes) shows as “-/+”. That’s your red flag.

Plan Files and JSON for Automation

Canonical snippet:
```bash
# Save a plan and export JSON
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json

# Example: count destroys and replaces
jq '[.resource_changes[] | select(.change.actions|index("delete"))] | length' plan.json
jq '[.resource_changes[] | select(.change.actions|index("replace"))] | length' plan.json
```

You should save the plan output and export JSON for automated guardrails. Here is how to do it:

Once you have the JSON in hand, there are several types of policies you might want to validate against it. For example:

  • Deny destroy in prod unless an approval label is present
  • Enforce required tags/owners before apply
  • Block if predicted cost exceeds a budget
  • Post PR annotations that highlight risky actions and affected resources

Terraform Plan Examples: Local CLI to CI/CD

Local workflow:

  • terraform init
  • terraform plan -out=plan.tfplan
  • terraform show plan.tfplan (or use the plan snippet above)
  • Apply only after review

Minimal GitHub Actions gate using -detailed-exitcode:

```yaml
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 and Best Practices for Terraform Plan

Here are some best practices to avoid surprises when running ‘Terraform plan’:

  • Pin provider versions; 
  • run terraform init -upgrade deliberately, not on every run.
  • Use a consistent remote backend with locking (e.g., S3 with DynamoDB) for teams; – Avoid using local state
  • Reduce plan churn: stabilize data sources, add explicit dependencies (depends_on), and keep var-files consistent across environments.
  • Keep providers and Terraform versions aligned across developer machines and CI containers .

Where ControlMonkey Fits: Safer, Faster Plans

ControlMonkey adds context to Terraform plan so reviewers focus on what matters. It surfaces risky actions (destroys/replaces), drift indicators, and improvement hints before apply. Organization-wide guardrails and approvals span repos and environments.

It classifies safe vs. risky items to reduce noise and complements your GitHub/GitLab/Bitbucket/Azure DevOps workflows and existing Terraform stacks.

Wrap-Up and Next Step: Review Terraform Plans With Confidence

Terraform plan is your preview of infrastructure change and your best defense against surprises. Use it consistently, export JSON for policy checks, and gate merges in CI.

If you want faster reviews and stronger guardrails across teams, ControlMonkey can help with risk-aware plan insights and centralized policies. Request a demo to see it in action.

Author

Daniel Alfasi

Daniel Alfasi

Backend Developer and AI Researcher

Backend Developer at ControlMonkey, passionate about Terraform, Terragrunt, and AI. With a strong computer science background and Dean’s List recognition, Daniel is driven to build smarter, automated cloud infrastructure and explore the future of intelligent DevOps systems.