Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXLIV · ArtifactsTerraformPlans

Terraform plans as artifacts — the plan file as a review surface, plans across jobs

Intermediate⏱ ~22 mingit

What you'll learn

  • Upload a terraform plan file as an artifact with actions/upload-artifact@v4
  • Download the plan in the apply job with actions/download-artifact@v4 and apply it with terraform apply
  • Surface the plan to reviewers in the PR using the GitHub Actions UI
  • Recognise why the plan artifact must be the binary plan (not a regenerated text plan) for the audit trail to hold

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

Not yet marked complete on this device.

A Terraform plan is the most consequential artifact a CI pipeline produces. The plan is the contract between the PR and the production state change: it is the set of resource modifications the merge will apply, signed by the commit that proposed them, reviewed by the engineers who must approve them. The artifact is the mechanism that holds the contract: the plan file is uploaded in the plan job, downloaded in the apply job, and inspected in the PR by humans. Lose the artifact and the pipeline applies a plan that was never reviewed.

The plan as a binary artifact

Terraform produces a binary plan file when invoked with -out=FILE. The binary format is the canonical form: Terraform hashes the file and verifies the hash on apply. The text format produced by terraform show is a human-readable render of the binary plan, but the binary is the contract:

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.5
      - name: Terraform init
        run: terraform init -backend-config=env-config.hcl
      - name: Terraform plan
        run: terraform plan -out=tfplan -input=false
      - name: Upload plan
        uses: actions/upload-artifact@v4
        with:
          name: terraform-plan
          path: tfplan
          retention-days: 90
      - name: Render plan as text
        run: terraform show -no-color tfplan > tfplan.txt
      - name: Upload plan text
        uses: actions/upload-artifact@v4
        with:
          name: terraform-plan-text
          path: tfplan.txt
          retention-days: 90

The plan job uploads two artifacts: the binary tfplan (the contract) and the text tfplan.txt (the review surface). The PR UI renders the text for reviewers; the apply job downloads the binary and applies it.

The apply job downloads the binary

The apply job must apply the binary plan that was reviewed, not a regenerated plan. Regenerating the plan under a different checkout, a different state read, or a different time produces a different plan; the apply executes a different contract than the one that was reviewed:

  apply:
    needs: plan
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.5
      - name: Download plan
        uses: actions/download-artifact@v4
        with:
          name: terraform-plan
          path: ./plan
      - name: Terraform init
        run: terraform init -backend-config=env-config.hcl
      - name: Terraform apply
        run: terraform apply -input=false ./plan/tfplan

The apply step reads the binary plan, verifies the hash, and executes the changes. The audit trail is: the PR was reviewed against tfplan.txt; the apply executed tfplan; the two are bound by the artifact upload and download.

The plan as a review surface

The PR UI surfaces the artifact to reviewers. The Actions tab shows the workflow run; the run shows the plan job; the plan job’s artifacts list shows terraform-plan and terraform-plan-text. A reviewer clicks the text artifact and sees the rendered plan. The review is the act of reading the plan and approving the PR; the approval is the human signature on the contract.

flowchart LR
    A["PR opened with .tf changes"] --> B["Plan job"]
    B --> C["terraform plan -out=tfplan"]
    C --> D["upload-artifact terraform-plan"]
    C --> E["terraform show > tfplan.txt"]
    E --> F["upload-artifact terraform-plan-text"]
    D --> G["Artifact store"]
    F --> G
    G --> H["Apply job downloads terraform-plan"]
    G --> I["PR reviewer downloads terraform-plan-text"]
    H --> J["terraform apply -input=false"]
    I --> K["Reviewer reads plan, approves PR"]
    K --> L["Merge triggers apply"]
    L --> H

The chain is: commit → plan → binary plan → artifact → applied plan. The reviewer sees the text; the apply sees the binary; both are the same plan, bound by the run ID and the commit SHA.

Production discipline

  1. Upload the binary plan (-out=tfplan) and the text plan (terraform show) as separate artifacts. The binary is the contract; the text is the review surface.
  2. Apply the downloaded binary plan. Never regenerate the plan in the apply job.
  3. Run the plan job on the PR trigger. The reviewer must see the plan before approval.
  4. Set retention-days to the audit window. A typical value is 90 days; compliance-bound teams may need longer.
  5. Gate the apply on a protected environment. The apply job must require approval to run; the artifact is the evidence, the environment approval is the human gate.

Cross-course references

  • Git, CI/CD & GitOps — Part XLIV-01 (What an artifact is) covers the artifact model and the audit invariant.
  • Terraform for Production Sysadmins — Parts IX-XII (State) cover Terraform state, the artifact store for plans, and the audit invariant.
  • Git, CI/CD & GitOps — Part XXXVIII-06 (Artifacts, caches, and outputs) covers the three-mechanism model.

Quiz

Knowledge check · 4 questions

  1. Q1. A Terraform apply job re-runs `terraform plan` and then applies the new plan. What is the failure mode?

  2. Q2. A pipeline that uploads the text plan (`terraform show`) as an artifact but not the binary plan (`-out=tfplan`) is not sufficient for the apply job to apply the reviewed plan.

  3. Q3. Explain why the binary plan and the text plan are uploaded as two separate artifacts.

  4. Q4. Diagnose why a Terraform apply ran changes that the reviewer did not approve, and prescribe the fix.

    Team T's pipeline runs the plan job on the `pull_request` trigger and uploads the binary plan as an artifact. The apply job runs on `push` to `main` (after merge). The apply job re-checks-out the source, re-runs `terraform plan`, and applies the new plan. A reviewer approves a PR that adds an S3 bucket. After merge, the apply job creates the S3 bucket, plus an IAM role that the reviewer did not see in the plan. The IAM role is in production. The audit trail shows the PR was approved, but the apply executed a different plan than the one reviewed.

Passing score: 75%. Answers are checked in this browser.