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:
pattern3-pipeline Stack will fail and get stuck in DELETE_FAILED status if you haven’t emptied the bucket.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.
| Order | Stack | Notes |
|---|---|---|
| 1 | pattern3-automate | Can be deleted immediately |
| 2 | (not a Stack) | Delete AMIs and EBS Snapshots generated by Image Builder |
| 3 | pattern3-pipeline | Must empty the S3 bucket first |
| 4 | pattern3-app | Takes about 5 to 10 minutes |
| 5 | pattern3-base | Deletes NAT Gateway, Elastic IP, VPC |
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
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.
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
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.
Image Builder has two distinct types of ARNs, which are easily confused.
| Type | ARN format | Returned by |
|---|---|---|
| Image version | .../image/<recipe-name>/1.0.0 | list-images |
| Image build version | .../image/<recipe-name>/1.0.0/2 | list-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
}
}
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
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.
| Resource | Deletion command |
|---|---|
| Image pipeline | aws imagebuilder delete-image-pipeline --image-pipeline-arn <ARN> |
| Image recipe | aws imagebuilder delete-image-recipe --image-recipe-arn <ARN> |
| Component | aws imagebuilder delete-component --component-build-version-arn <ARN> |
| Infrastructure configuration | aws imagebuilder delete-infrastructure-configuration --infrastructure-configuration-arn <ARN> |
| SSM Automation document | aws ssm delete-document --name pattern3-automate-CreateImage |
| S3 bucket | aws s3 rb s3://<BUCKET_NAME> --force |
| Security group | aws ec2 delete-security-group --group-id <SG_ID> |
| IAM role | Delete 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
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.
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.
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.