Automated Terraform Plan Guardrails & Pre-Apply Disaster Prevention
Test Your Review Reflexes in the Arcade
Practice catching RDS replacement traps and count wipes under 30-second timer pressure.
prevent_destroy
Attach explicit lifecycle { prevent_destroy = true } locks to persistent cloud assets (databases, KMS keys, storage buckets). This ensures Terraform errors out locally during the plan phase if an engineer accidentally modifies an immutable attribute:
# main.tf - Critical Database Safeguard
resource "aws_db_instance" "core_production" {
identifier = "core-postgres-prod"
engine = "postgres"
instance_class = "db.r6g.2xlarge"
allocated_storage = 500
lifecycle {
prevent_destroy = true
}
}
moved {} Blocks
When refactoring resource collections from integer count to map-based for_each, declare moved blocks in your root module to reindex state addresses without destroying running compute:
# migrations.tf - Declarative State Re-indexing
moved {
from = aws_instance.worker[0]
to = aws_instance.worker["api"]
}
moved {
from = aws_instance.worker[1]
to = aws_instance.worker["auth"]
}
In your build automation, export the execution plan to an encrypted binary artifact, then convert it to normalized JSON for automated policy engines:
# Generate binary plan
terraform plan -out=tfplan.binary
# Convert binary plan to JSON AST
terraform show -json tfplan.binary > tfplan.json
Write a Conftest policy in Rego (policy/disaster_prevention.rego) that inspects the resource_changes array in tfplan.json and denies any PR attempting to delete or replace critical resources:
package terraform.analysis
import future.keywords.in
default allow = false
# Rule: Deny any destruction of RDS databases in production
deny[msg] {
some resource in input.resource_changes
resource.type == "aws_db_instance"
"delete" in resource.change.actions
msg := sprintf("BLOCKED: Pull request attempts to DESTROY database: %v", [resource.address])
}
# Rule: Block immutable replacement traps on S3 buckets
deny[msg] {
some resource in input.resource_changes
resource.type == "aws_s3_bucket"
resource.change.actions == ["delete", "create"]
msg := sprintf("BLOCKED: Pull request forces REPLACEMENT of S3 bucket: %v", [resource.address])
}
Embed Conftest evaluation directly into your Pull Request CI workflow (.github/workflows/terraform-pr.yml):
name: Terraform Plan & Security Guardrails
on:
pull_request:
branches: [main]
jobs:
validate-plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- uses: open-policy-agent/conftest-action@v1
with:
version: '0.45.0'
- name: Terraform Plan
run: |
terraform init
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
- name: Conftest Policy Check
run: conftest test tfplan.json -p policy/
Test your defense layer by pushing a deliberate database renaming branch. Observe the automated GitHub Actions pipeline failing instantly with an actionable error before any human can execute apply:
FAIL - tfplan.json - terraform.analysis - BLOCKED: Pull request attempts to DESTROY database: aws_db_instance.core_production
1 test, 0 passed, 0 warnings, 1 failure, 0 exceptions