Plan Approval Strategy #

terraform plan shows what will change. terraform apply actually changes it. Between the two there’s a very valuable gap: the opportunity for verification, review, and approval before production infrastructure is modified. How this gap is managed — who must approve, under what conditions automatic applies are allowed, and how to make sure what’s applied is exactly what was reviewed — is the essence of a plan approval strategy.

flowchart TD
    A["terraform plan"] --> B["plan output"]
    B --> C["Review"]
    C -->|"approve"| D["terraform apply"]
    C -->|"reject"| E["Revise config"]
    E --> A

    style A fill:#3b82f6,stroke:#1e40af,color:#fff
    style B fill:#10b981,stroke:#059669,color:#fff
    style C fill:#f97316,stroke:#ea580c,color:#fff
    style D fill:#10b981,stroke:#059669,color:#fff
    style E fill:#ef4444,stroke:#dc2626,color:#fff

Why Approval Matters #

WITHOUT AN APPROVAL STRATEGY:

  Developer A runs plan → sees the output → applies
  Problems:
  - Nobody knows what changed besides Developer A
  - The reviewed plan and the applied plan can differ
    (if something changed in between)
  - No audit trail of who authorized the change
  - For production: one person could destroy all resources unnoticed

WITH AN APPROVAL STRATEGY:

  Developer A → PR with configuration changes
  CI/CD → runs the plan, posts it to the PR as a comment
  Reviewer B → reviews the plan, approves the PR
  Merge to main → apply runs automatically OR is triggered manually
  Every step is recorded in Git history

Pattern 1: PR-Based Approval (Most Common) #

This is the most widely used pattern because it leverages the PR review infrastructure every team already has.

WORKFLOW:

  1. Developer creates a branch, changes the .tf configuration
  2. Pushes and opens a PR
  3. The CI pipeline automatically:
     - Runs terraform plan
     - Posts the plan output to the PR as a comment
     - Uploads tfplan as an artifact
  4. The reviewer sees the plan in the PR comment — not just the code, but its IMPACT
  5. The reviewer approves the PR if the plan is safe
  6. Merge to the main branch
  7. The pipeline on the main branch:
     - Downloads the same tfplan artifact from step 3
     - Runs terraform apply tfplan
     - NOT a new plan — the plan that was reviewed

THE SECURITY KEY:
  Use the same tfplan between review and apply.
  Don't run `terraform plan` again at apply time —
  the infrastructure condition can change between PR open and merge.
# GitHub Actions: Apply using the artifact from the PR plan
# .github/workflows/apply.yml

name: Terraform Apply

on:
  push:
    branches: [main]

jobs:
  apply:
    runs-on: ubuntu-latest
    environment: production  # Environment protection rules apply here
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "1.7.0"

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/TerraformApplyRole
          aws-region: ap-southeast-1

      - name: Download Plan Artifact
        uses: dawidd6/action-download-artifact@v3
        with:
          # Download the artifact from the merged PR (not a new plan)
          workflow: terraform.yml
          commit: ${{ github.event.before }}  # The commit before the merge
          name: tfplan-${{ github.event.before }}
          path: infrastructure/environments/production/

      - name: Terraform Init
        run: terraform init
        working-directory: infrastructure/environments/production

      - name: Terraform Apply
        run: terraform apply tfplan
        working-directory: infrastructure/environments/production

Pattern 2: Environment Protection Rules #

GitHub and GitLab provide native mechanisms to require approval before a workflow runs in a specific environment.

# GitHub: Configuration in Settings > Environments > production
# No extra code needed — configuration in the GitHub UI

# In the workflow, just reference the environment:
jobs:
  apply:
    environment: production  # <- The pipeline will pause and wait for approval
    runs-on: ubuntu-latest
    steps:
      - name: Terraform Apply
        run: terraform apply tfplan

# In the GitHub UI, set:
# - Required reviewers: [reviewer names]
# - Wait timer: 0 minutes (or more if you need preparation time)
# - Deployment branches: main only
# GitLab: when: manual on the apply job
apply:production:
  stage: apply
  script:
    - terraform apply tfplan
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual        # Requires a manual click
      allow_failure: false
  environment:
    name: production
  # In GitLab Settings > CI/CD > Protected Environments:
  # Only certain roles can click "play" on this job

Pattern 3: Atlantis — Pull Request Automation #

Atlantis is an open-source tool that runs as a server and manages the entire Terraform lifecycle through PR comments. Very popular for teams wanting a more integrated workflow.

HOW ATLANTIS WORKS:

  Developer opens a PR → Atlantis automatically runs terraform plan
  The plan output appears in the PR as a comment from the "atlantis" bot

  To apply:
  A reviewer types in the PR comment: atlantis apply
  Atlantis runs terraform apply with the reviewed plan
  Atlantis posts the apply result to the PR comment
  Atlantis automatically merges the PR if apply succeeds

ATLANTIS BENEFITS:
  - The whole workflow lives in the PR — no need to open another UI
  - Automatic locking — can't apply two PRs at once for the same workspace
  - Complete audit trail in the PR comment history
  - Multi-directory support — can handle many workspaces at once
# atlantis.yaml — configuration at the repository root
version: 3
projects:
  - name: production
    dir: infrastructure/environments/production
    workspace: default
    terraform_version: v1.7.0
    autoplan:
      when_modified:
        - "*.tf"
        - "*.tfvars"
        - "../../../modules/**/*.tf"
      enabled: true
    apply_requirements:
      - approved          # Requires at least 1 approval
      - mergeable         # The PR must not have conflicts

When Auto-Apply Is Allowed #

Not every apply must go through manual approval. There are conditions where full automation makes sense.

AUTOMATIC APPLY (no manual approval) — SAFE if:
  ✓ Only dev or staging environments
  ✓ Changes are only non-destructive resources (adding new resources)
  ✓ There are automated tests run after the apply
  ✓ Small team with a high level of trust
  ✓ Infrastructure is easy to recreate if something goes wrong

MANUAL APPLY (requires approval) — MANDATORY if:
  ✗ Production environment
  ✗ There are destroy operations in the plan (-/+ or -)
  ✗ Changes to stateful resources (databases, state backends)
  ✗ Changes to security configuration (IAM, security groups)
  ✗ Large team or a team still in the onboarding process
# GitHub Actions: Auto-approve for dev, manual for production
jobs:
  apply-dev:
    if: contains(github.event.head_commit.message, '[dev]')
    environment: dev  # No required reviewers
    steps:
      - run: terraform apply -auto-approve tfplan

  apply-production:
    if: github.ref == 'refs/heads/main'
    environment: production  # Required reviewers configured
    steps:
      - run: terraform apply tfplan

flowchart TD
    A["plan file"] --> B["Automation\nchecks"]
    B -->|"pass"| C["Team review"]
    B -->|"fail"| D["Reject"]
    C -->|"approve"| E["Apply"]
    C -->|"reject"| D

    style A fill:#3b82f6,stroke:#1e40af,color:#fff
    style B fill:#8b5cf6,stroke:#6d28d9,color:#fff
    style C fill:#f97316,stroke:#ea580c,color:#fff
    style D fill:#ef4444,stroke:#dc2626,color:#fff
    style E fill:#10b981,stroke:#059669,color:#fff

Automated Approval Gates #

Approval gates ensure the plan is reviewed before applying, even in fully automated pipelines.

# GitHub Actions: Environment protection rules
jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - run: terraform plan -out=tfplan
      - uses: actions/upload-artifact@v4
        with:
          name: tfplan
          path: tfplan

  apply:
    needs: plan
    runs-on: ubuntu-latest
    environment: production  # Requires approval!
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: tfplan
      - run: terraform apply tfplan
flowchart LR
    A["Plan"] --> B["Upload\nartifact"]
    B --> C["Approval\nGate"]
    C -->|"Approved"| D["Apply"]
    C -->|"Rejected"| E["Cancel"]

    style A fill:#e3f2fd,stroke:#1565c0
    style C fill:#fff3e0,stroke:#e65100
    style D fill:#e8f5e9,stroke:#2e7d32
    style E fill:#ffebee,stroke:#c62828

Approval Audit Trails #

# Make sure all approvals are recorded for compliance

# GitHub Actions: approval events in the audit log
# Organization Settings → Audit log → filter: "environment_protection_rule"

# Terraform Cloud: approvals are recorded in the run history
# Every run records: who approved, when, comment

# Custom: send approval events to an audit system
# GitHub Actions: log the approval
jobs:
  apply:
    environment: production
    steps:
      - name: Log approval
        run: |
          echo "Approved by: ${{ github.actor }}"
          echo "Run ID: ${{ github.run_id }}"
          echo "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
          # Send to the audit system          

Plan Review Checklist #

PLAN REVIEW CHECKLIST:

□ Are all changes IN LINE with what was requested?
□ Is there any resource being accidentally deleted?
□ Is any sensitive value exposed in the plan?
□ Is the cost impact reasonable? (check in Terraform Cloud/Infracost)
□ Are security groups not open to 0.0.0.0/0?
□ Does the database not have deletion_protection = false?
□ Is the tag naming convention consistent?
□ Is there any breaking change that could cause downtime?

Auto-Apply Guardrails #

# DON'T auto-apply in production!
# But auto-applying in dev with guardrails is fine

# Terraform Cloud: per-workspace auto-apply setting
# dev workspace: auto-apply enabled
# staging workspace: auto-apply disabled
# production workspace: auto-apply disabled + require approval

# Sentinel policy for guardrails
# policy/restrict-auto-apply.sentinel
import "tfplan/v2" as tfplan
import "tfrun" as tfrun

main = rule {
  tfrun.workspace.name is not "production" or
  all tfplan.resource_changes as _, rc {
    not (rc.change.actions contains "delete")
  }
}

Summary #

  • PR-based approval is the most common pattern — the plan is automatically posted to the PR, reviewers see the impact, merge approval = authorization to apply.
  • Use the same tfplan between review and apply — don’t re-plan at apply time, the infrastructure condition can change in between.
  • Environment protection rules in GitHub/GitLab allow approval at the workflow level, not just the PR level — suitable for more complex pipelines.
  • Atlantis is a more integrated solution — the entire Terraform workflow (plan, review, apply) happens through PR comments without opening another UI.
  • Automatic applies are safe for dev and staging, but production always needs approval — especially if there are destroy operations or security changes.
  • Audit trails are a hidden benefit of a good approval strategy — Git history records who authorized every infrastructure change.

← Previous: CI Pipeline   Next: Automated Apply →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact