Compares deployed CloudFormation templates with locally synthesized CDK templates to detect drift, validate changes, and ensure consistency before deployment. Use when the user wants to compare CDK output with a deployed stack, check for infrastructure drift, run a pre-deployment validation, audit IAM or security changes, investigate a failing deployment, or perform a 'cdk diff'-style review. Triggered by phrases like 'compare templates', 'check for drift', 'cfn drift', 'stack comparison', 'infrastructure drift detection', 'safe to deploy', or 'what changed in my CDK stack'.
95
93%
Does it follow best practices?
Impact
100%
1.08xAverage score across 5 eval scenarios
Passed
No known issues
# 1. Retrieve the deployed template
aws cloudformation get-template \
--stack-name <stack-name> \
--region <region> \
--profile <profile> \
--query TemplateBody \
--output json > deployed.json
# 2. Synthesize the local CDK template
make synth
cp cdk.out/<stack-name>.template.json local.json
# 3. Compare structure
jq 'keys' deployed.json
jq 'keys' local.json
# 4. Compare resource counts
jq '.Resources | length' deployed.json
jq '.Resources | length' local.json
# 5. Find added/removed resource IDs
diff <(jq -r '.Resources | keys[]' deployed.json | sort) \
<(jq -r '.Resources | keys[]' local.json | sort)
# 6. Deep diff of a specific resource
diff <(jq '.Resources.<ResourceId>' deployed.json) \
<(jq '.Resources.<ResourceId>' local.json)# Check AWS credentials
aws sts get-caller-identity --profile <profile>
# → If this fails: verify AWS_PROFILE or --profile value before proceeding
# Confirm stack exists
aws cloudformation describe-stacks \
--stack-name <stack-name> --region <region> --profile <profile> \
--query 'Stacks[0].StackStatus'
# → If StackNotFoundException: check stack name and region
# Confirm CDK project synthesises cleanly
make synth
# → If synth fails: fix missing env vars in env-local.mk / env.mk before proceeding# Deployed template
aws cloudformation get-template \
--stack-name <stack-name> --region <region> --profile <profile> \
--query TemplateBody --output json > deployed.json
# → Validate: jq '.' deployed.json >/dev/null || echo "ERROR: invalid JSON"
# Local template
cp cdk.out/<stack-name>.template.json local.json
# → Validate: jq '.' local.json >/dev/null || echo "ERROR: invalid JSON"# 1. Structure (top-level keys)
diff <(jq 'keys' deployed.json) <(jq 'keys' local.json)
# 2. Resource count
echo "Deployed: $(jq '.Resources | length' deployed.json)"
echo "Local: $(jq '.Resources | length' local.json)"
# 3. Added / removed resources
comm -3 \
<(jq -r '.Resources | keys[]' deployed.json | sort) \
<(jq -r '.Resources | keys[]' local.json | sort)
# 4. Security — CDK Nag suppressions
diff \
<(jq '[.Resources[].Metadata."cdk_nag" // empty]' deployed.json) \
<(jq '[.Resources[].Metadata."cdk_nag" // empty]' local.json)
# 5. IAM roles and policies
diff \
<(jq '[.Resources | to_entries[] | select(.value.Type | startswith("AWS::IAM"))]' deployed.json) \
<(jq '[.Resources | to_entries[] | select(.value.Type | startswith("AWS::IAM"))]' local.json)Categorise each difference before reporting:
| Category | Examples | Action |
|---|---|---|
| Expected | Environmental tags, GitRef, stack-name in resource IDs | Auto-approve |
| Low risk | Display names, cosmetic metadata | Note and approve |
| Medium risk | Alarm thresholds, EventBridge schedules, Lambda config | Review and approve |
| High risk | IAM policies, encryption settings | Require explicit sign-off |
| Critical | CDK Nag suppressions, public access flags, resource removal | Block — get InfoSec/stakeholder approval |
# Timestamped directory for audit trail
BRANCH=$(git rev-parse --abbrev-ref HEAD)
DIR="cfn-compare-results/$(date +%Y-%m-%d-%H%M%S)_deployed-main_local-${BRANCH}"
mkdir -p "$DIR"
cp deployed.json "$DIR/"
cp local.json "$DIR/"
jq -r '.Resources | keys[]' deployed.json | sort > "$DIR/deployed-resources.txt"
jq -r '.Resources | keys[]' local.json | sort > "$DIR/local-resources.txt"
# Write report
cat > "$DIR/comparison-report.md" <<EOF
# CloudFormation Template Comparison
## Summary
- Deployed: <stack-name> ($(jq '.Resources|length' deployed.json) resources)
- Local: <stack-name> ($(jq '.Resources|length' local.json) resources)
- Status: ✅ Safe to deploy | ⚠️ Review required | ❌ Critical issues
## Differences
<!-- Populate from Step 3 output -->
## Recommendations
<!-- List required actions -->
## Deployment Decision
<!-- Approve | Reject | Conditional — reasoning -->
EOFUse CloudFormation template comparison when:
cdk deploy is failing or showing unexpected diffsSkip template comparison when:
Always categorize diffs by risk level before approving:
jq errors that waste time debugging.jq '.Resources' deployed.json → parse error: Expected separator between values at line 1, column 3.jq '.' deployed.json >/dev/null && echo "valid" || echo "INVALID" before any comparison.diff deployed.json local.json → terminal flooded with irrelevant formatting differences.diff shows IAM role trust policy changed → "looks fine, deploying" → security breach.jq filter for AWS::IAM::*), document justification, get InfoSec approval before deploy.| Error | Cause | Fix |
|---|---|---|
Stack not found | Wrong name/region | Verify --stack-name, --region, --profile |
CDK synth failed | Missing env var | Check env-local.mk and env.mk |
jq: parse error | Invalid JSON from CLI | Use --output json and --query TemplateBody |
Diff > 5000 lines | Template too large | Switch to hierarchical comparison (Step 3) instead of line diff |
aws CLI — configured with appropriate profilejq — JSON query and transformationmake — CDK synthesis via make synthbash — shell scriptingdiff / comm — comparison utilitiesscripts/compare-cfn-templates.shreferences/compare-cfn-templates.md.gitlab-ci.yml validate-template stage