DevOps Peach

Advanced Terraform · Testing and CI/CD · Lesson 3 of 4

Plan and Apply in a CI Pipeline

A pull request runs checks and terraform plan -out, posts the plan for review, and after approval applies exactly that saved plan, with short-lived OIDC credentials and one run at a time per state.

Practice interactively →

In a team nobody runs apply from a laptop. The pipeline does, and the plan that was reviewed is exactly the plan that gets applied.

flowchart LR
  PR["Pull request"] --> CK["fmt, validate, tflint, test"]
  CK --> PL["plan -out=tfplan"]
  PL --> CM["Plan posted on the PR"]
  CM --> RV["Review + approval"]
  RV --> MG["Merge to main"]
  MG --> AP["apply tfplan (protected environment)"]
  %% step PR: A change starts as a pull request; branch protection forces the pipeline to run.
  %% step CK: Static checks and tests run first because they are fast and need no credentials.
  %% step PL: plan -out saves the exact set of changes to a binary file.
  %% step CM: The pipeline posts terraform show output as a comment so reviewers read the real diff.
  %% step RV: A second person approves both the code and the plan.
  %% step AP: Apply uses the saved plan; if the state changed since, Terraform refuses it as stale.

The key commands

export TF_IN_AUTOMATION=1
terraform init -input=false
terraform plan -input=false -out=tfplan
terraform show -no-color tfplan > plan.txt        # human-readable, for the PR comment
terraform show -json tfplan > plan.json           # machine-readable, for policies (4.4)
terraform apply -input=false tfplan               # no prompt: a saved plan is already approved

A saved plan contains the full configuration and variable values, so it may hold secrets: keep it as a short-lived, access-controlled artifact, never in a public place.

GitHub Actions sketch

name: terraform
on:
  pull_request:
  push:
    branches: [main]
permissions:
  id-token: write          # OIDC token for the cloud role
  contents: read
  pull-requests: write
concurrency:
  group: tf-dev            # one run per state at a time
jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with: { terraform_version: 1.13.4 }
      - run: terraform init -input=false
      - run: terraform plan -input=false -out=tfplan
      - run: terraform show -no-color tfplan > plan.txt
      # post plan.txt as a PR comment, upload tfplan as an artifact

The apply job runs on main in a protected environment with required reviewers, downloads the artifact and runs terraform apply tfplan. Many teams use Atlantis, HCP Terraform or Spacelift instead of hand-written workflows; the flow is the same.

Credentials

Traps: applying from a fresh plan on merge instead of the reviewed saved plan means the applied changes can differ from what was approved; and two pipelines applying the same state at once wait on the lock or fail, which is why concurrency groups matter.

Practice interactively →