Skip to content

Instantly share code, notes, and snippets.

@Eyjafjallajokull
Last active December 25, 2023 02:53
Show Gist options
  • Star 75 You must be signed in to star a gist
  • Fork 58 You must be signed in to fork a gist
  • Save Eyjafjallajokull/4e917414cfb191391f9e51f6a8c3e46a to your computer and use it in GitHub Desktop.
Save Eyjafjallajokull/4e917414cfb191391f9e51f6a8c3e46a to your computer and use it in GitHub Desktop.
AWS EBS - Find unused snapshots

This script can help you find and remove unused AWS snapshots and volumes.

There is hardcoded list of regions that it searches, adjust the value to suit your needs.

Use snapshot.py snapshot-report to generate report.csv containing information about all snapshots.

snapshot.py snapshot-cleanup lets you interactively delete snapshot if it finds it is referencing unexisting resources.

./snapshots.py --help
Usage: snapshots.py [OPTIONS] COMMAND [ARGS]...

  Helper commands for Snapshots management.

Options:
  --help  Show this message and exit.

Commands:
  snapshot-cleanup  Find and delete unreferenced snapshots.
  snapshot-delete   Delete single snapshot by id.
  snapshot-report   Find unreferenced snapshots.
  volume-cleanup    Find and delete unused volumes.
#!/usr/local/bin/python3
import csv
import re
from collections import OrderedDict
from pprint import pprint
import boto3
import click
from botocore.exceptions import ClientError
regions = ['us-west-1', 'eu-central-1']
ec2 = None
exists_icon = '✅'
not_exists_icon = '❌'
@click.group()
def cli():
'''
Helper commands for Snapshots management.
'''
pass
@cli.command()
def snapshot_report():
'''
Find unreferenced snapshots.
'''
global ec2
with open('report.csv', 'w') as csv_file:
csv_writer = csv.writer(csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_MINIMAL)
csv_writer.writerow([
'id',
'volume_id',
'volume_exists',
'ami_id',
'ami_exists',
'instance_id',
'instance_exists',
'size',
'start_time',
'description' ])
for region in regions:
ec2 = boto3.client('ec2', region_name=region)
for snapshot in get_snapshots():
csv_writer.writerow([
snapshot['id'],
snapshot['volume_id'],
snapshot['volume_exists'],
snapshot['ami_id'],
snapshot['ami_exists'],
snapshot['instance_id'],
snapshot['instance_exists'],
str(snapshot['size']) + 'gb',
str(snapshot['start_time']),
snapshot['description']])
@cli.command()
def snapshot_cleanup():
'''
Find and delete unreferenced snapshots.
'''
global ec2
print('{:22} {:23} {:23} {:23} {:>7} {:25} {:30}'.format('snapshot id', 'volume id', 'ami id',
'instance id', 'size', 'start time', 'description'))
for region in regions:
ec2 = boto3.client('ec2', region_name=region)
print('region={}'.format(region))
for snapshot in get_snapshots():
volume_exists = exists_icon if snapshot['volume_exists'] else not_exists_icon
ami_exists = exists_icon if snapshot['ami_exists'] else not_exists_icon
instance_exists = exists_icon if snapshot['instance_exists'] else not_exists_icon
print('{:22} {:22} {:22} {:22} {:>7} {:25} {:30}'.format(
snapshot['id'],
snapshot['volume_id'] + volume_exists,
snapshot['ami_id'] + ami_exists,
snapshot['instance_id'] + instance_exists,
str(snapshot['size']) + 'gb',
str(snapshot['start_time']),
snapshot['description']
))
if not snapshot['volume_exists'] and not snapshot['ami_exists'] and not snapshot['instance_exists'] and click.confirm('Delete?', default=True):
ec2.delete_snapshot(SnapshotId=snapshot['id'])
@cli.command()
def volume_cleanup():
'''
Find and delete unused volumes.
'''
global ec2
print('{:23} {:20} {:>7} {:10} {:23}'.format(
'volume id', 'status', 'size', 'created', 'snapshot id'))
for region in regions:
ec2 = boto3.client('ec2', region_name=region)
print('region={}'.format(region))
for volume in get_available_volumes():
snapshot_exists = exists_icon if volume['snapshot_exists'] else not_exists_icon
print('{:23} {:20} {:>7} {:10} {:22}'.format(
volume['id'],
volume['status'],
str(volume['size']) + 'gb',
volume['create_time'].strftime('%Y-%m-%d'),
volume['snapshot_id'] + snapshot_exists
))
if not volume['snapshot_exists']:
print('Tags:')
print(' '+('\n '.join(['{}={}'.format(click.style(key, fg='blue'), tag)
for key, tag in volume['tags'].items()])))
if click.confirm('Delete?', default=True):
ec2.delete_volume(VolumeId=volume['id'])
@cli.command()
@click.argument('snapshot_id')
def snapshot_delete(snapshot_id):
'''
Delete single snapshot by id.
'''
try:
ec2.delete_snapshot(SnapshotId=snapshot_id)
print('Deleted ' + snapshot_id)
except ClientError as e:
print('Failed to delete ' + snapshot_id)
print(e)
def get_snapshots():
'''
Get all snapshots.
'''
for snapshot in ec2.describe_snapshots(OwnerIds=['self'])['Snapshots']:
instance_id, image_id = parse_description(snapshot['Description'])
yield {
'id': snapshot['SnapshotId'],
'description': snapshot['Description'],
'start_time': snapshot['StartTime'],
'size': snapshot['VolumeSize'],
'volume_id': snapshot['VolumeId'],
'volume_exists': volume_exists(snapshot['VolumeId']),
'instance_id': instance_id,
'instance_exists': instance_exists(instance_id),
'ami_id': image_id,
'ami_exists': image_exists(image_id),
}
def get_available_volumes():
'''
Get all volumes in 'available' state. (Volumes not attached to any instance)
'''
for volume in ec2.describe_volumes(Filters=[{'Name': 'status', 'Values': ['available']}])['Volumes']:
yield {
'id': volume['VolumeId'],
'create_time': volume['CreateTime'],
'status': volume['State'],
'size': volume['Size'],
'snapshot_id': volume['SnapshotId'],
'snapshot_exists': str(snapshot_exists(volume['SnapshotId'])),
'tags': OrderedDict(sorted([(tag['Key'], tag['Value']) for tag in volume['Tags']])),
}
def snapshot_exists(snapshot_id):
if not snapshot_id:
return ''
try:
ec2.describe_snapshots(SnapshotIds=[snapshot_id])
return True
except ClientError:
return False
def volume_exists(volume_id):
if not volume_id:
return False
try:
ec2.describe_volumes(VolumeIds=[volume_id])
return True
except ClientError:
return False
def instance_exists(instance_id):
if not instance_id:
return ''
try:
return len(ec2.describe_instances(InstanceIds=[instance_id])['Reservations']) != 0
except ClientError:
return False
def image_exists(image_id):
if not image_id:
return ''
try:
return len(ec2.describe_images(ImageIds=[image_id])['Images']) != 0
except ClientError:
return False
def parse_description(description):
regex = r"^Created by CreateImage\((.*?)\) for (.*?) "
matches = re.finditer(regex, description, re.MULTILINE)
for matchNum, match in enumerate(matches):
return match.groups()
return '', ''
if __name__ == '__main__':
cli()
@smartkriash
Copy link

I am running the script for fetching the snapshot report before running delete with below command in windows

python snapshots.py snapshot-report

where will report get saved

If i run the same script in linux with below command

python /home/ec2-user/snapshots.py snapshot-report

It is generating a empty csv file.

Please help to generate report of orphaned snapshots

@MarkZagorski
Copy link

Did you modify the script to change the regions variable to reflect your used regions?
If you dont have anything in those two defined regions.. its not going to return anything.. because you dont have anything...

regions = ['us-west-1', 'eu-central-1']

Works out of the box for me...

@deeco
Copy link

deeco commented Nov 29, 2019

how does it handle if description of snapshot has different value other than regex = r"^Created by CreateImage((.?)) for (.?) "

@harishnehru
Copy link

Hi Eyjafjallajokull

I am not able to get the snapshot-cleanup working. Only the Report is working, none of the other function is working.

I am getting the below error, could you please help. I am running on Python 3.8.2

$ python snapshots.py snapshot-cleanup
b'snapshot id volume id ami id instance id size start time description '
region=eu-west-1
Traceback (most recent call last):
File "snapshots.py", line 217, in
cli()
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 828, in call
return self.main(*args, **kwargs)
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 781, in main
rv = self.invoke(ctx)
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 1227, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 1046, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 590, in invoke
return callback(*args, **kwargs)
File "snapshots.py", line 77, in snapshot_cleanup
print('{:22} {:22} {:22} {:22} {:>7} {:25} {:30}'.format(
File "C:\Program Files\Python38\lib\encodings\cp1252.py", line 19, in encode
return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode character '\u274c' in position 35: character maps to

Looking forward for your advise.

Thank you

@harishnehru
Copy link

Hi Eyjafjallajokull

I am not able to get the snapshot-cleanup working. Only the Report is working, none of the other function is working.

I am getting the below error, could you please help. I am running on Python 3.8.2

$ python snapshots.py snapshot-cleanup
b'snapshot id volume id ami id instance id size start time description '
region=eu-west-1
Traceback (most recent call last):
File "snapshots.py", line 217, in
cli()
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 828, in call
return self.main(*args, **kwargs)
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 781, in main
rv = self.invoke(ctx)
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 1227, in invoke
return _process_result(sub_ctx.command.invoke(sub_ctx))
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 1046, in invoke
return ctx.invoke(self.callback, **ctx.params)
File "C:\Users\alan\AppData\Roaming\Python\Python38\site-packages\click\core.py", line 590, in invoke
return callback(*args, **kwargs)
File "snapshots.py", line 77, in snapshot_cleanup
print('{:22} {:22} {:22} {:22} {:>7} {:25} {:30}'.format(
File "C:\Program Files\Python38\lib\encodings\cp1252.py", line 19, in encode
return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode character '\u274c' in position 35: character maps to

Looking forward for your advise.

Thank you

I was able to fix this by encoding with utf-8 on Windows 10. Use utf-8 in the code as shown below.

print('{:22} {:22} {:22} {:22} {:>7} {:25} {:30}'.format(
snapshot['id'],
snapshot['volume_id'] + volume_exists,
snapshot['ami_id'] + ami_exists,
snapshot['instance_id'] + instance_exists,
str(snapshot['size']) + 'gb',
str(snapshot['start_time']),
snapshot['description']
).encode('utf8'))

@x86nick
Copy link

x86nick commented May 11, 2020

Hello Eyjafjallajokull

Another helpful feature might be shared AMI vs Non Shared AMI.
Deleting Non Shared AMI will save cost. Or AMI thats is older then x amount of time will save cost as well.

@vickypiet
Copy link

I ran the python script to list snapshots via AWS Lambda. However, getting below error. Did anyone face this issue yet?
Unable to import module 'lambda_function': No module named 'click'

@dinhhuy258
Copy link

I ran the python script to list snapshots via AWS Lambda. However, getting below error. Did anyone face this issue yet?
Unable to import module 'lambda_function': No module named 'click'

Run command pip3 install click to install click module then everything will be fine

@Praba-N
Copy link

Praba-N commented Sep 21, 2021

Hi,

I'm getting following error while run script from Linux. Could someone help to fix?

File "snap.py", line 15
SyntaxError: Non-ASCII character '\xe2' in file snap.py on line 15, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details

Thanks!

@apio-sys
Copy link

There seems to be an issue with the regex in parse_description(description) since it is not returning the ami-id out of the "Created by CreateImage(i-01010101010101010) for ami-01010101010101010" string in the snapshot description. Hence the script will propose to delete a snapshot which has an AMI still registered thus rendering the remaining image useless without it's corresponding snapshot...

@bgth
Copy link

bgth commented May 7, 2022

Please check the regex.
regex = r"^Created by CreateImage\((.*?)\) for (.*)"

@TheDauntless
Copy link

A fixed parse_description which handles more 'created by' situations

def parse_description(description):
    if "from" in description:
        regex = r"^Created by CreateImage\((.*?)\) for (.*)(?: from .*)"
    else:
        regex = r"^Created by CreateImage\((.*?)\) for (ami.*)"
    matches = re.finditer(regex, description, re.MULTILINE)
    for matchNum, match in enumerate(matches):
        return match.groups()
    return '', ''

@ocordovez
Copy link

One question - How can I run this script for several accounts in an org ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment