#!/usr/bin/env bash # # cfn-stack-decommission.sh — safely retire a CloudFormation/CDK stack. # # Reports first (default), acts only with --execute. The value is the pre-flight: # it predicts what will ORPHAN (DeletionPolicy: Retain resources survive a stack # delete) and what will BLOCK the delete (consumed exports, non-empty buckets), # so you don't discover surviving tables/buckets after the fact. # # Built from the Day 4 LedgerFlow decommission, where 4 of 5 DynamoDB tables + # 2 of 3 S3 buckets were RemovalPolicy.RETAIN and orphaned. See feedback memory # `feedback_cfn_decommission_and_remediation`. # # Usage: # scripts/cfn-stack-decommission.sh [--profile NAME] [--execute] STACK # # (no --execute) REPORT only: termination protection, consumed exports, # Retain resources (orphans-to-be), in-stack S3 buckets. # --execute Disable termination protection, empty Delete-policy buckets, # delete the stack, wait, then delete the Retain orphans. # set -euo pipefail PROFILE_ARG=(); EXECUTE=0; STACK="" while [[ $# -gt 0 ]]; do case "$1" in --profile) PROFILE_ARG=(--profile "$2"); shift 2 ;; --execute) EXECUTE=1; shift ;; -h|--help) sed -n '2,22p' "$0"; exit 0 ;; -*) echo "unknown flag: $1" >&2; exit 2 ;; *) STACK="$1"; shift ;; esac done [[ -z "$STACK" ]] && { echo "usage: $0 [--profile NAME] [--execute] STACK" >&2; exit 2; } aws_() { aws "${PROFILE_ARG[@]}" "$@"; } R="us-east-1" echo "== stack: $STACK ==" aws_ cloudformation describe-stacks --stack-name "$STACK" --region "$R" \ --query 'Stacks[0].{Status:StackStatus,TermProt:EnableTerminationProtection}' --output table echo "-- consumed exports (any import BLOCKS the delete) --" BLOCKED=0 for e in $(aws_ cloudformation list-exports --region "$R" \ --query "Exports[?ExportingStackId && contains(ExportingStackId,':stack/$STACK/')].Name" --output text 2>/dev/null); do imp=$(aws_ cloudformation list-imports --export-name "$e" --region "$R" --query 'Imports' --output text 2>/dev/null || true) if [[ -n "$imp" && "$imp" != "None" ]]; then echo " BLOCK: export $e imported by: $imp"; BLOCKED=1; fi done [[ $BLOCKED -eq 0 ]] && echo " none" echo "-- DeletionPolicy: Retain resources (these ORPHAN, survive the delete) --" TMP="$(aws_ cloudformation get-template --stack-name "$STACK" --region "$R" --query TemplateBody --output json)" echo "$TMP" | python3 -c ' import json,sys res=json.load(sys.stdin).get("Resources",{}) orphans=[(r.get("Type"),lid,r.get("Properties",{}).get("TableName") or r.get("Properties",{}).get("BucketName") or "") for lid,r in res.items() if r.get("DeletionPolicy")=="Retain"] [print(f" {t:<28} {lid} {name}") for t,lid,name in sorted(orphans)] or print(" none") ' echo "-- in-stack S3 buckets (non-empty Delete-policy buckets block; check auto-delete) --" for b in $(aws_ cloudformation list-stack-resources --stack-name "$STACK" --region "$R" \ --query "StackResourceSummaries[?ResourceType=='AWS::S3::Bucket'].PhysicalResourceId" --output text 2>/dev/null); do n=$(aws_ s3api list-objects-v2 --bucket "$b" --max-items 1 --query 'KeyCount' --output text 2>/dev/null || echo "?") v=$(aws_ s3api get-bucket-versioning --bucket "$b" --query 'Status' --output text 2>/dev/null || echo "-") echo " $b objects~=$n versioning=$v" done if [[ $EXECUTE -eq 0 ]]; then echo; echo "REPORT ONLY. Re-run with --execute to delete (after reviewing the orphans + blocks above)." exit 0 fi [[ $BLOCKED -eq 1 ]] && { echo "ABORT: a consumed export blocks the delete (see above)." >&2; exit 1; } read -r -p "EXECUTE decommission of '$STACK'? [y/N] " ans; [[ "$ans" =~ ^[Yy]$ ]] || { echo "aborted"; exit 0; } aws_ cloudformation update-termination-protection --stack-name "$STACK" --no-enable-termination-protection --region "$R" >/dev/null 2>&1 || true echo "deleting stack..." aws_ cloudformation delete-stack --stack-name "$STACK" --region "$R" aws_ cloudformation wait stack-delete-complete --stack-name "$STACK" --region "$R" echo "stack deleted. Review the Retain orphans above and remove them with delete-table / delete-bucket" echo "(versioned buckets: purge all versions + delete-markers first — see the iam-user-delete sibling pattern)."