Auto Start/Stop EC2 with AWS EventBridge and Lambda for Cost Saving

AWS, EC2, Cost Saving, Automation, Serverless

Main project image

TL;DR

AWS EC2 Charged Rules

AWS does not charged you when EC2 instance are stopped. You can stop EC2 resources when they are not being used. Most of Dev/Staging resources do not need to run 24/7. If you start and stop them during business hours. It can save a lot of money. Take a look at the comparison summarizing the costs associated with each scenario for running a t2.micro EC2 instance with an automated start/stop configuration versus running it continuously (24/7).

Cost ComponentInstance Running 24/7Automated Start/Stop Configuration
EC2 Hourly Rate$0.0116/hour$0.0116/hour
Total Running Hours per Month720 hours160 hours
Monthly Cost for EC2 Instance$8.352$1.856
EBS Volume Size8 GiB8 GiB
EBS Monthly Cost$0.80$0.80
Total Monthly Cost$9.152$2.656
Savings Amount-$6.496
Percentage Savings-71%

Now, you can save cost around 70% for your dev/staging environment.

Automation with AWS Services

I decided to use these AWS services for

Workflow

Get Started

Deploy EC2 with AWS CloudFormation

First, we will create EC2 instance with t2.micro type for demonstrate. I will create instance in my Thailand Region.

AWSTemplateFormatVersion: '2010-09-09'
Description: EC2 instance with auto start/stop during business hours

Parameters:
  InstanceType:
    Type: String
    Default: t2.micro
    Description: EC2 instance type
  KeyName:
    Type: String
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instance.

Resources:
  # EC2 Instance
  MyEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: ami-0c55b159cbfafe1f0
      KeyName: !Ref KeyName
      IamInstanceProfile: !Ref EC2InstanceProfile

Outputs:
  InstanceId:
    Description: The Instance ID of the EC2 instance.
    Value: !Ref MyEC2Instance  

Run CloudFormation CLI to deploy and then you will see EC2 created in AWS Management Console

Setup Lambda Function for Start/Stop EC2

We can use AWS-SDK with AWS Lambda to console start/stop on our EC2. I decided to use NodeJS. First, we create folder called lambda_functions and run npm init than create 2 file for each start/stop function.

πŸ“lambda_functions
└── package.json
└── startInstance.js
└── stopInstance.js

with content in package.json

{
  "name": "dev-ec2-start-stop-lambda-function",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "@aws-sdk/client-ec2": "^3.0.0"
  }
}

and then startInstance.js

import { EC2Client, StartInstancesCommand } from '@aws-sdk/client-ec2';
const ec2Client = new EC2Client({ region: 'ap-southeast-7' });

export const handler = async (event) => {
    const command = new StartInstancesCommand({ InstanceIds: [event.instance_id] });
    await ec2Client.send(command);
    return `Started your instance: ${event.instance_id}`;
};

with stopInstance.js

import { EC2Client, StopInstancesCommand } from '@aws-sdk/client-ec2';
const ec2Client = new EC2Client({ region: 'ap-southeast-7' });

export const handler = async (event) => {
    const command = new StopInstancesCommand({ InstanceIds: [event.instance_id] });
    await ec2Client.send(command);
    return `Stopped your instance: ${event.instance_id}`;
};

Next, zip folder and upload it to an S3 Bucket. We will use it in next CloudFormation Template.

Setup Event Bridge with CloudFormation

Now we modify our CloudFormation template to Add EventBridge to invoke Lambda function to start/stop within during business hour.

AWSTemplateFormatVersion: '2010-09-09'
Description: EC2 instance with auto start/stop during business hours in Thailand timezone

Parameters:
  InstanceType:
    Type: String
    Default: t4g.nano
    Description: EC2 instance type
  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instance.
  S3Bucket:
    Type: String
    Description: S3 bucket where Lambda code is stored
  S3Key:
    Type: String
    Description: S3 key for the Lambda code zip file

Resources:
  DevEC2Instance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: ami-08f3e969e66cd963a
      KeyName: !Ref KeyName

  DevSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable SSH access
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: '22'
          ToPort: '22'
          CidrIp: 0.0.0.0/0

  LambdaStartFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: './lambda_functions/startInstance.handler'
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        S3Bucket: !Ref S3Bucket
        S3Key: !Ref S3Key
      Runtime: nodejs18.x
      Timeout: 30

  LambdaStopFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: './lambda_functions/stopInstance.handler'
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        S3Bucket: !Ref S3Bucket
        S3Key: !Ref S3Key
      Runtime: nodejs18.x
      Timeout: 30

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: EC2StartStopPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:StartInstances
                  - ec2:StopInstances
                Resource: '*'

  StartInstanceRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: 'cron(0 1 * * ? *)'  # 08:00 UTC (Thailand Time)
      Targets:
        - Arn: !GetAtt LambdaStartFunction.Arn
          Id: "StartEC2Instance"
          Input: !Sub '{"instance_id": "${DevEC2Instance}"}'

  StopInstanceRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: 'cron(0 11 * * ? *)'  # 18:00 UTC (Thailand Time)
      Targets:
        - Arn: !GetAtt LambdaStopFunction.Arn
          Id: "StopEC2Instance"
          Input: !Sub '{"instance_id": "${DevEC2Instance}"}'

  EventRulePermissionStart:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref LambdaStartFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt StartInstanceRule.Arn

  EventRulePermissionStop:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref LambdaStopFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt StopInstanceRule.Arn

Outputs:
  InstanceId:
    Description: "Instance ID of the newly created EC2 instance"
    Value: !Ref DevEC2Instance

Conclusion

After we apply CloudFormation Template. Our EC2 Instance will start/stop automatically during business hour we config in Amazon EventBridge. This can provide you with about 70% cost saving for resources that do not need to run 24/7 like dev/staging environment.

References