Git, CI/CD & GitOpsXLIX · Infrastructure CIInfrastructure CI
Plan and review — terraform plan as the review artefact; the comment-on-PR pattern
What you'll learn
- Run terraform plan -out=tfplan and explain why the plan is saved as a binary
- Convert a plan to JSON with terraform show -json tfplan and explain what JSON adds
- Implement the comment-on-PR pattern for surfacing plan output on pull requests
- Recognise why re-planning between review and apply breaks the review contract
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
The plan-and-review stage is the sixth gate in the pipeline. It is the gate that humans operate; everything before it is mechanical, everything after it depends on the artefact this stage produces. The artefact is the plan: the exact set of changes Terraform would make if apply were run right now. The plan is what a reviewer approves, and the plan is what the apply stage executes. The two must be the same plan. A pipeline that re-plans between review and apply is a pipeline that has lost the meaning of review.
Plan as a binary
terraform plan produces two outputs by default: a
human-readable diff on stdout and a plan file on disk if
the -out flag is given. The plan file is a binary; it is
not a text file, it is not a diff, and it cannot be
regenerated from the diff. The binary contains the exact
state of every resource Terraform intends to change, plus
enough metadata for terraform apply to recognise and
apply that exact plan without re-evaluating.
terraform plan -out=tfplan
The -out=tfplan argument tells Terraform to save the plan
to a file called tfplan. The binary is the artefact that
the apply stage will use:
terraform apply tfplan
Without -out, the apply stage would have to re-plan to
know what to do. Re-planning can produce a different plan:
a resource may have drifted, a provider may have returned
different values, or a variable sourced from a remote
backend may have changed. A re-plan between review and
apply is a new plan that has not been reviewed. The
review contract is broken.
flowchart LR
A["plan -out=tfplan"] --> B["Binary plan file"]
B --> C["Reviewer reads diff"]
B --> D["Apply stage reads binary"]
C --> E["Approve"]
E --> D
Plan as JSON
The binary plan is the artefact for terraform apply, but
it is not human-readable. For review surfaces — pull request
comments, dashboards, audit logs — the plan needs to be in
a structured format. Terraform provides this with
terraform show:
terraform show -json tfplan > tfplan.json
The JSON output contains every resource the plan would create, update, or destroy, with full attribute values before and after, plus the action Terraform would take and the reason. The JSON is what the comment-on-PR pattern posts to the pull request, what the audit pipeline archives, and what downstream tooling diffs against previous plans.
The canonical fields reviewers read:
resource_changes[].change.actions— the list of actions Terraform will take:["create"],["update"],["delete"], or combinations like["delete", "create"]for replacement.resource_changes[].change.beforeandresource_changes[].change.after— the resource state before and after the apply.resource_changes[].change.replacing— true when a create-after-delete sequence will occur.
The comment-on-PR pattern
The review surface for a Terraform pull request is the pull request itself. The plan must appear on the pull request as a comment, updated on every push, so the reviewer can read the proposed changes without leaving the review tool. The comment-on-PR pattern is the standard implementation:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
- run: terraform plan -out=tfplan
- run: terraform show -json tfplan > tfplan.json
- uses: actions/upload-artifact@v4
with:
name: tfplan
path: tfplan
- uses: actions/upload-artifact@v4
with:
name: tfplan-json
path: tfplan.json
- name: Comment plan on PR
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('tfplan.json', 'utf8');
// ... render and post comment
The comment-on-PR pattern has four moving parts that must agree:
- The plan binary is uploaded as an artefact. The apply
job downloads that artefact and runs
terraform applyagainst it. - The plan JSON is uploaded as a separate artefact.
- The pull request comment is updated with a rendered version of the plan: resource actions, count of changes, and a link to the full JSON artefact.
- The apply job requires that the comment has been
approved — either through a GitHub Actions environment
approval, a
/applyslash command from a reviewer, or a required status check tied to the comment.
The discipline is that the binary plan and the rendered comment are produced by the same job. A reviewer reading the comment is reading the same bytes the apply stage will execute.
Why re-planning breaks the contract
A re-plan between review and apply is the failure mode the entire plan-and-review stage exists to prevent. The sequence that breaks the contract is:
- Reviewer reads the comment, sees no resource replacements, approves.
- An unrelated change touches a data source the plan used. The variable or remote data changes between review and apply.
- The apply stage re-plans. The re-plan produces a different plan, including a resource replacement the reviewer did not see.
- The apply executes the un-reviewed plan.
The fix is to make re-planning impossible:
- The apply job downloads the exact plan binary that was produced by the plan job. No re-plan command runs.
- The apply job runs
terraform apply tfplanwith the binary as the argument, notterraform apply -auto-approvewith no argument. - If the plan binary does not exist or does not match the committed SHA, the apply job fails before any change is made.
Production discipline
Three rules govern the plan-and-review stage:
- The plan that is reviewed is the plan that is applied. A pipeline that re-plans between review and apply has lost the meaning of review.
- Sensitive values are redacted before the plan
leaves the runner. The JSON plan is treated as
sensitive output regardless of
sensitiveannotations in the configuration. - The plan binary is the artefact of record. The plan binary is archived alongside the apply job’s logs; six months later, when an audit asks what was applied, the binary answers.
Cross-course references
- Terraform for Production Sysadmins — Part XXVI (PlanReview) covers the plan-as-artefact pattern in depth, including redaction patterns for sensitive variables.
- Kubernetes for Production Sysadmins — Part XXXV (GitOps) covers the analogous pattern in GitOps controllers, where the diff between desired and actual state replaces the plan.
- Pipeline Internals — Part XLIV (Artifacts) covers the artefact-handling primitives that the plan binary and JSON rely on.
Quiz
Knowledge check · 4 questions
Q1. A pipeline runs `terraform plan` on every pull request, posts the diff as a comment, then runs `terraform apply -auto-approve` on merge. What contract has been broken?
Q2. `terraform show -json tfplan` redacts variables marked `sensitive = true` from the output JSON.
Q3. Give the command that produces a binary plan file and the command that converts that file to JSON, naming the flag each uses.
Q4. Diagnose an audit failure where the bytes that were reviewed cannot be matched to the bytes that were applied, and propose the corrected pipeline.
Six months after a Terraform apply, an auditor asks: which exact plan bytes were approved, and which exact bytes were applied? The team can produce the review comment with the rendered plan, but the apply job ran `terraform apply -auto-approve` and did not archive the plan binary. The audit cannot prove that the applied plan matches the reviewed plan.
Passing score: 75%. Answers are checked in this browser.