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:#fffWhy 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:#fffAutomated 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:#c62828Approval 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.