How to Identify and Clean Up Orphaned EBS Snapshots from Deleted AMIs in AWS


4 views

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:

  1. Always create a backup list before deletion: aws ec2 describe-snapshots --snapshot-ids $ORPHANED_SNAPSHOTS > snapshot_backup.txt
  2. Consider implementing a deletion dry-run first
  3. 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:

  1. Navigate to EC2 → Snapshots
  2. Sort by "Description" column
  3. 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"]
    }
  }'