How to Create an AWS Lambda EC2 AutoStart Function

Matthew Caspento
Zaloni Engineering
Published in
9 min readJun 23, 2022

Automation is a wonderful thing, especially if you’re lazy and don’t want to spend more than 3 minutes doing something. In this case, I hated getting up early each morning to start my environment. So I decided to automate my morning.

Use Case:

The use case we’ll be covering in this blog is automating the start of environments in order to enable an engineer to start the work day without delay. When an engineer starts their day, they have to authenticate to AWS. Once authenticated they need to switch regions and roles to where the EC2 instance resides. Then they have to start the instance after navigating to the EC2 Management Console. Once started, an engineer would have to wait for the instance to start. In total, the engineer would have spent 3 minutes or more.

Calculated yearly, each engineer wastes about 21 hours starting their environment.

Think of the cost of an employee who is earning $60K a year working124,800 minutes. That employee is making $.48 cents per minute, so each day the employee’s wasted time is $1.44 multiply that by 260 work days you get $375.

For each employee, there's an opportunity cost of $375 a year per engineer to spend 3 mins a day to start an instance. It’s something I know a lot of us do and it’s usually in the morning getting ready for work.

Currently, we have 35 instances that were manually started from 1/23/2022 at 7 pm EST to 1/24/2022 at 9 am EST. With that in mind if those are instances just being started that comes out to be an opportunity cost of $13,125 a year. As a company grows, so will the number of employees thus increasing that cost.

Currently, I have an array of holiday dates for India & the United States. A script will check if today is in the array if so, it would skip the autoStart command. I have it set to run Monday-Friday once an hour on the hour from 12 am to 8 pm GMT. So I can limit the number of invocations from the lambda function to save on costs.

Documentation:

How to use the function: Just add the tags to the desired instance. The following syntax is required to work.

Tags Required

start_auto: True/False, 1/0, Off/On (Default to False if Tag not present)start_time: Should be 4 digits in hhmm format representing thehour and minute to start the EC2 instancehour is 24 hour time format
start_tz: Timezone to use for when interpreting start_time
Valid Time Zones from
https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
For US East use “America/New_York”
For India use “Asia/Calcutta”
start_lastdt:
Left blank for the script to update and patch
EC2 Instances will be started when all of the following are true:
- Instance is not running
- start_auto Tag is set to True (or equivalent)
- start_tz is a valid timezone
- current time hhmm >> start_time with current time converted to
start_tz timezone
AWS Permissions are assumed by the role assigned to the Lambda function
Requires ec2:DescribeInstances and ec2:StartInstances

EventBridge

Cronjob expression is: cron(0 0–20/1 ? * MON-FRI *)

IAM Role

New role & custom policy to start the instances

Step One:

Before creating the lambda function, you will want to create a new IAM role with a custom permission policy. Start by making your way to the policies list located in the IAM dashboard sidebar and create a new policy.

You can use this JSON template, just replace the region and account Id.

{
“Version”: “2012–10–17”,
“Statement”: [
{
“Effect”: “Allow”,
“Action”: [
“logs:CreateLogGroup”
],
“Resource”: “arn:aws:logs:<currentRegion>:<accountID>:*”
},
{
“Effect”: “Allow”,
“Action”: [
“logs:CreateLogStream”,
“logs:PutLogEvents”
],
“Resource”: “arn:aws:logs:<currentRegion>:<accountID>:log-group:/aws/lambda/run_autoStart:*”
},
{
“Action”: [
“ec2:StartInstances”,
“ec2:CreateTags”,
“ec2:DescribeInstances”
],
“Resource”: “*”,
“Effect”: “Allow”
}]
}

Review the policy:

Once the policy is created. We can now create an IAM role. Switch over to the Roles list and create a new role and attach the newly created policy.

Now we can create a lambda function. When doing so make sure you change the execution role to use an existing role.

Once created, we can build a trigger event rule. Just add a trigger and select the EventBridge(CloudWatch Events).

Make sure you add the expression in cron().

cron(0 3–20/1 ? * MON-FRI *)

Now before we move on to adding the code. Save yourself a headache later and edit the runtime settings. Replace the first part of the handler with whatever you named the function.

Use the name of the function you created

Once this is done we can create the function. BUT first, we need to download a pytz package so we can differentiate between timezones. You can use:

$ pip3 install pytz

You will find the package in the python folder. Make a copy of the package and export it to an empty folder where we’ll eventually make it a zip as that’s the only way to import an app. When at the folder, open a code editor so you can create a new file that will contain all the logic for the function.

Make sure the folder is structured like:

For the script you can use this:

###################################################################################
## Name: autoStart.py
## Purpose: to automatically start EC2 instances at a specific time
## each day that are tagged appropriately
##
## Tags Required:
## start_auto: True/False, 1/0, Off/On (Default to False if Tag not present)
##
## start_time: Should be 4 digits in hhmm format representing the
## hour and minute to start the EC2 instance
## hour is 24 hour time format
##
## start_tz: Timezone to use for when interpreting start_time
## Valid Time Zones from
## https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
## For US East use "America/New_York"
## For India use "Asia/Calcutta"
##
## EC2 Instances will be started when all of the following are true:
## - Instance is not running
## - start_auto Tag is set to True (or equivalent)
## - start_tz is a valid timezone
## - current time hhmm >> start_time with current time converted to
## start_tz timezone
##
## AWS Permissions are assumed by the role assigned to the Lambda function
## Requires ec2:DescribeInstances and ec2:StartInstances
##
## Python Dependencies:
## Package pytz must be installed locally and then uploaded into Lambda
## using the zip upload option.
## From same directly as this source file use:
## install --target ./package pytz --system
##
## This install the pytz package locally. Again from the same directory
## as this source file use:
## zip -r9 lambda_package.zip . --exclude *.yml
## The resulting zip can be uploaded into Lambda
##
####################################################################################
import json
import os
import boto3
import datetime
from datetime import datetime, date
from dateutil.parser import parse
import sys
sys.path.append(r'./package')
import pytz
#############################################################################
## sts_assume_role
## Function only required if running locally for debug purposes
#############################################################################
def sts_assume_role( sess, arn, sessionName ):
sts_connection = sess.client('sts')
assume_role_object = sts_connection.assume_role(RoleArn=arn, RoleSessionName=sessionName, DurationSeconds=3600)
## sess.credentials = assume_role_object['Credentials']
tmp_access_key = assume_role_object['Credentials']['AccessKeyId']
tmp_secret_key = assume_role_object['Credentials']['SecretAccessKey']
security_token = assume_role_object['Credentials']['SessionToken']
boto3_session = boto3.session.Session( aws_access_key_id=tmp_access_key,
aws_secret_access_key=tmp_secret_key,
aws_session_token=security_token )
return boto3_session
############################################################################
## compare date to Zaloni holidays
##
############################################################################
zaloni_holidays = [
"2022-01-02",
"2022-01-14",
"2022-01-16",
"2022-01-26",
"2022-03-18",
"2022-04-01",
"2022-04-15",
"2022-05-03",
"2022-05-30",
"2022-06-20",
"2022-07-04",
"2022-07-11",
"2022-08-15",
"2022-08-31",
"2022-09-05",
"2022-10-04",
"2022-10-05",
"2022-10-24",
"2022-11-24",
"2022-11-25",
"2022-12-23",
"2022-12-03",
"2022-12-26"
]
today = date.today().isoformat()
print("Is today a Holiday for Zaloni Employees?")
print(today in zaloni_holidays)
holiday_skip_day = today in zaloni_holidays
#############################################################################
## start_instances_for_region
##
#############################################################################
def start_instances_for_region( region ):
utc_now = pytz.utc.localize(datetime.utcnow())

## master_session = boto3.session.Session( "TBD KEY ID", "TBD KEY SECRET ID")
## mysession = sts_assume_role( master_session, "arn:aws:iam::<accountID>:role/cross-account-admin", "tempSession1" )


ec2_client = boto3.client("ec2", region_name=region)
instances = ec2_client.describe_instances( MaxResults=800)
# print( instances )
i=0
for x in instances['Reservations']:
for instance in x['Instances']:
i+=1
InstanceId = instance['InstanceId']
InstanceState = instance['State']['Name']

start_auto = False
start_time = ""
start_tz = ""
start_lastdt = ""
InstanceName = ""
if 'Tags' in instance:
for tag in instance['Tags']:

if ( tag['Key'] == 'start_auto' ):
start_auto = tag['Value']
if ( str(start_auto).lower() in ("1", "on", "true") ):
start_auto = True
else:
start_auto = False
if ( tag['Key'] == 'start_time' ):
start_time = tag['Value']
if ( tag['Key'] == 'start_tz' ):
start_tz = tag['Value']
if ( tag['Key'] == 'start_lastdt' ):
start_lastdt = tag['Value']
if ( tag['Key'] == 'Name' ):
InstanceName = tag['Value']
if ( start_auto == True and InstanceState == "stopped"):
print( "Found EC2 %s (%s) in state %s and tags [%s, %s, %s]..."%( InstanceName, InstanceId, InstanceState, start_auto, start_time, start_tz))

if ( InstanceState == "stopped" and start_auto == True and start_time.isdigit() and len(start_time) == 4):
print("..Instance %s (%s) is stopped and tagged for automatic start"%( InstanceName, InstanceId ))
#############################################################
## Convert UTC_NOW to localized now so we can compare times
## and see if we need to start this machine
#############################################################
if ( start_tz != ""):
if ( start_tz not in pytz.all_timezones ):
print("..ERROR: Invalid Timezone String (%s) Specified in start_tz Tag for Instance %s"%( start_tz, InstanceId ))
else:
localized_now = utc_now.astimezone(pytz.timezone( start_tz ))
localized_time = ( localized_now.hour * 100 ) + localized_now.minute
last_start_dt = None
try:
if ( start_lastdt != ""):
last_start_dt = datetime.strptime( start_lastdt, "%Y-%m-%d %H:%M:%S" )
except ValueError as ve:
print("Error parsing last start date/time: %s"%( str(ve) ))
except:
print("Error parsing last start date/time")
print( "..Localized Time=%s"%( localized_time ))
print( "..Start Time=%s"%( start_time ))
print( "..Last Start Time=%s"%( start_lastdt))
skip_autostart = False
if ( last_start_dt != None ):
print( "..%s %s | %s %s | %s %s"% ( last_start_dt.year, localized_now.year, last_start_dt.month, localized_now.month, last_start_dt.day, localized_now.day))
if ( last_start_dt.year == localized_now.year and last_start_dt.month == localized_now.month and last_start_dt.day == localized_now.day ):
print("..instance already started once today, skipping auto start...")
skip_autostart = True
if(holiday_skip_day == False):
if ( localized_time >= int(start_time) and skip_autostart == False):
print("..Starting instance %s (%s)..."% (InstanceName, InstanceId) )
response = ec2_client.start_instances( InstanceIds=[ InstanceId ] )
print( response )
localized_now_str = localized_now.strftime( "%Y-%m-%d %H:%M:%S" )
print("..setting start_lastdt tag value to %s"%( localized_now_str))
ec2_client.create_tags(Resources=[ InstanceId], Tags=[{'Key':'start_lastdt', 'Value':localized_now_str}])
else:
print("Skipping instance %s in state %s and start_auto=%s..."%( InstanceId, InstanceState, start_auto ))
#############################################################################
## lambda_handler
#############################################################################
def lambda_handler(event, context):

#regions = ['us-east-1', 'us-east-2', 'ap-south-1']
regions = os.environ['region_list'].replace(" ","").split(",")
print("==============\nEnvironment regions %s..."%( regions))
for region in regions:
print("==============\nChecking region %s..."%( region))
start_instances_for_region( region )
return {
'statusCode': 200,
'body': json.dumps('Successful')
}
## Uncomment when debugging locally
# lambda_handler( None, None )

You should notice 4 tags that contain start ; these tags are required on the desired EC2 instance to autoStart.

Once you create the file add it to the folder:

Once added, the next step is to zip the folder so you can upload it to the lambda function.

Make sure to review the code and everything else before proceeding. Add tags to the function itself as it is good practice.

Now it’s time to test by adding tags to instances.

Example of the tags on my instance:

Leave the start_lastdt tag blank as that will be updated by the function.

Currently the function will be invoked once an hour. For testing, I recommend changing the expression to every minute. Other than that you’re finished.

Congrats you can now officially be 3 mins lazier each day.

Future Goals:

My future goal is to have a policy set for tagging these. Possibly creating a resource group to house the tags so we can only let a specific account tag EC2’s with the group to allow autoStart. Another goal would be to implement the BambooHR API to check if employees are on PTO or out that day, or even no longer work at the company. And if true, it would not start the instance with their name. That also means you need to implement strict requirements on instance tagging, such as requiring the owner tag for id. We can clean up the instances that are not assigned as they are not being used daily. As a company gets bigger and bigger it will need to cover all the potential losses.

--

--