#!/usr/bin/env python3 import boto3 import datetime import decimal import json import urllib.request import sys AGE_THRESHOLD = datetime.timedelta(hours=1) D = decimal.Decimal SLACK_URL = 'https://hooks.slack.com/services/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' # Class to minimize network chaos when getting pricing information # from the 'pricing' API. Basically, this caches price information # in a dict so that we do not have to look up the price for the same # instance type over and over in a single region. class Prices: # The 'pricing' API uses these ridiculous descriptive names # instead of the ubiquitous short names, and I have no idea # where to look up the mapping between them, it's so silly. region_map = { 'us-east-2': 'US East (Ohio)', 'us-east-1': 'US East (N. Virginia)', 'us-west-1': 'US West (N. California)', 'us-west-2': 'US West (Oregon)', 'af-south-1': 'Africa (Cape Town)', 'ap-east-1': 'Asia Pacific (Hong Kong)', 'ap-south-1': 'Asia Pacific (Mumbai)', 'ap-northeast-3': 'Asia Pacific (Osaka)', 'ap-northeast-2': 'Asia Pacific (Seoul)', 'ap-southeast-1': 'Asia Pacific (Singapore)', 'ap-southeast-2': 'Asia Pacific (Sydney)', 'ap-northeast-1': 'Asia Pacific (Tokyo)', 'ca-central-1': 'Canada (Central)', 'eu-central-1': 'Europe (Frankfurt)', 'eu-west-1': 'Europe (Ireland)', 'eu-west-2': 'Europe (London)', 'eu-south-1': 'Europe (Milan)', 'eu-west-3': 'Europe (Paris)', 'eu-north-1': 'Europe (Stockholm)', 'me-south-1': 'Middle East (Bahrain)', 'sa-east-1': 'South America (São Paulo)', } prices={} def __init__(self, region): self.region = region self.region_name = self.region_map[region] self.pricing = boto3.client('pricing', region_name='us-east-1') def price(self, instance_type): if not instance_type in self.prices: try: p = self.pricing.get_products( ServiceCode='AmazonEC2', FormatVersion='aws_v1', Filters=[ { 'Type':'TERM_MATCH', 'Field':'operatingSystem', 'Value':'Linux'}, { 'Type':'TERM_MATCH', 'Field':'operation', 'Value':'RunInstances'}, { 'Type':'TERM_MATCH', 'Field':'capacitystatus', 'Value':'Used'}, { 'Type':'TERM_MATCH', 'Field':'tenancy', 'Value':'Shared'}, { 'Type':'TERM_MATCH', 'Field':'location', 'Value':self.region_name}, { 'Type':'TERM_MATCH', 'Field':'instanceType', 'Value':instance_type}, ] ) if len(p['PriceList']) > 1: print('Price lookup matched more than one item!') price_details = json.loads(p['PriceList'][0])['terms']['OnDemand'] if len(price_details) > 1: print('Price lookup matched more than one item!') print(price_details.keys()) for px in price_details: product = price_details[px] for px in product['priceDimensions']: price = product['priceDimensions'][px]['pricePerUnit']['USD'] self.prices[instance_type] = D(price) except KeyError as e: print('Could not find region: {}'.format(e.__traceback__)) return(self.prices[instance_type]) def run(): ec2 = boto3.client('ec2', region_name='us-east-1') regions = ec2.describe_regions() instances = {} # for region in ['us-east-1', 'us-east-2', 'us-west-1', 'us-west-2']: for region in [r['RegionName'] for r in regions['Regions']]: p = Prices(region=region) print(region) e = boto3.client('ec2', region_name=region) reservations = e.describe_instances( Filters=[{ 'Name':'instance-state-name', 'Values':['running'] }]) for r in reservations['Reservations']: for i in r['Instances']: # Skip instances younger than age_threshold if datetime.datetime.now(datetime.timezone.utc) - i['LaunchTime'] < AGE_THRESHOLD: continue if region not in instances: instances[region] = [] inst={ 'id': i['InstanceId'], 'az': i['Placement']['AvailabilityZone'], 'time': i['LaunchTime'], 'type': i['InstanceType'], 'key': i.get('KeyName'), 'cost': p.price(i['InstanceType']), } instances[region].append(inst) total_cost = D() message = {'blocks': [ {'type': 'section', 'text': {'type': 'mrkdwn', 'text': 'Cost report for public cloud instances running longer than {}.'.format(AGE_THRESHOLD)}}, {'type': 'divider'}, ]} region_costs = [] for r in instances: cost = sum([inst['cost'] for inst in instances[r]]) total_cost += cost region_costs.append('*{}*: ${}\n'.format(r, cost.quantize(D('1.01')))) region_costs.append('total: ${}\n'.format(total_cost.quantize(D('1.01')))) message['blocks'] += [ {'type': 'header', 'text': {'type': 'plain_text', 'text': 'AWS', 'emoji': True}}, {'type': 'section', 'text': {'type': 'mrkdwn', 'text': '\n'.join(region_costs)}} ] print(json.dumps(message)) urllib.request.urlopen(SLACK_URL, json.dumps(message).encode() ) if __name__ == '__main__': run()