Automatic starting and stopping of AWS EC2 instances with Lambda and CloudWatch

I needed to figure out a way to start/stop instances automatically during certain periods. The obvious way is Lambda, but how to do it. We wanted some instances to run from Monday to Friday, and to start at 7am and stop at 5pm.

Luckily there is a library that abstracts everything you need for starting and stopping your instances. And coupled with Lambda and CloudWatch we can easily accomplish what we want.

IAM Role

First thing let’s create a new IAM role. The role is defined bellow, so we need some things so we can create logs and allow our Lambda functions to be able to start/stop an instance, with the following steps.

  • Go to IAM Management console
  • Roles
  • Create New Role
    • Role Name: lambda_start_stop_ec2
  • Role Type
    • AWS Service Roles: AWS Lambda
  • Create role
  • Edit lambda_start_stop_ec2 role, and create a new custom inline policy with the json contents bellow.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:Start*",
        "ec2:Stop*"
      ],
      "Resource": "*"
    }
  ]
}

Lambda

We need to create 2 lambda functions this, and if you really want you can rework both functions into one.

So for both lambda functions you create a new Python Lambda function, with the respective code and existing role lambda_start_stop_ec2, and we have our functions that can start and stop our instances

Start Instance

This function is responsible for starting the instances.

import boto3

def lambda_handler(event, context):

    if not bool(event):
        raise Exception('No event defined!')
        
    instances = event.get("instances")
    region = event.get("region")
        
    if not bool(instances):
        raise Exception('Instances are not defined!')
        
    if not type(instances) is list:
        raise Exception('Instances is not a list!')
        
    if not bool(instances):
        raise Exception('No instances defined')

    if not bool(region):
        raise Exception('Region is not defined!')

    ec2 = boto3.client('ec2', region_name=region)
    ec2.start_instances(InstanceIds=instances)
        
    for instance in instances:
         print "Started {} instance in {} region".format(instance, region)

Stop Instance

This function is responsible for stopping the instances

import boto3

def lambda_handler(event, context):

    if not bool(event):
        raise Exception('No event defined!')
        
    instances = event.get("instances")
    region = event.get("region")       
    
    if not bool(instances):
        raise Exception('Instances are not defined!')
        
    if not type(instances) is list:
        raise Exception('Instances is not a list!')
        
    if not bool(instances):
        raise Exception('No instances defined')

    if not bool(region):
        raise Exception('Region is not defined!')

    ec2 = boto3.client('ec2', region_name=region)
    ec2.stop_instances(InstanceIds=instances)
        
    for instance in instances:
         print "Started {} instance in {} region".format(instance, region)

CloudWatch

Create both rules for starting and stopping the instances you have defined.

When creating new rule, set Event selector to Schedule, and select how and when to trigger it. You can add as many targets as you want to a rule, for different regions and so on.

StartInstances

  • New target: Lambda function
  • Function name: StartEC2Instances
  • Configure input
    • Constant (JSON)
{
  "region": "us-east-1",
  "instances": [
    "i-1e91dd10"
  ]
}

StopInstances

  • New target: Lambda function
  • Function name: StopEC2Instances
  • Configure input
    • Constant (JSON)
{
  "region": "us-east-1",
  "instances": [
    "i-1e91dd10",
    "i-1e95235f",
    "i-1e92235f"    
  ]
}

Logs

Now in the logs you can see when the instances have been started/stopped

/aws/lambda/StartEC2Instances

START RequestId: b1b0176b-ae46-11e6-b8f8-c9b72337b2e1 Version: $LATEST
Started i-1e91dd10 instance in us-east-1 region
END RequestId: b1b0176b-ae46-11e6-b8f8-c9b72337b2e1
REPORT RequestId: b1b0176b-ae46-11e6-b8f8-c9b72337b2e1	Duration: 0.27 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 18 MB	

/aws/lambda/StopEC2Instances

START RequestId: da073d48-ae46-11e6-a365-eb4b71a100af Version: $LATEST
Stopped i-1e91dd10 instance in us-east-1 region
Stopped i-1e95235f instance in us-east-1 region
Stopped i-1e92235f instance in us-east-1 region
END RequestId: da073d48-ae46-11e6-a365-eb4b71a100af
REPORT RequestId: da073d48-ae46-11e6-a365-eb4b71a100af	Duration: 0.32 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 14 MB	

And there we go, we have a working system that can start/instances whenever we want.