FYI: All of the code mentioned in this article can be found here
During one of my recent assignments, I faced a requirement to schedule the start and stop of EC2 instances. The challenge was to design a mechanism that would be user-friendly and require minimal maintenance.
The specifications outlined that users should be able to initiate the starting and stopping of a specific EC2 instance through an API, based on provided parameters. A route like /provision using the HTTP POST method was chosen for this purpose. Parameters would be passed in the request body in JSON format:
- start_time (timezone-less datetime): The date and time when the specified action should start for the EC2 instance, without considering timezones.
- end_time (timezone-less datetime): The date and time when the specified action should end for the EC2 instance, without accounting for timezones.
- instance_id (EC2 instance identifier): The unique identifier assigned to the EC2 instance.
The AWS API is sufficient for starting and stopping EC2 instances. This task can be easily achieved with a Lambda function written in Python, using the Boto3 SDK.
Now, triggering these Lambdas at specific dates and times is the next challenge. For this, the EventBridge scheduler is the perfect fit.
To implement this, our API route will create two rules within EventBridge. The first rule will activate a Lambda function to start the EC2 instance, and the second to stop it. However, there's a potential problem with this approach: What happens to the rules we create that may have already been triggered?
Not long ago, AWS introduced a solution to this dilemma – the capability to automatically delete rules post-trigger. You can read more about it in this article: https://aws.amazon.com/blogs/compute/automatically-delete-schedules-upon-completion-with-amazon-eventbridge-scheduler/
Having conceptualized our architecture, it's time to dive in. Here's a brief outline, represented as a diagram:
In this scenario, we'll primarily use AWS and Python as our programming language. For deploying our infrastructure, we'll use code, specifically Terraform.
Hands-on
Let's begin with the first step - two Lambdas: one to start and the other to stop.
Here's the Lambda function to start one or more EC2 instances:
import boto3
def lambda_handler(event, _context):
region = 'eu-west-3'
ec2 = boto3.client('ec2', region_name=region)
ec2.start_instances(InstanceIds=event["instances"])
for instance in event["instances"]:
print('Started your instances: ' + instance)
And here's the function to stop one or more EC2 instances:
import boto3
def lambda_handler(event, _context):
region = 'eu-west-3'
ec2 = boto3.client('ec2', region_name=region)
ec2.stop_instances(InstanceIds=event["instances"])
for instance in event["instances"]:
print('Stopped your instances: ' + instance)
Once our instance management code is set up, the next task is creating our rules in EventBridge. We'll utilize a third Lambda function for this. This function will generate these rules using the AWS SDK, eliminating the need for manual intervention.
import json
import os
import boto3
def lambda_handler(event, _context):
body = json.loads(event["body"])
region = os.environ["REGION"]
start_time = body["start_time"]
end_time = body["end_time"]
ec2_server_id = body["ec2_instances_id"]
scheduler = boto3.client('scheduler', region_name=region)
scheduler.create_schedule(
ActionAfterCompletion="DELETE",
FlexibleTimeWindow={"Mode": "OFF"},
Name="ec2-schedule-start-" + str(ec2_server_id),
ScheduleExpression="at(" + start_time + ")",
ScheduleExpressionTimezone="Europe/Paris",
Target={
"Arn": os.environ["START_EC2_LAMBDA_ARN"],
"RoleArn": os.environ["EXECUTE_SCHEDULE_ROLE_ARN"],
"Input": "{\"instances\": [\"" + ec2_server_id + "\"]}"
},
)
scheduler.create_schedule(
ActionAfterCompletion="DELETE",
FlexibleTimeWindow={"Mode": "OFF"},
Name="ec2-schedule-stop-" + str(ec2_server_id),
ScheduleExpression="at(" + end_time + ")",
ScheduleExpressionTimezone="Europe/Paris",
Target={
"Arn": os.environ["STOP_EC2_LAMBDA_ARN"],
"RoleArn": os.environ["EXECUTE_SCHEDULE_ROLE_ARN"],
"Input": "{\"instances\": [\"" + ec2_server_id + "\"]}"
},
)
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({
'success': True
}),
"isBase64Encoded": False
}
For the Terraform code, please refer to this GitHub link where I've made my code public. Using this, you can deploy all components to your AWS account (follow the README for instructions).
After deployment, you'll have an API in the API Gateway with a /provision route. You can access this with a POST request, using the following body:
{
“ec2_instances_id“: “xxxxxxxxxx“,
“start_time”: “2023-04-01T05:00:30”,
“end_time”: “2023-04-01T07:00:30”
}
This API call facilitates the creation of two rules within the EventBridge Scheduler. One rule will start your application at the time specified in the start_time field, while the other will halt your instance at the time indicated in the end_time field.
This streamlined setup makes orchestrating one or many EC2 instances incredibly straightforward. API-based interaction is immensely convenient, and the automated rule cleanup post-execution is a significant advantage.
However, exercise caution. Currently, this stack doesn't manage "concurrency". For instance, if one event starts at 3:00 PM and ends at 6:00 PM, you can still have another event that begins at 5:00 PM and concludes at 8:00 PM. This overlap could result in the instance stopping at 6:00 PM during the second event. Pairing this stack with an app like a scheduler or calendar would be optimal (spoiler alert: this integration is part of my ongoing project).
And there you have it! I hope you found this article helpful!
Top comments (2)
Sometime back I wrote this one. This one without Lambda.
dev.to/awsmantra/eventbridge-sched...
This is a great solution for scheduling the start and stop of EC2 instances. The use of an API makes it user-friendly and the EventBridge scheduler makes it easy to trigger Lambda functions at specific dates and times. The ability to automatically delete rules post-trigger is a great way to avoid having to manage orphaned rules.