Friday, 23 September 2016

AWS Scheduler - Lambda function to stop/start EC2 instances and save costs

https://s3.amazonaws.com/solutions-reference/ec2-scheduler/latest/ec2-scheduler.pdf
https://github.com/awslabs/ec2-scheduler

{
    "AWSTemplateFormatVersion": "2010-09-09",
    "Description": "(SO0002) - EC2 Scheduler: This template installs an opt-in version of the EC2 Scheduler for automatically starting and stopping EC2 instances.",
    "Parameters": {
        "Schedule": {
            "Description": "Schedule for CWE Scheduled Expression (e.g. rate([5 minutes|1 hour|1 day]) or cron(0 17 ? * MON-FRI *))",
            "Type": "String",
            "Default": "rate(5 minutes)"
        },
        "DefaultStartTime": {
            "Description": "Default Start Time (UTC, 24-hour format)",
            "Type": "String",
            "Default": "0800"
        },
        "DefaultStopTime": {
            "Description": "Default Start Time (UTC, 24-hour format)",
            "Type": "String",
            "Default": "1800"
        },
        "DefaultDaysActive": {
            "Description": "Enter 'all', 'weekdays', or any combination of days ('mon', 'tue', 'wed', 'thu', 'fri', 'sat', or 'sun') comma separated",
            "Type": "String",
            "Default": "all"
        },
        "CustomTagName": {
            "Description": "Custom Tag Name",
            "Type": "String",
            "Default": "scheduler:ec2-startstop"
        },
        "DynamoDBTableName": {
            "Description": "DynamoDB Table Name",
            "Type": "String",
            "Default": "EC2-Scheduler"
        },
        "ReadCapacityUnits": {
            "ConstraintDescription": "should be between 5 and 10000",
            "Default": "1",
            "Description": "Provisioned read throughput",
            "MaxValue": "10000",
            "MinValue": "1",
            "Type": "Number"
        },
        "WriteCapacityUnits": {
            "ConstraintDescription": "should be between 5 and 10000",
            "Default": "1",
            "Description": "Provisioned write throughput",
            "MaxValue": "10000",
            "MinValue": "1",
            "Type": "Number"
        },
        "SendAnonymousData": {
            "Description": "Send anonymous data to AWS",
            "Type": "String",
            "Default": "Yes",
            "AllowedValues": [
                "Yes",
                "No"
            ]
        },
        "CloudWatchMetrics": {
            "Description": "Create CloudWatch Custom Metric",
            "Type": "String",
            "Default": "Enabled",
            "AllowedValues": [
                "Enabled",
                "Disabled"
            ]
        }
    },
    "Metadata": {
        "AWS::CloudFormation::Interface": {
            "ParameterGroups": [
                {
                    "Label": {
                        "default": "Tag Configuration"
                    },
                    "Parameters": [
                        "CustomTagName"
                    ]
                },
                {
                    "Label": {
                        "default": "CloudWatch Event Schedule Configuration"
                    },
                    "Parameters": [
                        "Schedule"
                    ]
                },
                {
                    "Label": {
                        "default": "Default Value Configuration"
                    },
                    "Parameters": [
                        "DefaultStartTime",
                        "DefaultStopTime",
                        "DefaultDaysActive"
                    ]
                },
                {
                    "Label": {
                        "default": "DynamoDB Configuration"
                    },
                    "Parameters": [
                        "DynamoDBTableName",
                        "ReadCapacityUnits",
                        "WriteCapacityUnits"
                    ]
                },
                {
                    "Label": {
                        "default": "CloudWatch Custom Metric"
                    },
                    "Parameters": [
                        "CloudWatchMetrics"
                    ]
                },
                {
                    "Label": {
                        "default": "Anonymous Metrics Request"
                    },
                    "Parameters": [
                        "SendAnonymousData"
                    ]
                }
            ]
        }
    },
    "Resources": {
        "ec2SchedulerRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": "lambda.amazonaws.com"
                            },
                            "Action": "sts:AssumeRole"
                        }
                    ]
                },
                "Path": "/",
                "Policies": [
                    {
                        "PolicyName": "ec2SchedulerPermissions",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "logs:CreateLogGroup",
                                        "logs:CreateLogStream",
                                        "logs:PutLogEvents"
                                    ],
                                    "Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/*"
                                },
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "dynamodb:GetItem"
                                    ],
                                    "Resource": [
                                        "arn:aws:dynamodb:*:*:table/*"
                                    ]
                                },
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "ec2:StartInstances",
                                        "ec2:StopInstances",
                                        "ec2:DescribeRegions",
                                        "ec2:DescribeInstances",
                                        "cloudwatch:PutMetricData",
                                        "cloudformation:DescribeStacks"
                                    ],
                                    "Resource": "*"
                                }
                            ]
                        }
                    }
                ]
            }
        },
        "ec2SchedulerOptIn": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Handler": "ec2-scheduler.lambda_handler",
                "Role": {
                    "Fn::GetAtt": [
                        "ec2SchedulerRole",
                        "Arn"
                    ]
                },
                "Description": "EC2 Scheduler Lambda function for automatically starting and stopping EC2 instances.",
                "Code": {
                    "S3Bucket": {
                        "Fn::Join": [
                            "",
                            [
                                "solutions-",
                                {
                                    "Ref": "AWS::Region"
                                }
                            ]
                        ]
                    },
                    "S3Key": "ec2-scheduler/v1/ec2-scheduler.zip"
                },
                "Runtime": "python2.7",
                "Timeout": "300"
            }
        },
        "CreateParamDDB": {
            "Properties": {
                "AttributeDefinitions": [
                    {
                        "AttributeName": "SolutionName",
                        "AttributeType": "S"
                    }
                ],
                "KeySchema": [
                    {
                        "AttributeName": "SolutionName",
                        "KeyType": "HASH"
                    }
                ],
                "ProvisionedThroughput": {
                    "ReadCapacityUnits": {
                        "Ref": "ReadCapacityUnits"
                    },
                    "WriteCapacityUnits": {
                        "Ref": "WriteCapacityUnits"
                    }
                },
                "TableName": {
                    "Ref": "DynamoDBTableName"
                }
            },
            "Type": "AWS::DynamoDB::Table"
        },
        "SolutionHelperRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": "lambda.amazonaws.com"
                            },
                            "Action": "sts:AssumeRole"
                        }
                    ]
                },
                "Path": "/",
                "Policies": [
                    {
                        "PolicyName": "Solution_Helper_Permissions",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "logs:CreateLogGroup",
                                        "logs:CreateLogStream",
                                        "logs:PutLogEvents"
                                    ],
                                    "Resource": "arn:aws:logs:*:*:log-group:/aws/lambda/*"
                                },
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "dynamodb:PutItem"
                                    ],
                                    "Resource": [
                                        "arn:aws:dynamodb:*:*:table/*"
                                    ]
                                },
                                {
                                    "Effect": "Allow",
                                    "Action": [
                                        "lambda:AddPermission",
                                        "lambda:CreateFunction",
                                        "lambda:DeleteFunction",
                                        "lambda:GetFunction",
                                        "lambda:UpdateFunctionCode",
                                        "lambda:UpdateFunctionConfiguration",
                                        "s3:GetObject",
                                        "events:DeleteRule",
                                        "events:DisableRule",
                                        "events:EnableRule",
                                        "events:PutEvents",
                                        "events:PutRule",
                                        "events:PutTargets",
                                        "events:RemoveTargets",
                                        "events:ListTargetsByRule",
                                        "iam:PassRole"
                                    ],
                                    "Resource": "*"
                                }
                            ]
                        }
                    }
                ]
            }
        },
        "SolutionHelper": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Handler": "solution-helper.lambda_handler",
                "Role": {
                    "Fn::GetAtt": [
                        "SolutionHelperRole",
                        "Arn"
                    ]
                },
                "Description": "This function creates a CloudFormation custom lambda resource that writes parameters into DynamoDB table.",
                "Code": {
                    "S3Bucket": {
                        "Fn::Join": [
                            "",
                            [
                                "solutions-",
                                {
                                    "Ref": "AWS::Region"
                                }
                            ]
                        ]
                    },
                    "S3Key": "library/solution-helper/v1/solution-helper.zip"
                },
                "Runtime": "python2.7",
                "Timeout": "120"
            }
        },
        "PutDdbData": {
            "Type": "Custom::PutDDBData",
            "Properties": {
                "ServiceToken": {
                    "Fn::GetAtt": [
                        "SolutionHelper",
                        "Arn"
                    ]
                },
                "StoreInDDB": {
                    "Fn::Join": [
                        "",
                        [
                            "{ 'TableName' : '",
                            {
                                "Ref": "CreateParamDDB"
                            },
                            "', ",
                            "'Item': {",
                            "'CustomTagName': {'S': '",
                            {
                                "Ref": "CustomTagName"
                            },
                            "'},",
                            "'SolutionName': {'S': 'EC2Scheduler'},",
                            "'DefaultStartTime': {'S': '",
                            {
                                "Ref": "DefaultStartTime"
                            },
                            "'},",
                            "'DefaultStopTime': {'S': '",
                            {
                                "Ref": "DefaultStopTime"
                            },
                            "'},",
                            "'SendAnonymousData': {'S': '",
                            {
                                "Ref": "SendAnonymousData"
                            },
                            "'},",
                            "'CloudWatchMetrics': {'S': '",
                            {
                                "Ref": "CloudWatchMetrics"
                            },
                            "'},",
                            "'UUID': {'S': '",
                            {
                                "Fn::GetAtt": [
                                    "CreateUniqueID",
                                    "UUID"
                                ]
                            },
                            "'},",
                            "'DefaultDaysActive': {'S': '",
                            {
                                "Ref": "DefaultDaysActive"
                            },
                            "'}",
                            "}",
                            "}"
                        ]
                    ]
                },
                "DependsOn": [
                    "CreateUniqueID",
                    "CreateParamDDB"
                ]
            }
        },
        "CreateUniqueID": {
            "Type": "Custom::CreateUUID",
            "Properties": {
                "ServiceToken": {
                    "Fn::GetAtt": [
                        "SolutionHelper",
                        "Arn"
                    ]
                },
                "Region": {
                    "Ref": "AWS::Region"
                },
                "CreateUniqueID": "true",
                "DependsOn": [
                    "SolutionHelper"
                ]
            }
        },
        "ScheduledRule": {
            "Type": "AWS::Events::Rule",
            "Properties": {
                "Description": "Rule to trigger EC2Scheduler function on a schedule",
                "ScheduleExpression": {
                    "Ref": "Schedule"
                },
                "State": "ENABLED",
                "Targets": [
                    {
                        "Arn": {
                            "Fn::GetAtt": [
                                "ec2SchedulerOptIn",
                                "Arn"
                            ]
                        },
                        "Id": "TargetFunctionV1"
                    }
                ]
            }
        },
        "PermissionForEventsToInvokeLambda": {
            "Type": "AWS::Lambda::Permission",
            "Properties": {
                "FunctionName": {
                    "Ref": "ec2SchedulerOptIn"
                },
                "Action": "lambda:InvokeFunction",
                "Principal": "events.amazonaws.com",
                "SourceArn": {
                    "Fn::GetAtt": [
                        "ScheduledRule",
                        "Arn"
                    ]
                }
            }
        }
    },
    "Outputs": {
        "UUID": {
            "Description": "Newly created random UUID.",
            "Value": {
                "Fn::GetAtt": [
                    "CreateUniqueID",
                    "UUID"
                ]
            }
        },
        "DDBTableName": {
            "Description": "DynamoDB Table Name",
            "Value": {
                "Ref": "CreateParamDDB"
            }
        }
    }
}

######################################################################################################################
#  Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.                                           #
#                                                                                                                    #
#  Licensed under the Amazon Software License (the "License"). You may not use this file except in compliance        #
#  with the License. A copy of the License is located at                                                             #
#                                                                                                                    #
#      http://aws.amazon.com/asl/                                                                                    #
#                                                                                                                    #   
#  or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES #
#  OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions    #
#  and limitations under the License.                                                                                #
######################################################################################################################


import boto3
import datetime
import json
from urllib2 import Request
from collections import Counter

def putCloudWatchMetric(region, instance_id, instance_state):
    
    cw = boto3.client('cloudwatch')

    cw.put_metric_data(
        Namespace='EC2Scheduler',
        MetricData=[{
            'MetricName': instance_id,
            'Value': instance_state,

            'Unit': 'Count',
            'Dimensions': [
                {
                    'Name': 'Region',
                    'Value': region
                }
            ]
        }]
        
    )

def lambda_handler(event, context):

    print "Running EC2 Scheduler"

    ec2 = boto3.client('ec2')
    cf = boto3.client('cloudformation')
    outputs = {}
    stack_name = context.invoked_function_arn.split(':')[6].rsplit('-', 2)[0]
    response = cf.describe_stacks(StackName=stack_name)
    for e in response['Stacks'][0]['Outputs']:
        outputs[e['OutputKey']] = e['OutputValue']
    ddbTableName = outputs['DDBTableName']

    awsRegions = ec2.describe_regions()['Regions']
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(ddbTableName)
    response = table.get_item(
        Key={
            'SolutionName': 'EC2Scheduler'
        }
    )
    item = response['Item']



    # Reading Default Values from DynamoDB
    customTagName = str(item['CustomTagName'])
    customTagLen = len(customTagName)
    defaultStartTime = str(item['DefaultStartTime'])
    defaultStopTime = str(item['DefaultStopTime'])
    defaultTimeZone = 'utc'
    defaultDaysActive = str(item['DefaultDaysActive'])
    sendData = str(item['SendAnonymousData']).lower()
    createMetrics = str(item['CloudWatchMetrics']).lower()
    UUID = str(item['UUID'])
    TimeNow = datetime.datetime.utcnow().isoformat()
    TimeStamp = str(TimeNow)

    # Declare Dicts
    regionDict = {}
    allRegionDict = {}
    regionsLabelDict = {}
    postDict = {}  
    
    for region in awsRegions:
        try:
            # Create connection to the EC2 using Boto3 resources interface
            ec2 = boto3.resource('ec2', region_name=region['RegionName'])

            awsregion = region['RegionName']
            now = datetime.datetime.now().strftime("%H%M")
            nowMax = datetime.datetime.now() - datetime.timedelta(minutes=45)
            nowMax = nowMax.strftime("%H%M")
            nowDay = datetime.datetime.today().strftime("%a").lower()

            # Declare Lists
            startList = []
            stopList = []
            runningStateList = []
            stoppedStateList = []

            # List all instances
            instances = ec2.instances.all()

            print "Creating", region['RegionName'], "instance lists..."

            for i in instances:

                for t in i.tags:
                    if t['Key'][:customTagLen] == customTagName:

                        ptag = t['Value'].split(";")

                        # Split out Tag & Set Variables to default
                        default1 = 'default'
                        default2 = 'true'
                        startTime = defaultStartTime
                        stopTime = defaultStopTime
                        timeZone = defaultTimeZone
                        daysActive = defaultDaysActive
                        state = i.state['Name']
                        itype = i.instance_type

                        # Post current state of the instances
                        if createMetrics == 'enabled':
                            if state == "running":
                                putCloudWatchMetric(region['RegionName'], i.instance_id, 1)
                            if state == "stopped":
                                putCloudWatchMetric(region['RegionName'], i.instance_id, 0)

                        # Parse tag-value
                        if len(ptag) >= 1:
                            if ptag[0].lower() in (default1, default2):
                                startTime = defaultStartTime
                            else:
                                startTime = ptag[0]
                                stopTime = ptag[0]
                        if len(ptag) >= 2:
                            stopTime = ptag[1]
                        if len(ptag) >= 3:
                            timeZone = ptag[2].lower()
                        if len(ptag) >= 4:
                            daysActive = ptag[3].lower()

                        isActiveDay = False

                        # Days Interpreter
                        if daysActive == "all":
                            isActiveDay = True
                        elif daysActive == "weekdays":
                            weekdays = ['mon', 'tue', 'wed', 'thu', 'fri']
                            if (nowDay in weekdays):
                                isActiveDay = True
                        else:
                            daysActive = daysActive.split(",")
                            for d in daysActive:
                                if d.lower() == nowDay:
                                    isActiveDay = True

                        # Append to start list
                        if startTime >= str(nowMax) and startTime <= str(now) and \
                                isActiveDay == True and state == "stopped":
                            startList.append(i.instance_id)
                            print i.instance_id, " added to START list"
                            if createMetrics == 'enabled':
                                putCloudWatchMetric(region['RegionName'], i.instance_id, 1)

                        # Append to stop list
                        if stopTime >= str(nowMax) and stopTime <= str(now) and \
                                isActiveDay == True and state == "running":
                            stopList.append(i.instance_id)
                            print i.instance_id, " added to STOP list"
                            if createMetrics == 'enabled':
                                putCloudWatchMetric(region['RegionName'], i.instance_id, 0)

                        if state == 'running':
                            runningStateList.append(itype)
                        if state == 'stopped':
                            stoppedStateList.append(itype)

            # Execute Start and Stop Commands
            if startList:
                print "Starting", len(startList), "instances", startList
                ec2.instances.filter(InstanceIds=startList).start()
            else:
                print "No Instances to Start"

            if stopList:
                print "Stopping", len(stopList) ,"instances", stopList
                ec2.instances.filter(InstanceIds=stopList).stop()
            else:
                print "No Instances to Stop"


            # Built payload for each region
            if sendData == "yes":
                countRunDict = {}
                typeRunDict = {}
                countStopDict = {}
                typeStopDict = {}
                runDictType = {}
                stopDictType = {}   
                runDict = dict(Counter(runningStateList))
                for k, v in runDict.iteritems():
                    countRunDict['Count'] = v
                    typeRunDict[k] = countRunDict['Count']

                stopDict = dict(Counter(stoppedStateList))
                for k, v in stopDict.iteritems():
                    countStopDict['Count'] = v
                    typeStopDict[k] = countStopDict['Count']

                runDictType['instance_type'] = typeRunDict
                stopDictType['instance_type'] = typeStopDict

                typeStateSum = {}
                typeStateSum['running'] = runDictType
                typeStateSum['stopped'] = stopDictType
                StateSum = {}
                StateSum['instance_state'] = typeStateSum
                regionDict[awsregion] = StateSum
                allRegionDict.update(regionDict)
            
        except Exception as e:
            print ("Exception: "+str(e))
            continue

    # Build payload for the account
    if sendData == "yes":
        regionsLabelDict['regions'] = allRegionDict
        postDict['Data'] = regionsLabelDict
        postDict['TimeStamp'] = TimeStamp
        postDict['Solution'] = 'SO0002'
        postDict['UUID'] = UUID
        # API Gateway URL to make HTTP POST call
        url = 'https://metrics.awssolutionsbuilder.com/generic'
        data=json.dumps(postDict)
        headers = {'content-type': 'application/json'}

        req = Request(url, data, headers)


No comments:

Post a Comment