├── .gitignore ├── README.md ├── check-reserved-elasticache-instance ├── check-reserved-instances └── check-reserved-rds-instances /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # check-aws-reserved-instances 2 | 3 | This script shows summary about using "Reserved" and "On-demand" ec2 instances. Namely: 4 | 5 | * Which "On-demand" instances haven't got a "Reserved" instance; 6 | * Which "Reserved" instances are unused; 7 | * And which "Reserved" instances are expiring soon. 8 | 9 | The script is heavily based on Scott Bigelow's work: https://github.com/epheph/ec2-check-reserved-instances 10 | 11 | ## Requirements 12 | 13 | - Python 2.6+ 14 | - argparse (required when using Python 2.6) 15 | - boto3 16 | 17 | ## How to work with it 18 | 19 | For the script needs your [AWS Security Credentials](http://docs.aws.amazon.com/AWSSecurityCredentials/1.0/AboutAWSCredentials.html). 20 | You can specify them in [the Boto config](http://boto3.readthedocs.io/en/latest/guide/configuration.html) (`~/.boto` or `/etc/boto.cfg`) or using script command line arguments or by exporting in an environment variables (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`). 21 | 22 | Example: 23 | 24 | host# export AWS_ACCESS_KEY_ID=ABCDE 25 | host# export AWS_SECRET_ACCESS_KEY=5ji7haengeeFoh8eziebeu 26 | host# ./check-reserved-instances --region us-west-1 -w 60 27 | Unused reserved instances: 28 | (2) Linux/UNIX m1.small us-west-1c 29 | (3) Linux/UNIX m1.large us-west-1c 30 | 31 | Soon expiring (less than 60d) reserved instances: 32 | 93bbbca2-d072-4dcc-bb7e-7c137ad565f7 Linux/UNIX m1.small us-west-1c 2014-04-15 33 | bbcd9749-4bf0-440a-bf53-3641e3732b73 Linux/UNIX m1.small us-west-1c 2014-04-03 34 | 35 | On-demand instances, which haven't got a reserved instance: 36 | (1) Linux/UNIX m3.medium us-west-1c 37 | (3) Linux/UNIX m1.large us-west-1b 38 | (1) Linux/UNIX m1.medium us-west-1b 39 | 40 | Running on-demand instances: 27 41 | Reserved instances: 22 42 | 43 | 44 | For more help use: 45 | 46 | host# check-reserved-instances -h 47 | -------------------------------------------------------------------------------- /check-reserved-elasticache-instance: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import datetime 7 | 8 | import boto3.session 9 | import six 10 | from dateutil.tz import tzutc 11 | 12 | parser = argparse.ArgumentParser( 13 | description="Shows summary about 'Reserved' and 'On-demand' elasticache instances") 14 | parser.add_argument("--aws-access-key", type=str, default=None, 15 | help="AWS Access Key ID. Defaults to the value of the " 16 | "AWS_ACCESS_KEY_ID environment variable (if set)") 17 | parser.add_argument("--aws-secret-key", type=str, default=None, 18 | help="AWS Secret Access Key. Defaults to the value of the " 19 | "AWS_SECRET_ACCESS_KEY environment variable (if set)") 20 | parser.add_argument("--region", type=str, default="cn-north-1", 21 | help="AWS Region name. Default is 'cn-north-1'") 22 | parser.add_argument("-w", "--warn-time", type=int, default=30, 23 | help="Expire period for reserved instances in days. " 24 | "Default is '30 days'") 25 | args = parser.parse_args() 26 | 27 | def instance_header(): 28 | return ("\t(%s)\t%12s\t%s" %("Count", "Engine", "Instance")) 29 | 30 | def expire_header(): 31 | return ("\t(%s)\t%36s\t%14s\t%s" %("ID", "Type", "Engine", "Region")) 32 | 33 | session = boto3.session.Session(region_name=args.region, 34 | aws_access_key_id=args.aws_access_key, 35 | aws_secret_access_key=args.aws_secret_key) 36 | 37 | elasticache_client = session.client('elasticache') 38 | clusters = elasticache_client.describe_cache_clusters()['CacheClusters'] 39 | 40 | running_instances = {} 41 | for c in clusters: 42 | if c['CacheClusterStatus'] != 'available': 43 | continue 44 | if not c['PreferredAvailabilityZone'].startswith(args.region): 45 | continue 46 | a_zone = args.region 47 | key = (c['Engine'], c['CacheNodeType'], a_zone) 48 | running_instances[key] = running_instances.get(key, 0) + 1 49 | 50 | reserved_instances = {} 51 | soon_expire_ri = {} 52 | 53 | reserved_elasticache_instances = elasticache_client.describe_reserved_cache_nodes() 54 | reservations = reserved_elasticache_instances['ReservedCacheNodes'] 55 | now = datetime.datetime.utcnow().replace(tzinfo=tzutc()) 56 | for ri in reservations: 57 | if ri['State'] == 'retired': 58 | continue 59 | ri_id = ri['ReservedCacheNodesOfferingId'] 60 | ri_type = ri['CacheNodeType'] 61 | ri_count = ri['CacheNodeCount'] 62 | ri_engine = ri['ProductDescription'] 63 | key = (ri_engine, ri_type, args.region) 64 | reserved_instances[key] = \ 65 | reserved_instances.get(key, 0) + ri_count 66 | expire_time = ri['StartTime'] + datetime.timedelta(seconds=ri['Duration']) 67 | if (expire_time - now) < datetime.timedelta(days=args.warn_time): 68 | soon_expire_ri[ri_id] = (ri_type, ri_engine, args.region, expire_time) 69 | 70 | diff = dict([(x, reserved_instances[x] - running_instances.get(x, 0)) 71 | for x in reserved_instances]) 72 | 73 | for pkey in running_instances: 74 | if pkey not in reserved_instances: 75 | diff[pkey] = -running_instances[pkey] 76 | 77 | unused_ri = {} 78 | unreserved_instances = {} 79 | for k, v in six.iteritems(diff): 80 | if v > 0: 81 | unused_ri[k] = v 82 | elif v < 0: 83 | unreserved_instances[k] = -v 84 | 85 | # Report 86 | print("Unused reserved Elasticache instances:") 87 | print(instance_header()) 88 | for k, v in sorted(six.iteritems(unused_ri), key=lambda x: x[0]): 89 | print("\t(%s)\t%12s\t%s" %(v, k[0], k[1])) 90 | if not unused_ri: 91 | print("\tNone") 92 | print("") 93 | 94 | print("Expiring soon (less than %sd) reserved Elasticache instances:" % args.warn_time) 95 | print(expire_header()) 96 | for k, v in sorted(six.iteritems(soon_expire_ri), key=lambda x: x[1][:2]): 97 | print("\t%s\t%12s\t%s\t%s\t%s" %(k, v[0], v[1], v[2], v[3].strftime('%Y-%m-%d'))) 98 | if not soon_expire_ri: 99 | print("\tNone") 100 | print("") 101 | 102 | print("On-demand Elasticache instances, which haven't got a reserved Elasticache instance:") 103 | print(instance_header()) 104 | for k, v in sorted(six.iteritems(unreserved_instances), key=lambda x: x[0]): 105 | print("\t(%s)\t%12s\t%s" %(v, k[0], k[1])) 106 | if not unreserved_instances: 107 | print("\tNone") 108 | print("") 109 | 110 | print("Running on-demand Elasticache instances: %s" % sum(running_instances.values())) 111 | print("Reserved Elasticache instances: %s" % sum(reserved_instances.values())) 112 | print("") 113 | -------------------------------------------------------------------------------- /check-reserved-instances: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import datetime 7 | import re 8 | 9 | import boto3.session 10 | import six 11 | from dateutil.tz import tzutc 12 | 13 | parser = argparse.ArgumentParser( 14 | description="Shows summary about 'Reserved' and 'On-demand' ec2 instances") 15 | parser.add_argument("--aws-access-key", type=str, default=None, 16 | help="AWS Access Key ID. Defaults to the value of the " 17 | "AWS_ACCESS_KEY_ID environment variable (if set)") 18 | parser.add_argument("--aws-secret-key", type=str, default=None, 19 | help="AWS Secret Access Key. Defaults to the value of the " 20 | "AWS_SECRET_ACCESS_KEY environment variable (if set)") 21 | parser.add_argument("--region", type=str, default="cn-north-1", 22 | help="AWS Region name. Default is 'cn-north-1'") 23 | parser.add_argument("-w", "--warn-time", type=int, default=30, 24 | help="Expire period for reserved instances in days. " 25 | "Default is '30 days'") 26 | args = parser.parse_args() 27 | REGION = args.region + ' (Region)' 28 | 29 | session = boto3.session.Session(region_name=args.region, 30 | aws_access_key_id=args.aws_access_key, 31 | aws_secret_access_key=args.aws_secret_key) 32 | ec2 = session.resource('ec2') 33 | ec2_client = session.client('ec2') 34 | 35 | 36 | IMAGE_PLATFORM_PAT = { 37 | 'Red Hat': [ 38 | re.compile(r'.*\b(RHEL|rhel)\b.*'), 39 | ], 40 | } 41 | image_id_to_platform = {} 42 | 43 | # Retrieve instances 44 | instances = ec2.instances.all() 45 | running_instances = {} 46 | for i in instances: 47 | if i.state['Name'] != 'running': 48 | continue 49 | 50 | image_id = i.image_id 51 | if image_id in image_id_to_platform: 52 | platform = image_id_to_platform[image_id] 53 | else: 54 | platform = 'Linux/UNIX' 55 | try: 56 | image = ec2.Image(image_id) 57 | if image.platform: 58 | platform = image.platform 59 | elif image.image_location.startswith('841258680906/'): 60 | platform = 'Red Hat' 61 | else: 62 | image_name = image.name 63 | for plat, pats in six.iteritems(IMAGE_PLATFORM_PAT): 64 | for pat in pats: 65 | if pat.match(image_name): 66 | platform = plat 67 | break 68 | except AttributeError: 69 | platform = 'Linux/UNIX' 70 | 71 | image_id_to_platform[image_id] = platform 72 | 73 | key = (platform, i.instance_type, i.placement['AvailabilityZone'] if 'AvailabilityZone' in i.placement else REGION) 74 | running_instances[key] = running_instances.get(key, 0) + 1 75 | 76 | # Retrieve reserved instances 77 | reserved_instances = {} 78 | soon_expire_ri = {} 79 | 80 | reservations = ec2_client.describe_reserved_instances() 81 | now = datetime.datetime.utcnow().replace(tzinfo=tzutc()) 82 | for ri in reservations['ReservedInstances']: 83 | if ri['State'] not in ('active', 'payment-pending'): 84 | continue 85 | key = (ri['ProductDescription'], ri['InstanceType'], 86 | ri['AvailabilityZone'] if 'AvailabilityZone' in ri else REGION) 87 | reserved_instances[key] = \ 88 | reserved_instances.get(key, 0) + ri['InstanceCount'] 89 | expire_time = ri['Start'] + datetime.timedelta(seconds=ri['Duration']) 90 | if (expire_time - now) < datetime.timedelta(days=args.warn_time): 91 | soon_expire_ri[ri['ReservedInstancesId']] = key + (expire_time,) 92 | 93 | # Create a dict relating reserved instance keys to running instances. 94 | # Reserveds get modulated by running. 95 | diff = dict([(x, reserved_instances[x] - running_instances.get(x, 0)) 96 | for x in reserved_instances]) 97 | 98 | # Subtract all region-unspecific RIs. 99 | for reserved_pkey in reserved_instances: 100 | if reserved_pkey[2] == REGION: 101 | # Go through all running instances and subtract. 102 | for running_pkey in running_instances: 103 | if running_pkey[0] == reserved_pkey[0] and running_pkey[1] == reserved_pkey[1]: 104 | diff[running_pkey] = diff.get(running_pkey, 0) + running_instances[running_pkey] 105 | diff[reserved_pkey] -= running_instances[running_pkey] 106 | 107 | # For all other running instances, add a negative amount 108 | for pkey in running_instances: 109 | if pkey not in reserved_instances: 110 | diff[pkey] = diff.get(pkey, 0) - running_instances[pkey] 111 | 112 | unused_ri = {} 113 | unreserved_instances = {} 114 | for k, v in six.iteritems(diff): 115 | if v > 0: 116 | unused_ri[k] = v 117 | elif v < 0: 118 | unreserved_instances[k] = -v 119 | 120 | # Report 121 | print("Reserved instances:") 122 | for k, v in sorted(six.iteritems(reserved_instances), key=lambda x: x[0]): 123 | print("\t(%s)\t%12s\t%s\t%s" % ((v,) + k)) 124 | print("") 125 | 126 | print("Unused reserved instances:") 127 | for k, v in sorted(six.iteritems(unused_ri), key=lambda x: x[0]): 128 | print("\t(%s)\t%12s\t%s\t%s" % ((v,) + k)) 129 | if not unused_ri: 130 | print("\tNone") 131 | print("") 132 | 133 | print("Expiring soon (less than %sd) reserved instances:" % args.warn_time) 134 | for k, v in sorted(six.iteritems(soon_expire_ri), key=lambda x: x[1][:2]): 135 | (platform, instance_type, region, expire_date) = v 136 | expire_date = expire_date.strftime('%Y-%m-%d') 137 | print("\t%s\t%12s\t%s\t%s\t%s" % (k, platform, instance_type, region, 138 | expire_date)) 139 | if not soon_expire_ri: 140 | print("\tNone") 141 | print("") 142 | 143 | print("On-demand instances, which haven't got a reserved instance:") 144 | for k, v in sorted(six.iteritems(unreserved_instances), key=lambda x: x[0]): 145 | print("\t(%s)\t%12s\t%s\t%s" % ((v,) + k)) 146 | if not unreserved_instances: 147 | print("\tNone") 148 | print("") 149 | 150 | print("Running on-demand instances: %s" % sum(running_instances.values())) 151 | print("Reserved instances: %s" % sum(reserved_instances.values())) 152 | print("") 153 | -------------------------------------------------------------------------------- /check-reserved-rds-instances: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import argparse 6 | import datetime 7 | 8 | import boto3.session 9 | import six 10 | from dateutil.tz import tzutc 11 | 12 | parser = argparse.ArgumentParser( 13 | description="Shows summary about 'Reserved' and 'On-demand' rds instances") 14 | parser.add_argument("--aws-access-key", type=str, default=None, 15 | help="AWS Access Key ID. Defaults to the value of the " 16 | "AWS_ACCESS_KEY_ID environment variable (if set)") 17 | parser.add_argument("--aws-secret-key", type=str, default=None, 18 | help="AWS Secret Access Key. Defaults to the value of the " 19 | "AWS_SECRET_ACCESS_KEY environment variable (if set)") 20 | parser.add_argument("--region", type=str, default="cn-north-1", 21 | help="AWS Region name. Default is 'cn-north-1'") 22 | parser.add_argument("-w", "--warn-time", type=int, default=30, 23 | help="Expire period for reserved instances in days. " 24 | "Default is '30 days'") 25 | args = parser.parse_args() 26 | 27 | 28 | session = boto3.session.Session(region_name=args.region, 29 | aws_access_key_id=args.aws_access_key, 30 | aws_secret_access_key=args.aws_secret_key) 31 | 32 | rds_client = session.client('rds') 33 | 34 | instances = rds_client.describe_db_instances()['DBInstances'] 35 | 36 | def normalized_engine(name): 37 | engine = {} 38 | engine['postgres'] = 'postgresql' 39 | return engine.get(name, name) 40 | def instance_header(): 41 | return ("\t(%s)\t%12s\t%s\t%s" %("Count", "Engine", "Instance", "Multi-AZ")) 42 | 43 | running_instances = {} 44 | for i in instances: 45 | if i['DBInstanceStatus'] != 'available': 46 | continue 47 | if not i['AvailabilityZone'].startswith(args.region): 48 | continue 49 | a_zone = args.region 50 | db_engine = normalized_engine(i['Engine']) 51 | key = (db_engine, i['DBInstanceClass'], a_zone, i['MultiAZ']) 52 | running_instances[key] = running_instances.get(key, 0) + 1 53 | 54 | reserved_instances = {} 55 | soon_expire_ri = {} 56 | 57 | reserved_rds_instances = rds_client.describe_reserved_db_instances() 58 | reservations = reserved_rds_instances['ReservedDBInstances'] # noqa 59 | now = datetime.datetime.utcnow().replace(tzinfo=tzutc()) 60 | for ri in reservations: 61 | if ri['State'] == 'retired': 62 | continue 63 | ri_id = ri['ReservedDBInstanceId'] 64 | ri_type = ri['DBInstanceClass'] 65 | ri_count = ri['DBInstanceCount'] 66 | ri_engine = ri['ProductDescription'] 67 | ri_multiaz = ri['MultiAZ'] 68 | key = (ri_engine, ri_type, args.region, ri_multiaz) 69 | reserved_instances[key] = \ 70 | reserved_instances.get(key, 0) + ri_count 71 | ri_start_time = ri['StartTime'] 72 | expire_time = ri_start_time + datetime.timedelta(seconds=ri['Duration']) 73 | if (expire_time - now) < datetime.timedelta(days=args.warn_time): 74 | soon_expire_ri[ri_id] = (ri_type, ri_engine, args.region, expire_time) 75 | 76 | diff = dict([(x, reserved_instances[x] - running_instances.get(x, 0)) 77 | for x in reserved_instances]) 78 | 79 | for pkey in running_instances: 80 | if pkey not in reserved_instances: 81 | diff[pkey] = -running_instances[pkey] 82 | 83 | unused_ri = {} 84 | unreserved_instances = {} 85 | for k, v in six.iteritems(diff): 86 | if v > 0: 87 | unused_ri[k] = v 88 | elif v < 0: 89 | unreserved_instances[k] = -v 90 | 91 | # Report 92 | print("Unused reserved RDS instances:") 93 | print(instance_header()) 94 | for k, v in sorted(six.iteritems(unused_ri), key=lambda x: x[0]): 95 | print("\t(%s)\t%12s\t%s\t%s" %(v, k[0], k[1], k[3])) 96 | if not unused_ri: 97 | print("\tNone") 98 | print("") 99 | 100 | print("Expiring soon (less than %sd) reserved RDS instances:" % args.warn_time) 101 | for k, v in sorted(six.iteritems(soon_expire_ri), key=lambda x: x[1][:2]): 102 | print("\t%s\t%12s\t%s\t%s\t%s" %( 103 | k, v[0], v[1], v[2], v[3].strftime('%Y-%m-%d'))) 104 | if not soon_expire_ri: 105 | print("\tNone") 106 | print("") 107 | 108 | print("On-demand RDS instances, which haven't got a reserved RDS instance:") 109 | print(instance_header()) 110 | for k, v in sorted(six.iteritems(unreserved_instances), key=lambda x: x[0]): 111 | print("\t(%s)\t%12s\t%s\t%s" %(v, k[0], k[1], k[3])) 112 | if not unreserved_instances: 113 | print("\tNone") 114 | print("") 115 | 116 | print("Running on-demand RDS instances: %s" % sum(running_instances.values())) 117 | print("Reserved RDS instances: %s" % sum(reserved_instances.values())) 118 | print("") 119 | --------------------------------------------------------------------------------