Resource Cleanup

After completing the lab, we proceed to clean up all the created resources.

Please read this section carefully before starting. Deleting the CloudFormation Stacks is not enough. There are two groups of resources that are left behind and continue to incur charges:

  • AMIs and EBS Snapshots created by EC2 Image Builder. CloudFormation does not manage them, and according to the DeleteImage documentation, deleting the image resource in Image Builder does not delete its associated AMI and snapshot.
  • Contents inside the S3 logging bucket. CloudFormation cannot delete a bucket that still contains data, so the pattern3-pipeline Stack will fail and get stuck in DELETE_FAILED status if you haven’t emptied the bucket.

Order of Execution

The Stacks depend on each other via Export and Fn::ImportValue, so they must be deleted in the exact reverse order of creation. If you try to delete pattern3-base first, CloudFormation will reject the request because its Exported values are still in use by other Stacks.

OrderStackNotes
1pattern3-automateCan be deleted immediately
2(not a Stack)Delete AMIs and EBS Snapshots generated by Image Builder
3pattern3-pipelineMust empty the S3 bucket first
4pattern3-appTakes about 5 to 10 minutes
5pattern3-baseDeletes NAT Gateway, Elastic IP, VPC

Step 1: Delete Automation Stack

From the CloudFormation Console, delete the pattern3-automate Stack. Or use the CLI:

aws cloudformation delete-stack --stack-name pattern3-automate --region ap-southeast-2

aws cloudformation wait stack-delete-complete --stack-name pattern3-automate --region ap-southeast-2

Step 2: Delete AMIs and EBS Snapshots

This is the most critical step regarding costs. Every time the pipeline runs successfully, it generates an AMI alongside an EBS Snapshot, and the snapshot will continue to incur storage charges indefinitely until deleted.

  1. List the AMIs you own to identify which ones to delete:

    aws ec2 describe-images --owners self --region ap-southeast-2 --query "Images[].{Id:ImageId,Name:Name,Created:CreationDate}" --output table
    
  2. For each AMI, gather the associated snapshot IDs, then deregister the AMI:

AMI_ID=ami-xxxxxxxxxxxxxxxxx

SNAPSHOTS=$(aws ec2 describe-images --image-ids "$AMI_ID" --region ap-southeast-2 --query "Images[0].BlockDeviceMappings[].Ebs.SnapshotId" --output text)

aws ec2 deregister-image --image-id "$AMI_ID" --region ap-southeast-2

for SNAP in $SNAPSHOTS; do
  aws ec2 delete-snapshot --snapshot-id "$SNAP" --region ap-southeast-2
done
$AMI_ID = 'ami-xxxxxxxxxxxxxxxxx'

$SNAPSHOTS = (aws ec2 describe-images --image-ids $AMI_ID --region ap-southeast-2 --query "Images[0].BlockDeviceMappings[].Ebs.SnapshotId" --output text) -split '\s+'

aws ec2 deregister-image --image-id $AMI_ID --region ap-southeast-2

foreach ($SNAP in $SNAPSHOTS) {
    if ($SNAP) { aws ec2 delete-snapshot --snapshot-id $SNAP --region ap-southeast-2 }
}

Always deregister the AMI first, then delete the snapshots. Doing the reverse will leave behind a broken AMI that cannot be launched.

Additionally, --owners self only returns AMIs you’ve created yourself, so the command above will not affect Amazon’s public AMIs. However, if this account also contains AMIs from other workloads, cross-check the Name column to ensure you only delete the AMIs generated by this lab.

  1. Delete the image resources in Image Builder to keep the list clean.

Image Builder has two distinct types of ARNs, which are easily confused.

TypeARN formatReturned by
Image version.../image/<recipe-name>/1.0.0list-images
Image build version.../image/<recipe-name>/1.0.0/2list-image-build-versions

1.0.0 is the version of the recipe, and the final number is the build sequence of that version. Running the pipeline 3 times will yield 1.0.0/1, 1.0.0/2, 1.0.0/3.

The delete-image command only accepts the image build version ARN, meaning the one with the build number at the end. If you pass an ARN obtained from list-images, you will encounter an error:

An error occurred (InvalidParameterValueException) when calling the DeleteImage operation:
The value supplied for parameter 'imageBuildVersionArn' is not valid.
The supplied Arn is not a valid Image Builder Image Build Version Arn.

Thus, this must be a two-step process: fetch the image version ARNs first, and then list the build versions for each.

# Step 1: get the list of image version ARNs
aws imagebuilder list-images --owner Self --region ap-southeast-2 --query "imageVersionList[].arn" --output text

# Step 2: for each image version, list the build versions then delete them all
for VER in $(aws imagebuilder list-images --owner Self --region ap-southeast-2 --query "imageVersionList[].arn" --output text); do
  for BUILD in $(aws imagebuilder list-image-build-versions --image-version-arn "$VER" --region ap-southeast-2 --query "imageSummaryList[].arn" --output text); do
    echo "Deleting $BUILD"
    aws imagebuilder delete-image --image-build-version-arn "$BUILD" --region ap-southeast-2
  done
done
# Step 1: get the list of image version ARNs
$VERSIONS = (aws imagebuilder list-images --owner Self --region ap-southeast-2 --query "imageVersionList[].arn" --output text) -split '\s+'

# Step 2: for each image version, list the build versions then delete them all
foreach ($VER in $VERSIONS) {
    if (-not $VER) { continue }
    $BUILDS = (aws imagebuilder list-image-build-versions --image-version-arn $VER --region ap-southeast-2 --query "imageSummaryList[].arn" --output text) -split '\s+'
    foreach ($BUILD in $BUILDS) {
        if (-not $BUILD) { continue }
        Write-Output "Deleting $BUILD"
        aws imagebuilder delete-image --image-build-version-arn $BUILD --region ap-southeast-2
    }
}

Step 3: Delete Pipeline Stack

  1. Fetch the S3 logging bucket name from the Stack’s Outputs, then empty the bucket. If you skip emptying it, the Stack deletion will fail and hang in DELETE_FAILED:
BUCKET=$(aws cloudformation describe-stacks --stack-name pattern3-pipeline --region ap-southeast-2 --query "Stacks[0].Outputs[?OutputKey=='Pattern3LoggingBucketName'].OutputValue" --output text)

echo "Logging bucket: $BUCKET"

aws s3 rm "s3://${BUCKET}" --recursive --region ap-southeast-2
$BUCKET = aws cloudformation describe-stacks --stack-name pattern3-pipeline --region ap-southeast-2 --query "Stacks[0].Outputs[?OutputKey=='Pattern3LoggingBucketName'].OutputValue" --output text

Write-Output "Logging bucket: $BUCKET"

aws s3 rm "s3://$BUCKET" --recursive --region ap-southeast-2
  1. Delete the Stack:
    aws cloudformation delete-stack --stack-name pattern3-pipeline --region ap-southeast-2
    
    aws cloudformation wait stack-delete-complete --stack-name pattern3-pipeline --region ap-southeast-2
    

If you performed sections 4 and 5 manually via the Console, delete those resources here, before moving to Step 5. CloudFormation does not manage them, so deleting the Stack won’t clean them up.

ResourceDeletion command
Image pipelineaws imagebuilder delete-image-pipeline --image-pipeline-arn <ARN>
Image recipeaws imagebuilder delete-image-recipe --image-recipe-arn <ARN>
Componentaws imagebuilder delete-component --component-build-version-arn <ARN>
Infrastructure configurationaws imagebuilder delete-infrastructure-configuration --infrastructure-configuration-arn <ARN>
SSM Automation documentaws ssm delete-document --name pattern3-automate-CreateImage
S3 bucketaws s3 rb s3://<BUCKET_NAME> --force
Security groupaws ec2 delete-security-group --group-id <SG_ID>
IAM roleDelete instance profile, detach policy, delete inline policy, then delete role

The security group is the most crucial resource to delete before Step 5. It belongs to the pattern3-base VPC, and AWS prevents the deletion of any VPC that still contains non-default security groups. Overlooking it will cause the pattern3-base Stack to get stuck in DELETE_FAILED.

Delete the IAM Role with the following four commands in exact order:

ROLE=pattern3-recipe-instance-role

aws iam remove-role-from-instance-profile --instance-profile-name $ROLE --role-name $ROLE
aws iam delete-instance-profile --instance-profile-name $ROLE
aws iam detach-role-policy --role-name $ROLE --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
aws iam detach-role-policy --role-name $ROLE --policy-arn arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilder
aws iam delete-role-policy --role-name $ROLE --policy-name pattern3-recipe-instance-policy
aws iam delete-role --role-name $ROLE

Step 4: Delete Application Stack

This step deletes the Application Load Balancer, Auto Scaling Group, Launch Template, EC2 instances, and the associated IAM Role.

aws cloudformation delete-stack --stack-name pattern3-app --region ap-southeast-2

aws cloudformation wait stack-delete-complete --stack-name pattern3-app --region ap-southeast-2

The process takes about 5 to 10 minutes because it must wait for the Auto Scaling Group to scale the instance count down to 0 and for the Load Balancer to deregister its targets.

Step 5: Delete Infrastructure Stack

The final step deletes the NAT Gateway, Elastic IP, Internet Gateway, Subnets, and the VPC.

aws cloudformation delete-stack --stack-name pattern3-base --region ap-southeast-2

aws cloudformation wait stack-delete-complete --stack-name pattern3-base --region ap-southeast-2

The NAT Gateway is the most expensive resource in this lab, billed by the hour even when no traffic flows through. Make absolutely sure the pattern3-base Stack reaches DELETE_COMPLETE status.

Verify Complete Cleanup

Run the following commands; all of them should return empty results:

# No Stacks from this lab remain
aws cloudformation describe-stacks --region ap-southeast-2 --query "Stacks[?starts_with(StackName, 'pattern3')].{Name:StackName,Status:StackStatus}" --output table

# No AMIs owned by you remain
aws ec2 describe-images --owners self --region ap-southeast-2 --query "Images[].ImageId" --output text

# No EBS Snapshots owned by you remain
aws ec2 describe-snapshots --owner-ids self --region ap-southeast-2 --query "Snapshots[].SnapshotId" --output text

# No active NAT Gateways remain
aws ec2 describe-nat-gateways --region ap-southeast-2 --filter "Name=state,Values=available,pending" --query "NatGateways[].NatGatewayId" --output text

# No unassociated Elastic IPs remain
aws ec2 describe-addresses --region ap-southeast-2 --query "Addresses[?AssociationId==null].PublicIp" --output text

After the cleanup, it is highly recommended to check AWS Cost Explorer or the Billing Dashboard after 24 hours to ensure no stray resources are still incurring charges.

Congratulations on completing the lab.