EventBridge Scheduler- Start/Stop EC2 instance

EventBridge Scheduler- Start/Stop EC2 instance

It has been always challenging to bring down Cloud costs. In my organization, we have EC2, RDS, and ECS instances that we can shut down during off hours in our lower environment. This is a simple pattern that starts and stops ec2 instances using Universal Target. Using a universal target we can call the underlying AWS API without writing a single line of code using AWS SDK. As of now, it's having more than 270+ API calls.

This example uses AWS CLI, and CDK (Typescript). Assuming you already Bootstrapping your account to deploy the CDK app.

Here is the main stack which creates nested stacks of role, EC2Start and EC2Stop.

import {Stack, StackProps} from 'aws-cdk-lib';
import {Construct} from 'constructs';
import {SchedulesRole} from "./scheduler-role";
import {Ec2Start} from "./ec2-start";
import {Ec2Stop} from "./ec2-stop";

export class EC2InstanceStartStopStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);


    // Create Role
    const schedulesRole = new SchedulesRole(this,"SchedulerRoleStack")

    // Create EC2Start schedules
    const ec2Start = new Ec2Start(this,"Ec2Start", {
      roleArn: schedulesRole.roleArn,
    })

    // Create EC2Stop schedules
    const ec2Stop = new Ec2Stop(this,"Ec2Stop", {
      roleArn: schedulesRole.roleArn,
    })

  }
}

Give minimal permission to start/stop EC2 instances.

import { StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import {Effect, PolicyStatement, Role, ServicePrincipal} from "aws-cdk-lib/aws-iam";

export class SchedulesRole extends cdk.NestedStack  {
    private readonly _role: Role;
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props);


        // Add scheduler assumeRole
        this._role  = new Role(this,  "scheduler-ec2-start-stop", {
           assumedBy: new ServicePrincipal('scheduler.amazonaws.com'),
            roleName: "scheduler-ec2-start-stop"
        })

        // Add policy
        this._role.addToPolicy(  new PolicyStatement( {
            sid: 'EC2StartStopPermissions',
            effect: Effect.ALLOW,
            actions: [
                "ec2:DescribeInstances",
                "ec2:StartInstances",
                "ec2:StopInstances"
            ],
            resources: ["*"], //Give the least privileges
        }))
    }

    get roleArn(): string {
        return this._role.roleArn;
    }
}

Caution:- if the underlying EBS volume attached to EC2 is encrypted, please make sure you give appropriate KMS permission in the above policy.

Create an Ec2Start schedule. This will start all EC2 Instances at 8 AM CST time. AWS EventScheudler automatically adjusts DST time. See AWS StartInstances API

import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import { Role } from "aws-cdk-lib/aws-iam";
import {CfnSchedule} from "aws-cdk-lib/aws-scheduler";

interface Ec2StartProps extends cdk.NestedStackProps {
    roleArn: string
}

export class Ec2Start extends cdk.NestedStack {
    private readonly role: Role;

    constructor(scope: Construct, id: string, props: Ec2StartProps) {
        super(scope, id, props);

        // Start all EC2 Instance 8 am Central Time
        new CfnSchedule(this,"ec2-start-scheduler", {
            name: "ec2-start-scheduler",
            flexibleTimeWindow: {
                mode: "OFF"
            },
            scheduleExpression: "cron(0 8 ? * * *)",
            scheduleExpressionTimezone: 'America/Chicago',
            description: 'Event that start EC2 instances',
            target: {
                arn: 'arn:aws:scheduler:::aws-sdk:ec2:startInstances',
                roleArn: props.roleArn,
               input: JSON.stringify(
               { InstanceIds:['i-05c757e84518Z1234',
                              'i-0e8cf751ca6dD9678']}),
            },
        });
    }
}

Create an EC2Stop schedule. See AWS StopInstances API

import { Construct } from 'constructs';
import * as cdk from 'aws-cdk-lib';
import { Role } from "aws-cdk-lib/aws-iam";
import {CfnSchedule} from "aws-cdk-lib/aws-scheduler";

interface Ec2SopProps extends cdk.NestedStackProps {
    roleArn: string
}

export class Ec2Stop extends cdk.NestedStack {
    private readonly role: Role;

    constructor(scope: Construct, id: string, props: Ec2SopProps) {
        super(scope, id, props);

        // Start all EC2 Instance 8 am Central Time
        new CfnSchedule(this,"ec2-stop-scheduler", {
            name: "ec2-stop-scheduler",
            flexibleTimeWindow: {
                mode: "OFF"
            },
            scheduleExpression: "cron(0 20 ? * * *)",
            scheduleExpressionTimezone: 'America/Chicago',
            description: 'Event that start EC2 instances',
            target: {
                arn: 'arn:aws:scheduler:::aws-sdk:ec2:stopInstances',
                roleArn: props.roleArn,
                input: JSON.stringify(
                      { InstanceIds: ['i-05c757e84518Z1234',
                                      'i-0e8cf751ca6dD9678']}),
            },
        });
    }
}

Deployment Instructions :-

1) git clone git@github.com:awsmantra/eventbridge-schedules-ec2-start-stop.git

2) cd eventbridge-schedules-ec2-start-stop

3) npm install

4) cdk deploy --all -a "npx ts-node bin/app.ts" --profile dev

Cleanup :-

cdk destroy --profile dev

You can download the source code form here.

All Complex systems that work evolved from simpler systems that worked

--Gall's Law