Modern QA2026Plan-Time Analysis: What Will Actually Change? — tiles
Log inJoin
3 / 75 · 08 Infrastructure as Code Testing · Terraform Validation← prev⊞ allnext →☰ Read as one page

1.3Plan-Time Analysis: What Will Actually Change?

terraform plan is your integration contract -- it shows the delta between your declared state and the real world. This is where you move from "is the code valid?" to "what will the code do?"

Generating and Analyzing Plans

# Generate a plan file for downstream analysis
terraform plan -out=tfplan -detailed-exitcode

# Exit codes:
# 0 = no changes
# 1 = error
# 2 = changes present (this is the important one)

# Convert plan to JSON for programmatic analysis
terraform show -json tfplan > tfplan.json

Programmatic Plan Assertions

The JSON plan output is a goldmine for automated testing. You can write assertions against it just like any other test:

# scripts/validate_plan.py
import json
import sys

def load_plan(path="tfplan.json"):
    with open(path) as f:
        return json.load(f)

def check_no_destroys(plan):
    """No resources should be destroyed without explicit approval."""
    destroys = [
        change["address"]
        for change in plan["resource_changes"]
        if "delete" in change["change"]["actions"]
    ]
    if destroys:
        print(f"BLOCKED: Plan would destroy {len(destroys)} resources:")
        for r in destroys:
            print(f"  - {r}")
        return False
    return True

def check_no_replacements(plan):
    """Flag resources that will be replaced (destroy + create)."""
    replacements = [
        change["address"]
        for change in plan["resource_changes"]
        if change["change"]["actions"] == ["delete", "create"]
        or change["change"]["actions"] == ["create", "delete"]
    ]
    if replacements:
        print(f"WARNING: Plan will replace {len(replacements)} resources:")
        for r in replacements:
            print(f"  - {r}")
        return False
    return True

def check_no_public_s3(plan):
    """S3 buckets must never have public ACLs."""
    for change in plan["resource_changes"]:
        if change["type"] == "aws_s3_bucket":
            after = change["change"].get("after", {})
            acl = after.get("acl", "private")
            if acl != "private":
                print(f"BLOCKED: {change['address']} has ACL '{acl}' (must be 'private')")
                return False
    return True

if __name__ == "__main__":
    plan = load_plan()
    checks = [
        check_no_destroys(plan),
        check_no_replacements(plan),
        check_no_public_s3(plan),
    ]
    if not all(checks):
        sys.exit(1)
    print("All plan checks passed.")

Common Plan Assertions to Implement

Assertion Why It Matters Risk Level
No resource destroys Prevents accidental data loss Critical
No resource replacements on databases RDS replacement = downtime + data risk Critical
No public security group ingress Prevents open network exposure Critical
All S3 buckets encrypted Compliance requirement High
No oversized instances in non-prod Cost control Medium
All resources have required tags Governance and cost allocation Medium
No changes to IAM policies without review Security posture High