Stage 4 · Provision
Terraform at Scale
Plan Review Automation
PR comments, JSON plans, infracost diffs, and detecting destructive changes in CI — reviewing infrastructure changes automatically.
Plan in CI
Running terraform plan in CI gives reviewers visibility into infrastructure changes before they are applied. The plan output should be posted as a PR comment so reviewers can see exactly what will change without checking out the branch.
name: Terraform Plan
on: [pull_request]
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: terraform init -backend=false
- name: Terraform Plan
id: plan
run: terraform plan -out=tfplan -no-color
continue-on-error: true
- name: Comment on PR
uses: actions/github-script@v7
with:
script: |
const plan = `${{ steps.plan.outputs.stdout }}`
github.rest.issues.createComment({
issue_number: context.issue.number,
body: ```\n${plan}\n```
})The plan step runs with continue-on-error: true so the workflow continues even if the plan shows changes. The comment step posts the plan output as a PR comment. The -no-color flag removes ANSI escape codes for clean PR comments.
JSON Plan Output
The -json flag produces machine-readable plan output. Each line is a JSON object representing a resource change. This format is ideal for programmatic analysis, policy checks, and custom reporting.
$ terraform plan -out=tfplan -json > plan.json
# or
$ terraform show -json tfplan > plan.json
# Parse with jq
$ jq '[.resource_changes[] | select(.change.actions != ["no-op"])]' plan.jsonThe JSON plan contains resource changes, provider configurations, and metadata. Use jq to filter for actual changes (non-no-op actions). This is useful for building custom CI checks.
# Count resources to be added, changed, destroyed
$ jq '{
add: [.resource_changes[] | select(.change.actions[] == "create")] | length,
change: [.resource_changes[] | select(.change.actions[] == "update")] | length,
destroy: [.resource_changes[] | select(.change.actions[] == "delete")] | length
}' plan.json
# Find all resources being destroyed
$ jq '.resource_changes[] | select(.change.actions[] == "delete") | .address' plan.jsonThese jq queries extract key information from the plan. The count query shows the summary. The destroy query lists all resources that will be deleted. Use these in CI to gate on destructive changes.
PR Comments
- name: Comment with plan summary
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const plan = JSON.parse(fs.readFileSync('plan.json'));
const adds = plan.resource_changes.filter(r => r.change.actions.includes('create')).length;
const changes = plan.resource_changes.filter(r => r.change.actions.includes('update')).length;
const destroys = plan.resource_changes.filter(r => r.change.actions.includes('delete')).length;
const body = `## Terraform Plan Summary
| Action | Count |
|--------|-------|
| Add | ${adds} |
| Change | ${changes} |
| Destroy | ${destroys} |
<details><summary>Full plan</summary>
~~~
${planText}
~~~
</details>`;
github.rest.issues.createComment({
issue_number: context.issue.number,
body: body
})This script parses the JSON plan, counts resource changes, and creates a formatted PR comment. The full plan is in a collapsible details block. This gives reviewers a quick summary with the option to see details.
Cost Estimation
Infracost estimates the cost impact of infrastructure changes. It reads the plan JSON and shows the monthly cost diff. This helps teams understand the financial impact of their changes.
$ infracost auth login
$ infracost breakdown --path plan.json
$ infracost diff --path plan.json
# Output
# Monthly cost estimate: +$45.60
# + aws_instance.web (3x t3.medium): +$45.60
# - aws_instance.legacy (2x t3.micro): -$13.44
# Net change: +$32.16infracost breakdown shows the full cost estimate. infracost diff shows only the cost change. Both can generate PR comments with cost impact. This catches expensive changes before they are applied.
Detecting Destructive Changes
#!/bin/bash
# Check for destructive changes in the plan
DESTROY_COUNT=$(jq '[.resource_changes[] | select(.change.actions[] == "delete")] | length' plan.json)
if [ $DESTROY_COUNT -gt 0 ]; then
echo "ERROR: Plan includes $DESTROY_COUNT resource destructions"
jq '.resource_changes[] | select(.change.actions[] == "delete") | .address' plan.json
exit 1
fiThis script fails CI if any resources will be destroyed. Reviewers must approve destructions explicitly. This prevents accidental data loss from unexpected deletions.
Approval Gates
- Require plan approval before apply — manual review of the plan output.
- Gate on plan exit code — fail CI if the plan shows changes.
- Require approval for destructive changes — separate approval for deletions.
- Use GitHub environment protection rules — require approvals for production.
- Store plan artifacts — audit trail of what was reviewed and approved.
terraform show -json reads an existing plan file and produces JSON output. This avoids running the plan twice and ensures the JSON matches the plan that was approved.
Large plans may exceed GitHub's comment size limit. Truncate the output or use collapsible sections. For very large plans, post a summary and link to the full plan as a CI artifact.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.