During AWS resource cleanup, many engineers discover that deleting AMIs doesn't automatically remove their associated EBS snapshots. This creates "orphaned" snapshots that continue to incur storage costs while serving no purpose. Unlike AMI deregistration which is instantaneous, EBS snapshot deletion requires explicit action.
AWS intentionally maintains this separation because:
- Snapshots might be shared across multiple AMIs
- Some organizations require snapshot retention policies
- Manual deletion provides an extra layer of protection against accidental data loss
Here's a comprehensive method using AWS CLI (requires appropriate IAM permissions):
# First, get all EBS snapshots owned by your account
ALL_SNAPSHOTS=$(aws ec2 describe-snapshots --owner-ids self --query 'Snapshots[*].SnapshotId' --output text)
# Then get snapshots currently associated with existing AMIs
USED_SNAPSHOTS=$(aws ec2 describe-images --owners self --query 'Images[*].BlockDeviceMappings[*].Ebs.SnapshotId' --output text | sort | uniq)
# Compare to find orphans
ORPHANED_SNAPSHOTS=$(comm -23 <(echo "$ALL_SNAPSHOTS" | sort) <(echo "$USED_SNAPSHOTS" | sort))
echo "Found $(echo "$ORPHANED_SNAPSHOTS" | wc -w) orphaned snapshots:"
echo "$ORPHANED_SNAPSHOTS"
For more complex scenarios, Python's Boto3 library offers better flexibility:
import boto3
def find_orphaned_snapshots():
ec2 = boto3.client('ec2')
# Get all snapshots
all_snapshots = ec2.describe_snapshots(OwnerIds=['self'])['Snapshots']
# Get snapshots in use by AMIs
used_snapshots = set()
images = ec2.describe_images(Owners=['self'])['Images']
for image in images:
for bdm in image.get('BlockDeviceMappings', []):
if 'Ebs' in bdm and 'SnapshotId' in bdm['Ebs']:
used_snapshots.add(bdm['Ebs']['SnapshotId'])
# Find orphans
orphaned = ▼显示 not in used_snapshots]
return orphaned
if __name__ == "__main__":
orphans = find_orphaned_snapshots()
print(f"Found {len(orphans)} orphaned snapshots")
for snap in orphans:
print(f"ID: {snap['SnapshotId']}, Size: {snap['VolumeSize']}GB, Created: {snap['StartTime']}")
When dealing with hundreds of snapshots:
- Always create a backup list before deletion:
aws ec2 describe-snapshots --snapshot-ids $ORPHANED_SNAPSHOTS > snapshot_backup.txt
- Consider implementing a deletion dry-run first
- Use AWS Cost Explorer to verify storage cost reductions post-cleanup
Create an AWS Lambda function triggered by CloudWatch Events on AMI deletion:
import boto3
def lambda_handler(event, context):
ec2 = boto3.client('ec2')
# Extract deleted AMI ID from CloudTrail event
ami_id = event['detail']['requestParameters']['imageId']
# Get AMI info before deletion to find associated snapshots
try:
ami_info = ec2.describe_images(ImageIds=[ami_id])['Images'][0]
snapshots = [bdm['Ebs']['SnapshotId'] for bdm in ami_info['BlockDeviceMappings'] if 'Ebs' in bdm]
# Delete the snapshots (consider adding confirmation logic)
for snap_id in snapshots:
ec2.delete_snapshot(SnapshotId=snap_id)
print(f"Deleted snapshot: {snap_id}")
except Exception as e:
print(f"Error processing AMI {ami_id}: {str(e)}")
When managing AMIs in AWS, many engineers encounter a common issue: EBS snapshots remain after deleting their parent AMIs. These orphaned snapshots continue consuming storage and incur costs, while being difficult to identify through normal AWS interfaces.
The root cause lies in AWS's reference counting system. When an AMI references an EBS snapshot:
- The snapshot is preserved as long as the AMI exists
- Deleting the AMI removes the reference but doesn't automatically delete the snapshot
- The snapshot becomes untraceable through normal AMI queries
While the AWS Console doesn't directly show AMI-snapshot relationships for deleted AMIs, you can:
- Navigate to EC2 → Snapshots
- Sort by "Description" column
- Look for snapshots with descriptions containing "Created by CreateImage"
This manual method has significant limitations in accuracy and scalability.
For reliable identification of orphaned snapshots, use this AWS CLI pipeline:
#!/bin/bash
# Get all active AMI snapshot references
ACTIVE_SNAPSHOTS=$(aws ec2 describe-images --owners self \
--query 'Images[*].BlockDeviceMappings[*].Ebs.SnapshotId' \
--output text)
# Get all snapshots in account
ALL_SNAPSHOTS=$(aws ec2 describe-snapshots --owner-ids self \
--query 'Snapshots[*].SnapshotId' \
--output text)
# Find difference between sets
for snapshot in $ALL_SNAPSHOTS; do
if ! grep -q $snapshot <<< "$ACTIVE_SNAPSHOTS"; then
# Optional: Check if snapshot was created by AMI process
description=$(aws ec2 describe-snapshots \
--snapshot-ids $snapshot \
--query 'Snapshots[0].Description' \
--output text)
if [[ $description == *"Created by CreateImage"* ]]; then
echo "Orphaned AMI snapshot: $snapshot ($description)"
# Uncomment to delete:
# aws ec2 delete-snapshot --snapshot-id $snapshot
fi
fi
done
For better lifecycle management, implement this tagging strategy when creating AMIs:
aws ec2 create-image \
--instance-id i-1234567890abcdef0 \
--name "MyServer-2023" \
--description "AMI for MyServer" \
--tag-specifications 'ResourceType=snapshot,Tags=[{Key=SourceAMI,Value=ami-12345}]'
Then query orphaned snapshots by tag:
aws ec2 describe-snapshots \
--filters "Name=tag:SourceAMI,Values=ami-12345" \
--query "length(Snapshots)" \
--output text
For regular maintenance, deploy this Python Lambda function (set to run weekly):
import boto3
def lambda_handler(event, context):
ec2 = boto3.client('ec2')
# Get active AMI snapshots
amis = ec2.describe_images(Owners=['self'])
active_snaps = set()
for ami in amis['Images']:
for bdm in ami.get('BlockDeviceMappings', []):
if 'Ebs' in bdm and 'SnapshotId' in bdm['Ebs']:
active_snaps.add(bdm['Ebs']['SnapshotId'])
# Find and delete orphans
snapshots = ec2.describe_snapshots(OwnerIds=['self'])
for snap in snapshots['Snapshots']:
if (snap['SnapshotId'] not in active_snaps and
"Created by CreateImage" in snap.get('Description', '')):
print(f"Deleting orphaned snapshot: {snap['SnapshotId']}")
ec2.delete_snapshot(SnapshotId=snap['SnapshotId'])
return {
'statusCode': 200,
'body': f"Processed {len(snapshots['Snapshots'])} snapshots"
}
To track potential savings from cleaning orphaned snapshots:
aws ce get-cost-and-usage \
--time-period Start=2023-01-01,End=2023-01-31 \
--granularity MONTHLY \
--metrics "UnblendedCost" \
--filter '{
"Dimensions": {
"Key": "USAGE_TYPE_GROUP",
"Values": ["EC2: EBS - Snapshots"]
}
}'