Skip to main content

Command Palette

Search for a command to run...

EventBridge Scheduler- Start/Stop ECS instance

Updated
EventBridge Scheduler- Start/Stop ECS instance
R

Enterprise Architect with 20+ years of experience. passionate about tech. Writing and sharing a knowledge is my Mantra.

As I mentioned in my prior post, we used the ECS Fargate instance in my organization. During off hours, we shut down the ECS Fargate instance every day in our lower environment to cut costs.

Here is a prior article where I have explained about Start/Stop EC2 Instnace. This is a simple pattern that starts and stops the ECS Fargate instance using Universal Target.

Important:- This code will shut down 1 ECS instance as API doesn't support multiple ones. In a future article, I will show you how to shut down multiple instances using Step Functions.

API Target and Parameters.

Target :- arn:aws:scheduler:::aws-sdk:ecs:updateService

{
  "Service": "ecs-service",
  "Cluster": "MyCluster",
  "DesiredCount": 0
}

This example uses AWS CLI and CDK (TypeScript). Assuming you already Bootstrapping your account.

Here is the main stack which creates the nested stacks of role, ECSStart and ECSStop.

import {Stack, StackProps} from 'aws-cdk-lib';
import {Construct} from 'constructs';
import {SchedulesRole} from "./scheduler-role";
import {ECSStart} from "./ecs-start";
import {ECSStop} from "./ecs-stop";

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


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

    // Create ECSStart schedules
    const ecsStart = new ECSStart(this,"EcsStart", {
      roleArn: schedulesRole.roleArn,
    })

    // Create ECSStop schedules
    const ecsStop = new ECSStop(this,"EcsStop", {
      roleArn: schedulesRole.roleArn,
    })

  }
}

Give minimal permission to start and stop ECS 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-ecs-start-stop", {
          assumedBy: new  ServicePrincipal('scheduler.amazonaws.com'),
          roleName: "scheduler-ecs-start-stop"
        })

        // Add policy
        this._role.addToPolicy(  new PolicyStatement( {
            sid: 'ECSStartStopPermissions',
            effect: Effect.ALLOW,
            actions: [
                "ecs:List*",
                "ecs:Describe*",
                "ecs:UpdateService"
            ],
            resources: ["*"], //Give the least privileges
        }))
    }

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

Create an ECS start schedule. ECS instance will start at 8 AM CST time.

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 ECSStartProps extends cdk.NestedStackProps {
    roleArn: string
}

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

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

        // Start ECS Instance 8 am Central Time
        new CfnSchedule(this,"ecs-start-scheduler", {
            name: "ecs-start-scheduler",
            flexibleTimeWindow: {
                mode: "OFF"
            },
            scheduleExpression: "cron(0 8 ? * * *)",
            scheduleExpressionTimezone: 'America/Chicago',
            description: 'Event that start ECS instances',
            target: {
                arn: 'arn:aws:scheduler:::aws-sdk:ecs:updateService',
                roleArn: props.roleArn,
                input: JSON.stringify
                    ({ 
                       Service: "ecs-service", 
                       Cluster: "MYCluster", 
                       DesiredCount: 1 
                    }),                           
            },
        });
    }
}

Create an ECS stop schedule.

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 ECSSopProps extends cdk.NestedStackProps {
    roleArn: string
}

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

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

        // Start ECS Instance 8 am Central Time
        new CfnSchedule(this,"ecs-stop-scheduler", {
            name: "ecs-stop-scheduler",
            flexibleTimeWindow: {
                mode: "OFF"
            },
            scheduleExpression: "cron(0 20 ? * * *)",
            scheduleExpressionTimezone: 'America/Chicago',
            description: 'Event that start ECS instances',
            target: {
                arn: 'arn:aws:scheduler:::aws-sdk:ecs:updateService',
                roleArn: props.roleArn,
                input: JSON.stringify(
                  { 
                    Service: 'ecs-service', 
                    Cluster: 'MyCluster', 
                    DesiredCount: 0 
                  }),
            },
        });
    }
}

Caution:- Please make sure about your ECS Auto Scaling policy. In my case, ECS instances were kept recycling during start/stop due to the auto-scaling policy was not configured properly. This caused Docker image to keep downloading and costing more $$$.

Deployment Instructions:-

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

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

3) modified ecs-service name and cluster name ecs-start.ts and ecs-stop.ts

4) npm install

5) 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.

D
Devin2y ago

I'm not sure if something has changed, but I can't get Terraform to accept ecs:UpdateService as a valid target.

"The api UpdateService is not valid for the service aws-sdk:ecs"

R
Rakesh2y ago

Not sure about Terraform but we use Start/Stop our environment everyday with UpdateService

B
Ben Force3y ago

Thank you for sharing Rakesh!

I've been wanting to try out EventBridge Scheduler. I'm glad to see it takes the timezone into consideration!

R
Rakesh3y ago

Yes it does. It's really awesome. Please try and shoot me message if you have any question. I am sure you will love it..