Using AWS EventBridge and Lambda to Start and Stop EC2 Instances on a Schedule
EC2 is the virtual server service in AWS. Linux servers, Windows servers, etc. These servers are used for a lot of different purposes. Many times they are used to host client facing applications which need to be accessible 24/7. Other times we use EC2 instances to do specific work for a specific period of time hence no need to be available 24/7.
The most significant part of the cost of running an EC2 instance is the uptime of the instance itself. When the instance is stopped you do not incur cost of the instance itself (though you might have other costs like storage associated with that instance for which you will be charged). If we can stop an instance when we don’t need it and then restart when we do we could potentially save a lot of money.
This article describes an implementation of a scheduled EC2 stop/start scenario using Amazon Event Bridge and Lambda services. An example of when this might be useful is when we have a Dev environment that only needs to be active for a certain period of the workday we could create a schedule that stops the instances during off hours and then restarts the instances at the beginning of the work shift.
Essentially any situation where we have a defined period for when the instances should be available would be a candidate for this type solution.
The Lambda Functions
Let’s define and write our Lambda functions. We are going to have one that stops our instances and one that starts our instances. We’ll put a tag on our instances that we want to stop and start. This tag will have the key StopStart and if the value is “true” for a specific instance then that instance will be affected by our Lambda functions.
We’ll write our Lambda functions using Python and specify Python 3.10 as our runtime. We’ll also increase the function timeouts to 1 minute to give our function time to access the information on our instances through the EC2 services. One minute is overkill, but it doesn’t really cost us anything to set it to that. I have noticed that the functions will timeout at times if we leave the timeout set to the default 3 seconds.
We’ll use the AWS Python SDK, Boto3, and the API reference to EC2 services to determine what our calls need to look like from our code. We will use the “describe_instances”, “stop_instances”, and “start_instances” client reference methods to operate upon our EC2 instances.
The Python code for our “stop-my-ec2-instance” Lambda function is as follows:
import json
import boto3
import os
print('Loading function')
def lambda_handler(event, context):
client = boto3.client('ec2')
#Find all instances tagged StopStart=true
response = client.describe_instances(
Filters=[
{
'Name': 'tag:StopStart',
'Values': [
'true',
],
},
],
)
#If no tagged instances found exit with message
if len(response['Reservations']) == 0:
return "No instances found"
inst_str = ""
#Stop each instance tagged as StopStart=true
for x in response['Reservations']:
inst_id = x['Instances'][0]['InstanceId']
inst_str = inst_str + "\'" + inst_id + "\'" + ","
stop_response = client.stop_instances(
InstanceIds=[
inst_id,
],
)
inst_str = "Stopped Instances: " + inst_str
print(inst_str)
return inst_str
The code for the “start-my-ec2-instance” function is largely the same as the stop instance function with start substituted in the appropriate places.
import json
import boto3
import os
print('Loading function')
def lambda_handler(event, context):
client = boto3.client('ec2')
#Find all instances tagged StopStart=true
response = client.describe_instances(
Filters=[
{
'Name': 'tag:StopStart',
'Values': [
'true',
],
},
],
)
#If no tagged instances found exit with message
if len(response['Reservations']) == 0:
return "No instances found"
inst_str = ""
#Stop each instance tagged as StopStart=true
for x in response['Reservations']:
inst_id = x['Instances'][0]['InstanceId']
inst_str = inst_str + "\'" + inst_id + "\'" + ","
start_response = client.start_instances(
InstanceIds=[
inst_id,
],
)
inst_str = "Started Instances: " + inst_str
print(inst_str)
return inst_str
In our describe_instances call where we get the information on all the instances in a region, we are using a filter that will return only instances that have the tag key of “StopStart” with a value of “true”. These are the only instances we want to act on.
Once we have information on our tagged instances we check the returned “Reservations” list to see if it has at least one member. If not, there are no instances to stop or start and so we leave our function.
If there are instances we process those one at a time in the for loop and call either stop_instances or start_instances depending on which function is being executed. And then we are done. No need to check to see if an instance is already running or not since we won’t get an error if we try to stop and already stopped instance or start and already started instance.
In addition to the function code you will need an execution role attached to the Lambda function to allow it to read and write EC2. Here is a link to information on how to do that:
At this point all we need is something to trigger each of these Lambda functions on a schedule. And for that we will look at Amazon EventBridge.
Before we do that though, let’s take a look at a cool Lambda feature we can use to trigger our functions with an internet browser call: Lambda Function URL.
A function URL is a dedicated HTTP(S) endpoint for your Lambda function. You can create and configure a function URL through the Lambda console or the Lambda API. When you create a function URL, Lambda automatically generates a unique URL endpoint for you. Once you create a function URL, its URL endpoint never changes. Function URL endpoints have the following format:
https://<url-id>
.lambda-url.<region>.on.aws
The Function URL is found under the Configuration section of the individual Lambda function.
If you use Auth type NONE then the function invocation is open to public access. Auth type AWS_IAM will restrict invocation IAM users and roles within the account. You can then use the “awscurl” command to invoke the function from your AWS CLI session.
Creating a Lambda Invocation Schedule in EventBridge
Amazon EventBridge is a service in AWS that has several different facets to it for creating events to be used in Event Driven Architectures. It is essentialy an event listener that you can use to trigger downstream services like Lambda. The EventBridge Scheduler is a part of the services that will allow you to create event triggers on a schedule. So, if I want to trigger a specific Lambda function every Tuesday at 6am Central Time, I can do that very easily with EventBridge. You can schedule one-time triggers or recurring triggers with a Cron-based or Rate-based schedule. I.e., 10 minutes into every hour like a Cron job or a Rate of every 10 minutes.
We’ll setup our schedule to stop the instances every day at 6pm and start them every day at 6am. Twelve hours of daily downtime on these instances will save us a lot of money of the course of a month.
This is what our start instances schedule might look like in EventBridge:
And where we would specify the Lambda function to invoke:
We could deliver a payload to our Lambda function on invocation as well, but we really don’t need it in this case since we are just starting all appropriately tagged instances in the region.
We would create a similar schedule and invocation for our stop instances function.
And then we are done. Our Lambda functions will be invoked according to the schedules we setup in EventBridge and we have to option of over-riding either with our direct Lambda Function URL that we setup for each.
Summary
There are times we want to stop EC2 instances that don’t need to run 24/7. This can help us save money on our AWS accounts and the savings can be quite significant depending on how many and which type EC2 instances we are running. In this example we use tagging on our EC2 instances to identify which instances should be stopped or started. If we ever want to exclude instances from the stop/start schedule we can just change the value of the tag to something other than “true”.