#!/usr/bin/env python # ############################################################################### # # cleanup-packer-aws-resources.py Written by Farley <farley@neonsurge.com> # # Packer when used on an AWS account from tools like Jenkins or Rundeck, often # leaves reminants of its existance such as instances running, security groups, # and SSH keys. This script scans all regions of AWS for leftover packer # resources and removes them. # # This script can be run on the command-line standalone or ideally put into packer # and run via cloudwatch scheduled events like once a day or so # # NOTE: If you use this in AWS Lambda, please comment out the last line! It's # not required but will make sure this script doesn't run an extra time # # This is from Farley's AWS missing tools # https://github.com/AndrewFarley/farley-aws-missing-tools/ # ############################################################################### # # Minimm AWS Permissions Necessary to run this script # # NOTE: The lambda:InvokeFunction is only needed if you want to run this from AWS Lambda # Similar to the logs:* functions are only needed if you want to run from lambda and if you want logging # # { # "Version": "2012-10-17", # "Statement": [ # { # "Effect": "Allow", # "Action": [ # "logs:CreateLogGroup", # "logs:CreateLogStream", # "logs:DescribeLogGroups", # "logs:DescribeLogStreams", # "logs:PutLogEvents", # "ec2:DescribeRegions", # "ec2:DescribeInstances", # "ec2:DescribeKeyPairs", # "ec2:DescribeSecurityGroups", # "ec2:TerminateInstances", # "ec2:DeleteKeyPair", # "ec2:DeleteSecurityGroup" # ], # "Resource": "*" # }, # { # "Action": "lambda:InvokeFunction", # "Effect": "Allow", # "Resource": "*" # } # ] # } # ############################################################################### from __future__ import print_function # For AWS import boto3 # For pretty-print from pprint import pprint from datetime import datetime import calendar # The maximum age (in seconds) of a packer instance before we terminate it # 86400 == 1 day # 21600 == 6 hours # 10800 == 3 hours # 3600 == 1 hour max_age = 21600 # Whether or not to output debug info as it does things debug = False # Our AWS regions, we'll call the AWS API to get the list of regions, so this is always up to date ec2 = boto3.client('ec2', region_name='us-west-1') regions = [] awsregions = ec2.describe_regions()['Regions'] for region in awsregions: regions.append(region['RegionName']) del ec2, awsregions # Helper to convert datetime with TZ to Unix time def dt2ts(dt): return calendar.timegm(dt.utctimetuple()) # Helper to convert seconds to a sexy format "x hours, x minutes, x seconds" etc def display_time(seconds, granularity=2): intervals = ( ('months', 18144000), # 60 * 60 * 24 * 7 * 30 (roughly) ('weeks', 604800), # 60 * 60 * 24 * 7 ('days', 86400), # 60 * 60 * 24 ('hours', 3600), # 60 * 60 ('minutes', 60), ('seconds', 1), ) result = [] for name, count in intervals: value = seconds // count if value: seconds -= value * count if value == 1: name = name.rstrip('s') result.append("{} {}".format(value, name)) return ', '.join(result[:granularity]) # Get instances from AWS from all regions that... # #1: Are currently running # #2: Have the name "Packer Builder" # #3: def get_zombie_packer_instances(regions, maximum_age): global debug output = {} # Get our "now" timestamp for knowing how long ago instances were launched utc_now = datetime.now() utc_now_ts = int(utc_now.strftime("%s")) for region in regions: regionoutput = [] if debug is True: print("Scanning region " + region + " for instances") # Create our EC2 Handler ec2 = boto3.client('ec2', region_name=region) response = ec2.describe_instances( MaxResults=1000 ) for reservation in response['Reservations']: for instance in reservation['Instances']: if debug is True: print("Found instance: " + instance['InstanceId']) if instance['State']['Name'] == "running": if debug is True: print(" Instance is currently running") else: if debug is True: print(" Instance is not currently running, skipping...") continue packerNameTagMatched = False if 'Tags' in instance: for tag in instance['Tags']: if tag['Key'] == 'Name' and tag['Value'] == 'Packer Builder': if debug is True: print(" Instance is a packer builder") packerNameTagMatched = True break elif tag['Key'] == 'Name': if debug is True: print(" Instance is NOT a packer building, skipping...") break if packerNameTagMatched is False: continue if debug is True: print(" Found packer instance: " + instance['InstanceId']) launched_at = dt2ts(instance['LaunchTime']) if debug is True: print(" Instance started " + display_time(utc_now_ts - launched_at) + " ago ") # if (utc_now_ts - launched_at) > 86400: if (utc_now_ts - launched_at) > maximum_age: if debug is True: print(" Instance started more than a day ago, should be marked for termination") regionoutput.append({ "region": region, "instance_id": instance['InstanceId'], "keyname": instance['KeyName'], "security_groups": instance['SecurityGroups'] }) else: if debug is True: print(" Instance is too new to be terminated") output[region] = regionoutput return output def get_zombie_packer_keys(regions): global debug output = {} for region in regions: regionoutput = [] if debug is True: print("Scanning region " + region + " for keys") # Create our EC2 Handler ec2 = boto3.client('ec2', region_name=region) response = ec2.describe_key_pairs( Filters=[ { 'Name': 'key-name', 'Values': ['packer *'], }, ] ) for pair in response['KeyPairs']: regionoutput.append(pair['KeyName']) output[region] = regionoutput return output def get_zombie_packer_security_groups(regions): global debug output = {} for region in regions: regionoutput = [] if debug is True: print("Scanning region " + region + " for security groups") # Create our EC2 Handler ec2 = boto3.client('ec2', region_name=region) response = ec2.describe_security_groups( Filters=[ { 'Name': 'group-name', 'Values': ['packer *'], }, ] ) # NOTE: # Checking for stales doesn't seem to work, so we'll just try to delete without checking stale # # Get our VPCs # vpcs = ec2.describe_vpcs()['Vpcs'] # # Get all stale sgs in every vpc # for vpc in vpcs: # stale = ec2.describe_stale_security_groups(VpcId=vpc['VpcId'])['StaleSecurityGroupSet'] # pprint(stale) for pair in response['SecurityGroups']: regionoutput.append(pair['GroupName']) #if len(regionoutput) > 0: # stalegroups = ec2.describe_security_groups # for groupname in regionoutput: output[region] = regionoutput return output def lambda_handler(event, context): global regions, max_age print("Scanning " + str(len(regions)) + " AWS regions for zombie packer instances...") zombies = get_zombie_packer_instances(regions, max_age) for region,instances in zombies.iteritems(): if len(instances) == 0: print("Found NO zombie instances in " + region + ", skipping...") continue print("Found " + str(len(instances)) + " zombie packer instances in " + region + ", now terminating...") ec2 = boto3.client('ec2', region_name=region) instance_ids = [] for instance in instances: instance_ids.append(instance['instance_id']) try: response = ec2.terminate_instances( InstanceIds=instance_ids ) print("Successfully terminated instances") except: print("ERROR: Unable to terminate some or all resources") print("Scanning " + str(len(regions)) + " AWS regions for zombie packer keys...") zombies = get_zombie_packer_keys(regions) for region,keynames in zombies.iteritems(): if len(keynames) == 0: print("Found NO zombie keys in " + region + ", skipping...") continue print("Found " + str(len(keynames)) + " zombie packer keys in " + region + ", now deleting...") ec2 = boto3.client('ec2', region_name=region) for keyname in keynames: try: print("Deleting key " + keyname) response = ec2.delete_key_pair( KeyName=keyname ) print("Deleted") except: print("Error while trying to terminate resources") print("Scanning " + str(len(regions)) + " AWS regions for zombie packer security groups...") zombies = get_zombie_packer_security_groups(regions) for region,security_groups in zombies.iteritems(): if len(security_groups) == 0: print("Found NO zombie security groups in " + region + ", skipping...") continue print("Found " + str(len(security_groups)) + " zombie security groups in " + region + ", now terminating...") ec2 = boto3.client('ec2', region_name=region) for security_group in security_groups: try: print("Deleting security group " + security_group) response = ec2.delete_security_group( GroupName=security_group ) print("Deleted") except: print("Error while trying to terminate resources") # NOTE: REMOVE THIS BELOW FOR RUNNING ON LAMBDA otherwise spinning this lambda # up will already run its main logic. Which is okay, but not really intended # If anyone knows how to detect running on Lambda please add this feature! :P lambda_handler({}, "test")