When a stack is destroyed with RETAIN policies, the CloudFormation resources are removed from the stack but the underlying AWS resources — S3 buckets, EFS filesystems, Secrets Manager secrets, ECR repositories — persist. The awsplace project treats cleanup of these retained resources as a separate, manual, approval-gated operation. The staging runbook (docs/raftdb/staging-runbook.md) defines the authoritative procedure.
The separation between stack destruction and data destruction exists for three reasons:
The staging runbook summarizes this concisely:
Cleanup is a separate destructive operation requiring written data-destruction approval and a retention review. Match the recorded filesystem and bucket IDs; never use account-wide name matching. This runbook deliberately provides no blanket cleanup command.
Source: docs/raftdb/staging-runbook.md — §Retained data cleanup
After destroying RaftDbStagingStack, the following resources survive and must be cleaned up manually:
| Resource | Retention Behavior | Cleanup Complexity |
|---|---|---|
| RaftDB snapshot bucket (S3, versioned, KMS-managed encryption) | All object versions and delete markers persist | High — must empty every version and delete marker before bucket deletion |
| RaftDB EFS filesystem (encrypted) | Filesystem survives; mount targets may linger | Medium — must remove access points and mount targets before filesystem deletion |
The staging snapshot bucket is versioned with KMS_MANAGED encryption. CloudFormation refuses to delete a non-empty bucket, and with versioning enabled, “emptying” requires deleting every object version and every delete marker.
# 1. Record the bucket name before destroying the stack
BUCKET=$(jq -r '.RaftDbStagingStack.RaftDbSnapshotBucketName' \
cdk.out/raftdb-staging-outputs.json)
# 2. Verify the bucket identity — never match by name pattern
aws s3api list-object-versions --bucket "$BUCKET" --max-items 1
# 3. Empty all object versions (this is the destructive step)
aws s3api delete-objects --bucket "$BUCKET" \
--delete "$(aws s3api list-object-versions \
--bucket "$BUCKET" \
--query '{Objects: Versions[].{Key:Key,VersionId:VersionId}}' \
--output json)"
# 4. Empty all delete markers
aws s3api delete-objects --bucket "$BUCKET" \
--delete "$(aws s3api list-object-versions \
--bucket "$BUCKET" \
--query '{Objects: DeleteMarkers[].{Key:Key,VersionId:VersionId}}' \
--output json)"
# 5. Delete the bucket once empty
aws s3api delete-bucket --bucket "$BUCKET"
The lifecycle rule configured in raftdb.ts (35-day noncurrent version expiration) provides automatic eventual cleanup for old versions, but the complete immediate cleanup above is the only way to fully remove the bucket on demand.
EFS cleanup requires a specific ordering: access points and mount targets must be removed before the filesystem itself can be deleted.
# 1. Record the filesystem ID before destroying the stack
FILESYSTEM=$(jq -r '.RaftDbStagingStack.RaftDbFileSystemId' \
cdk.out/raftdb-staging-outputs.json)
# 2. List and delete all access points
aws efs describe-access-points --file-system-id "$FILESYSTEM" \
--query 'AccessPoints[].AccessPointId' --output text | while read ap_id; do
if [ -n "$ap_id" ]; then
aws efs delete-access-point --access-point-id "$ap_id"
fi
done
# 3. List and delete all mount targets
aws efs describe-mount-targets --file-system-id "$FILESYSTEM" \
--query 'MountTargets[].MountTargetId' --output text | while read mt_id; do
if [ -n "$mt_id" ]; then
aws efs delete-mount-target --mount-target-id "$mt_id"
fi
done
# 4. Wait for mount targets to finish deleting
aws efs describe-mount-targets --file-system-id "$FILESYSTEM" \
--query 'length(MountTargets)' | while read count; do
if [ "$count" -eq 0 ]; then break; fi
sleep 10
done
# 5. Delete the filesystem
aws efs delete-file-system --file-system-id "$FILESYSTEM"
Mount targets exist in each Availability Zone where the filesystem was mounted. They must all be in the deleted state before the filesystem can be removed.
Production retained resources (ECR repository, Secrets Manager secret, imported S3 buckets) are typically not cleaned up. They are designed to persist indefinitely:
| Resource | Cleanup Requirement | Typical Action |
|---|---|---|
| ECR Repository (awsplace-ecs) | Only if the project is fully decommissioned | Delete after confirming no active deployments depend on its images |
| Secrets Manager secret | Only if the application is fully decommissioned | Schedule deletion with a recovery window (default 30 days) |
| Imported S3 buckets (canvas, exports) | Never via CDK | These are imported (not created) by the stack; they must be managed independently |
Because imported S3 buckets are created outside CDK (via s3.Bucket.fromBucketName in storage.ts), CDK has no authority to delete them — they survive stack destruction automatically.
If the entire project is decommissioned, the ECR repository requires manual deletion:
# 1. Verify no active ECS tasks reference images in this repository
aws ecs list-tasks --cluster <cluster-name> --desired-status RUNNING
# 2. Delete all images (required before repository deletion)
aws ecr batch-delete-image \
--repository-name awsplace-ecs \
--image-ids "$(aws ecr list-images \
--repository-name awsplace-ecs \
--query 'imageIds[*]' --output json)"
# 3. Delete the repository
aws ecr delete-repository --repository-name awsplace-ecs --force
The –force flag on delete-repository is required because the repository may still contain images even after batch-delete-image. The lifecycle rule (max 10 images) keeps the image count manageable for this operation.
The staging runbook mandates documentation of every cleanup operation:
This approach aligns with the capacity runbook’s requirement that the service owner must explicitly approve resource retirement in writing.