💡 Why toInfrastructureIntermediate✨ AI-assisted

Why Terraform Plans Belong in Every Pull Request

WittyTech Daily··4 min read
#terraform#code-review#ci#atlantis

A reviewer opens a 12-line Terraform diff, sees a renamed variable and a tweaked tag, and clicks approve. Nobody notices that the change forces a replacement of the production database. The diff was honest. It just wasn't the whole story.

That's the case for a terraform plan pull request habit: the diff shows what you wrote, the plan shows what will happen.

Why a Terraform diff hides real impact

Terraform code is a description of desired state. The effect depends on current state, provider behavior, modules, and defaults you never typed. A few things a diff can't show:

  • A harmless-looking edit that marks a resource as forces replacement, so it gets destroyed and recreated.
  • A module version bump that adds or removes a dozen resources.
  • A count or for_each change that quietly deletes items from the middle of a list.
  • State that has moved since the author last ran anything.

The terraform plan command exists to answer exactly this: it compares configuration to real state and lists what would be created, changed or destroyed. Reviewing infrastructure code without it is like reviewing a SQL migration without knowing how many rows it touches.

Post the plan as a pull request comment

If the plan lives on the author's laptop, it won't be read. If it sits in the PR, it will. This GitHub Actions job runs a plan on every PR that touches infra/ and posts the result as a single comment.

name: terraform-plan
on:
  pull_request:
    paths: ["infra/**"]

permissions:
  contents: read
  pull-requests: write

jobs:
  plan:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: infra
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_wrapper: false
      # Add your cloud auth step here, ideally OIDC to a read-only role
      - run: terraform init -input=false
      - run: terraform plan -input=false -no-color -lock=false -out=tfplan
      - run: terraform show -no-color tfplan > plan.txt
      - name: Comment plan on the PR
        env:
          GH_TOKEN: ${{ github.token }}
          PR: ${{ github.event.pull_request.number }}
        run: |
          {
            echo "### Terraform plan"
            grep -E '^(Plan:|No changes)' plan.txt || true
            echo "<details><summary>Full plan</summary>"
            echo
            echo '```terraform'
            head -c 60000 plan.txt
            echo
            echo '```'
            echo "</details>"
          } > comment.md
          gh pr comment "$PR" --body-file comment.md --edit-last --create-if-none

The Plan: summary line sits above the collapsed full output, so a reviewer sees "1 to add, 0 to change, 1 to destroy" without scrolling. The head -c 60000 keeps you under GitHub's comment size limit. -lock=false stops a PR plan from blocking a real apply, which is fine because this plan never applies anything. --edit-last updates the previous comment instead of piling up a new one per push.

Prefer not to write this yourself? Atlantis does the same job: it runs atlantis plan automatically when a PR opens and posts the output, and you apply by commenting. The principle is identical, so pick whichever fits your team.

Answer the objection: noisy plans and secrets in output

The strongest pushback is real. Plans can be long, and plan output can leak secrets.

Noise. Collapse the full plan, lead with the summary line, and fix the root cause. A plan that always shows 40 unrelated changes is telling you about drift or unpinned providers, which is worth knowing. Split giant root modules so each PR gets a plan scoped to what changed.

Secrets. Terraform redacts values marked sensitive in plan output, but only those. A password in a plain string attribute or a tag will print. Also, the saved tfplan file can contain sensitive values in cleartext, so never upload it as a public or widely shared artifact. Mitigate with:

  • Marking secret variables and outputs sensitive = true.
  • Fetching secrets from a secret manager at runtime instead of passing them as config.
  • Posting only the text from terraform show, never the plan file.
  • Using a read-only credential for PR plans.

Fork PRs get a read-only token and no secrets by default, so the job above won't comment on them. That's a feature: don't loosen it to make plans work for strangers.

Gotchas

  • A plan is a snapshot. If state changes before merge, re-plan at apply time.
  • Plans from a PR need real backend access. Use a role that can read state but not change infrastructure.
  • Don't treat a green plan as approval. It tells reviewers what to look at, not whether it's right.

Start with one repository

Add the job to your noisiest infrastructure repo this week and make "read the plan" part of the review checklist. Infrastructure changes are the ones you can't cheaply roll back, so the people approving them should see the real impact before it reaches production.

← More in Infrastructure