EventBridge Scheduler- Start/Stop ECS instance

Search for a command to run...

Use Case:- I am sure you all remember how much boilerplate code we used to write when we convert the DynamoDB stream to the business object. Here is a small snippet of the GoLang program. m := &EntitySearch{} for name, value := range recordI...

Use Case:- Our company has an SFTP server in the lower environment, but we don't have regular traffic on it. Our QA team only uses it once every two weeks during production release testing. If you create an AWS SFTP server, you will be charged $0.30 ...

Use Case:- We have a business use case where we need to call our nightly job for multiple tenants. Initially, I planned to use EventBridge Bus and Rules to achieve this goal. However, we discovered that EventBridge Rules has a limit of 5 targets, whi...

Little Background:- in 2019 we started moving from server to serverless and started adopting AWS Lambda + GoLang for any new development. My chief architect and current CTO Ben Pyle(Blog) came up with the GoLang pattern which we used in all our Micro...

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.