CloudFormation Template Comparison Skill
Quick Start
# 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)
Expected Workflow
Step 1: Preparation — verify prerequisites
# 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
Step 2: Retrieval
# 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"
Step 3: Hierarchical Analysis
# 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)
Step 4: Risk Assessment
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 |
Step 5: Save Artifacts
# 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 -->
EOF
Decision Framework
When to Use This Skill
Use CloudFormation template comparison when:
- Pre-deployment validation: Verify CDK changes match expectations before deploying to prod
- Drift detection: Investigate whether console changes have diverged from IaC
- Security audits: Check for unauthorized IAM policy modifications or CDK Nag suppression changes
- Deployment troubleshooting: Understand why
cdk deploy is failing or showing unexpected diffs
- Change review: Provide stakeholders with concrete before/after comparison for approval
When NOT to Use This Skill
Skip template comparison when:
- Initial stack deployment: No deployed template exists yet — synthesize and deploy directly
- Cross-account comparisons: Different account IDs/ARNs make diffs noisy and unreliable
- Frequently changing resources: Dynamic autoscaling groups, ephemeral Lambdas — accept constant drift
- CloudFormation managed entirely outside CDK: If stack wasn't created via CDK, comparison won't map correctly
Risk Assessment Strategy
Always categorize diffs by risk level before approving:
- Auto-approve (green): Environmental tags, GitRef, timestamps — expected variance
- Review (yellow): Config changes (alarms, schedules) — verify intent, then approve
- Block (red): IAM policies, CDK Nag suppressions, resource deletions — require explicit sign-off
Anti-Patterns
NEVER compare templates without verifying both sources are valid JSON first
- WHY: invalid JSON from AWS CLI or CDK synth causes cryptic
jq errors that waste time debugging.
- BAD:
jq '.Resources' deployed.json → parse error: Expected separator between values at line 1, column 3.
- GOOD:
jq '.' deployed.json >/dev/null && echo "valid" || echo "INVALID" before any comparison.
NEVER rely on line-by-line diff for large templates
- WHY: 5000+ line diffs are unreadable and hide critical changes in noise.
- BAD:
diff deployed.json local.json → terminal flooded with irrelevant formatting differences.
- GOOD: hierarchical comparison (Step 3) — resource counts, added/removed IDs, then targeted deep diffs per resource.
NEVER approve deployments with unexplained IAM policy changes
- WHY: unauthorized privilege escalation or resource exposure can occur through subtle IAM modifications.
- BAD:
diff shows IAM role trust policy changed → "looks fine, deploying" → security breach.
- GOOD: extract IAM diff specifically (
jq filter for AWS::IAM::*), document justification, get InfoSec approval before deploy.
NEVER skip saving comparison artifacts before deployment
- WHY: if deployment goes wrong, you lose the evidence of what changed and can't rollback confidently.
- BAD: run comparison in terminal, approve deploy, stack fails → no record of what was attempted.
- GOOD: timestamped directory with deployed.json, local.json, diff report — audit trail for incident investigation.
Error Recovery
| 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 |
Required Tools
aws CLI — configured with appropriate profile
jq — JSON query and transformation
make — CDK synthesis via make synth
bash — shell scripting
diff / comm — comparison utilities
Common Scenarios
- Clean deployment: identical resource counts, only expected environmental differences → approve
- Drift detected: deployed threshold differs from local → revert console change or update CDK
- New CDK Nag suppression: requires documented justification and InfoSec approval
- Resource removal: block deployment, data-loss risk — review with stakeholders first
References
- Automated script:
scripts/compare-cfn-templates.sh
- Real-world examples:
references/compare-cfn-templates.md
- CI/CD integration:
.gitlab-ci.yml validate-template stage
1---2name: cfn-template-compare3description: 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'.4---56# CloudFormation Template Comparison Skill78## Quick Start910```bash11# 1. Retrieve the deployed template12aws cloudformation get-template \13 --stack-name <stack-name> \14 --region <region> \15 --profile <profile> \16 --query TemplateBody \17 --output json > deployed.json1819# 2. Synthesize the local CDK template20make synth21cp cdk.out/<stack-name>.template.json local.json2223# 3. Compare structure24jq 'keys' deployed.json25jq 'keys' local.json2627# 4. Compare resource counts28jq '.Resources | length' deployed.json29jq '.Resources | length' local.json3031# 5. Find added/removed resource IDs32diff <(jq -r '.Resources | keys[]' deployed.json | sort) \33 <(jq -r '.Resources | keys[]' local.json | sort)3435# 6. Deep diff of a specific resource36diff <(jq '.Resources.<ResourceId>' deployed.json) \37 <(jq '.Resources.<ResourceId>' local.json)38```3940## Expected Workflow4142### Step 1: Preparation — verify prerequisites4344```bash45# Check AWS credentials46aws sts get-caller-identity --profile <profile>47# → If this fails: verify AWS_PROFILE or --profile value before proceeding4849# Confirm stack exists50aws cloudformation describe-stacks \51 --stack-name <stack-name> --region <region> --profile <profile> \52 --query 'Stacks[0].StackStatus'53# → If StackNotFoundException: check stack name and region5455# Confirm CDK project synthesises cleanly56make synth57# → If synth fails: fix missing env vars in env-local.mk / env.mk before proceeding58```5960### Step 2: Retrieval6162```bash63# Deployed template64aws cloudformation get-template \65 --stack-name <stack-name> --region <region> --profile <profile> \66 --query TemplateBody --output json > deployed.json67# → Validate: jq '.' deployed.json >/dev/null || echo "ERROR: invalid JSON"6869# Local template70cp cdk.out/<stack-name>.template.json local.json71# → Validate: jq '.' local.json >/dev/null || echo "ERROR: invalid JSON"72```7374### Step 3: Hierarchical Analysis7576```bash77# 1. Structure (top-level keys)78diff <(jq 'keys' deployed.json) <(jq 'keys' local.json)7980# 2. Resource count81echo "Deployed: $(jq '.Resources | length' deployed.json)"82echo "Local: $(jq '.Resources | length' local.json)"8384# 3. Added / removed resources85comm -3 \86 <(jq -r '.Resources | keys[]' deployed.json | sort) \87 <(jq -r '.Resources | keys[]' local.json | sort)8889# 4. Security — CDK Nag suppressions90diff \91 <(jq '[.Resources[].Metadata."cdk_nag" // empty]' deployed.json) \92 <(jq '[.Resources[].Metadata."cdk_nag" // empty]' local.json)9394# 5. IAM roles and policies95diff \96 <(jq '[.Resources | to_entries[] | select(.value.Type | startswith("AWS::IAM"))]' deployed.json) \97 <(jq '[.Resources | to_entries[] | select(.value.Type | startswith("AWS::IAM"))]' local.json)98```99100### Step 4: Risk Assessment101102Categorise each difference before reporting:103104| Category | Examples | Action |105|---|---|---|106| **Expected** | Environmental tags, GitRef, stack-name in resource IDs | Auto-approve |107| **Low risk** | Display names, cosmetic metadata | Note and approve |108| **Medium risk** | Alarm thresholds, EventBridge schedules, Lambda config | Review and approve |109| **High risk** | IAM policies, encryption settings | Require explicit sign-off |110| **Critical** | CDK Nag suppressions, public access flags, resource removal | Block — get InfoSec/stakeholder approval |111112### Step 5: Save Artifacts113114```bash115# Timestamped directory for audit trail116BRANCH=$(git rev-parse --abbrev-ref HEAD)117DIR="cfn-compare-results/$(date +%Y-%m-%d-%H%M%S)_deployed-main_local-${BRANCH}"118mkdir -p "$DIR"119120cp deployed.json "$DIR/"121cp local.json "$DIR/"122jq -r '.Resources | keys[]' deployed.json | sort > "$DIR/deployed-resources.txt"123jq -r '.Resources | keys[]' local.json | sort > "$DIR/local-resources.txt"124125# Write report126cat > "$DIR/comparison-report.md" <<EOF127# CloudFormation Template Comparison128129## Summary130- Deployed: <stack-name> ($(jq '.Resources|length' deployed.json) resources)131- Local: <stack-name> ($(jq '.Resources|length' local.json) resources)132- Status: ✅ Safe to deploy | ⚠️ Review required | ❌ Critical issues133134## Differences135<!-- Populate from Step 3 output -->136137## Recommendations138<!-- List required actions -->139140## Deployment Decision141<!-- Approve | Reject | Conditional — reasoning -->142EOF143```144145## Decision Framework146147### When to Use This Skill148149Use CloudFormation template comparison when:150- **Pre-deployment validation**: Verify CDK changes match expectations before deploying to prod151- **Drift detection**: Investigate whether console changes have diverged from IaC152- **Security audits**: Check for unauthorized IAM policy modifications or CDK Nag suppression changes153- **Deployment troubleshooting**: Understand why `cdk deploy` is failing or showing unexpected diffs154- **Change review**: Provide stakeholders with concrete before/after comparison for approval155156### When NOT to Use This Skill157158Skip template comparison when:159- **Initial stack deployment**: No deployed template exists yet — synthesize and deploy directly160- **Cross-account comparisons**: Different account IDs/ARNs make diffs noisy and unreliable161- **Frequently changing resources**: Dynamic autoscaling groups, ephemeral Lambdas — accept constant drift162- **CloudFormation managed entirely outside CDK**: If stack wasn't created via CDK, comparison won't map correctly163164### Risk Assessment Strategy165166Always categorize diffs by risk level before approving:167168- **Auto-approve (green)**: Environmental tags, GitRef, timestamps — expected variance169- **Review (yellow)**: Config changes (alarms, schedules) — verify intent, then approve170- **Block (red)**: IAM policies, CDK Nag suppressions, resource deletions — require explicit sign-off171172## Anti-Patterns173174### NEVER compare templates without verifying both sources are valid JSON first175176- **WHY**: invalid JSON from AWS CLI or CDK synth causes cryptic `jq` errors that waste time debugging.177- **BAD**: `jq '.Resources' deployed.json` → `parse error: Expected separator between values at line 1, column 3`.178- **GOOD**: `jq '.' deployed.json >/dev/null && echo "valid" || echo "INVALID"` before any comparison.179180### NEVER rely on line-by-line diff for large templates181182- **WHY**: 5000+ line diffs are unreadable and hide critical changes in noise.183- **BAD**: `diff deployed.json local.json` → terminal flooded with irrelevant formatting differences.184- **GOOD**: hierarchical comparison (Step 3) — resource counts, added/removed IDs, then targeted deep diffs per resource.185186### NEVER approve deployments with unexplained IAM policy changes187188- **WHY**: unauthorized privilege escalation or resource exposure can occur through subtle IAM modifications.189- **BAD**: `diff` shows IAM role trust policy changed → "looks fine, deploying" → security breach.190- **GOOD**: extract IAM diff specifically (`jq` filter for `AWS::IAM::*`), document justification, get InfoSec approval before deploy.191192### NEVER skip saving comparison artifacts before deployment193194- **WHY**: if deployment goes wrong, you lose the evidence of what changed and can't rollback confidently.195- **BAD**: run comparison in terminal, approve deploy, stack fails → no record of what was attempted.196- **GOOD**: timestamped directory with deployed.json, local.json, diff report — audit trail for incident investigation.197198## Error Recovery199200| Error | Cause | Fix |201|---|---|---|202| `Stack not found` | Wrong name/region | Verify `--stack-name`, `--region`, `--profile` |203| `CDK synth failed` | Missing env var | Check `env-local.mk` and `env.mk` |204| `jq: parse error` | Invalid JSON from CLI | Use `--output json` and `--query TemplateBody` |205| `Diff > 5000 lines` | Template too large | Switch to hierarchical comparison (Step 3) instead of line diff |206207## Required Tools208209- `aws` CLI — configured with appropriate profile210- `jq` — JSON query and transformation211- `make` — CDK synthesis via `make synth`212- `bash` — shell scripting213- `diff` / `comm` — comparison utilities214215## Common Scenarios216217- **Clean deployment**: identical resource counts, only expected environmental differences → approve218- **Drift detected**: deployed threshold differs from local → revert console change or update CDK219- **New CDK Nag suppression**: requires documented justification and InfoSec approval220- **Resource removal**: block deployment, data-loss risk — review with stakeholders first221222## References223224- Automated script: `scripts/compare-cfn-templates.sh`225- Real-world examples: `references/compare-cfn-templates.md`226- CI/CD integration: `.gitlab-ci.yml` `validate-template` stage