Optimize EC2 cost by scheduling instances using EventBridge scheduler and Lambda
Tags: aws, lambda, devops
Published: 2023-07-03 09:26:23

Scheduling EC2 instances is a crucial step in AWS cost optimization. Because EC2 instances are invoiced by the hour or second depending on their usage, running instances constantly even when they are not in use can quickly add up to excessive costs. Organizations can connect their use of EC2 instances with actual business needs by putting in place a well-thought-out scheduling strategy, ensuring that instances are only operational during necessary times. By minimizing idling time, eliminating pointless fees, and increasing the effective use of computing resources, this strategy enables significant cost reductions. Companies can successfully control their AWS spending while still meeting operational expectations by dynamically scheduling instances to turn on and off at precise times.

In one of my side hustles, I stumbled upon a situation where we need to provision some new EC2 instances for an accounting software. We already purchased some reserved instances for our web applications that need to be running 24/7 and did not want to buy any more of them. Since, the accounting software will only be used during 9-5 work hours, we decided to schedule the EC2 instances to run only during the active hours. In this post, I'll show you the easiest way (imo) to achieve this behaviour.

If you look it up on Google, you might find this [cloudformation-template](https://docs.aws.amazon.com/solutions/latest/instance-scheduler-on-aws/solution-overview.html). However, when i really took a deeper dive into this, I felt like this is quite overkill for something so simple as stopping instances for specific hours in the day. So, i decided to write my own solution using just `lambda` and `EventBridge-Scheduler`.

## Step 1: Lambda 
```python
import json
import boto3
from datetime import datetime

# operating hours are specified in Asia/BD GMT+6 time
config = {
    "frontend": {
        "instance_id": "i-0b395375beefed021b",
        "non_operating_hours": {
            "start": 2,
            "end": 6
        }
    },
    "backend": {
        "instance_id": "i-054281d348bbf103165",
        "non_operating_hours": {
            "start": 2,
            "end": 6
        }
    },
    "accounts": {
        "instance_id": "i-0fb10809264e10342",
        "non_operating_hours": {
            "start": 0,
            "end": 9
        }
    }
}

def get_instance_state(instance_id):
    ec2_client = boto3.client('ec2')
    response = ec2_client.describe_instances(InstanceIds=[instance_id])
    
    reservations = response['Reservations']
    if not reservations:
        print("No reservations found for the provided instance ID.")
        return
    
    instances = reservations[0]['Instances']
    
    if not instances:
        print("No instances found for the provided instance ID.")
        return
    
    return instances[0]['State']['Name']


def start_instance(instance_id):
    ec2_client = boto3.client('ec2')
    response = ec2_client.start_instances(InstanceIds=[instance_id])
    return response

def stop_instance(instance_id):
    ec2_client = boto3.client('ec2')
    response = ec2_client.stop_instances(InstanceIds=[instance_id])
    return response

def lambda_handler(event, context):
    current_hour = datetime.now().hour
    print(f'Executing on BD hour {current_hour}')
        
    results = []
    for instance_name, conf in config.items():
        instance_id = conf['instance_id']
        instance_state = get_instance_state(instance_id)
        
        start = conf['non_operating_hours']['start']
        end = conf['non_operating_hours']['end']
        
        if current_hour >= start and current_hour <= end:
            is_stopped = 'stop' in instance_state
            
            if not is_stopped:
                res = stop_instance(instance_id)
            
            results.append(
                {
                    'action': "Stop" if not is_stopped else f"Already {instance_state}",
                    'conf': conf
                }
            )
            
        else:
            is_running = 'run' in instance_state
            if not is_running:
                res = start_instance(instance_id)
    
            results.append(
                {
                    'action': "Start" if not is_running else f"Already {instance_state}",
                    'conf': conf
                }
            )        
            
    response = json.dumps(results)
    
    print(response)
    
    return {
        'statusCode': 200,
        'body': response
    }

```
Please note that, this lambda code may or may not be a basic chatgpt dump 🤐 and there is huge room for improvement :) Feel free to improve it yourself as needed.

## Step 2: Create EventBridge schedule
AWS EventBridge supports cron expression based schedulers which happens to be just perfect for this use case. So, we decided to use it.
We can create the schedule either using Terraform, AWS CLI or AWS console.
For example, the terraform code will look sth like this,
```hcl
module "eventbridge" {

  create_bus = false

  rules = {
    crons = {
      description         = "Trigger for EC2StartStop Lambda"
      schedule_expression = "rate(1 hours)"
    }
  }

  targets = {
    crons = [
      {
        name  = "EC2StartStopLambda-Target-Cron"
        arn   = your_lambda_function_arn
        input = jsonencode({ "job" : "cron-by-rate" })
      }
    ]
  }
```
Now, every 1 hour our scheduler will trigger the lambda function and our lambda function will determine whether an instance should be up during that hour or not. This solution takes about 5minutes to setup but has the potential to reduce your compute bills by a significant margin (in my case, we reduced about 600 USD per month).

If you found this post helpful, CLICK BELOW 👇 <a href="https://www.buymeacoffee.com/ashiqurrahman" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Beer" style="height: 80px !important;width: 240px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>