概要
カスタムSSMドキュメント + EventBridgeでECSをスケジュール起動・停止(正確には停止ではなく、ECSのタスク数を0に変更)するCDKサンプルコード(TypeScript)を紹介します。
ECSの起動・停止させるカスタムSSMドキュメントをSSMDocumentスタックで作成して、CfnOutputでEventスタックに渡してEventBridgeによるスケジュール実行させます。
開発環境やSTG環境のECSをスケジュールに従って起動・停止するだけでもコスト削減効果が見込めると思います。
ファイル構成
ファイル構成は以下です。
- bin/
- main.ts
- lib/
- SsmDocuments/
- SsmDocumentsStack.ts
- Events/
- EventsStack.ts
- SsmDocuments/
サンプルコード
bin/main.ts
bin/main.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { Environment } from 'aws-cdk-lib';
import { SsmDocumentsStack } from "../lib/SsmDocuments/SsmDocumentsStack";
import { EventsStack } from "../lib/Events/EventsStack";
if (process.env['NODE_ENV'] == null || process.env['NODE_ENV'] === '') {
throw new Error('NODE_ENV is not set');
}
if (process.env['AWS_PROFILE'] == null || process.env['AWS_PROFILE'] === '') {
throw new Error('AWS_PROFILE is not set');
}
const app = new cdk.App();
const env: Environment = {
account: '<AWSアカウント番号>',
region: '<AWSリージョン>',
};
const ssmDocuments = new SsmDocumentsStack(app, {
env: env,
});
new EventsStack(app, {
env: env,
ssmDocuments: ssmDocuments.outputs.ssmDocuments,
});
lib/SsmDocuments/SsmDocumentsStack.ts
lib/SsmDocuments/SsmDocumentsStack.ts
import * as cdk from "aws-cdk-lib";
import { CfnDocument } from "aws-cdk-lib/aws-ssm";
import { Construct } from "constructs";
/**
* 構築プロパティ。
*/
export interface SsmDocumentsProps extends cdk.StackProps {}
/**
* 構築結果。
*/
export interface SsmDocumentsOutputs {
/**
* SSMドキュメント情報。
*/
ssmDocuments: {
/**
* ECS起動向けSSMドキュメント
*/
startECSServiceDocument: CfnDocument;
/**
* ECS停止向けSSMドキュメント
*/
stopECSServiceDocument: CfnDocument;
};
}
/**
* SSMドキュメント。
*/
export class SsmDocumentsStack extends cdk.Stack {
outputs: SsmDocumentsOutputs;
constructor(scope: Construct, props: SsmDocumentsProps) {
super(scope);
// ECS起動向けSSMドキュメント
const startECSServiceDocument = new CfnDocument(
this,
"StartECSServiceDocument",
{
name: "StartECSService",
documentType: "Automation",
content: {
schemaVersion: "0.3",
description: "ECS service start automation runbook",
parameters: {
EcsClusterName: {
type: "String",
},
EcsServiceName: {
type: "String",
},
DesiredCount: {
type: "Integer",
default: 1,
},
},
mainSteps: [
{
name: "ECS",
action: "aws:executeAwsApi",
inputs: {
Service: "ecs",
Api: "UpdateService",
cluster: "{{ EcsClusterName }}", // ECSクラスター名を指定
service: "{{ EcsServiceName }}", // ECSサービス名を指定
desiredCount: "{{ DesiredCount }}", // 起動時のタスク数を指定
},
},
],
},
}
);
// ECS停止向けSSMドキュメント
const stopECSServiceDocument = new CfnDocument(
this,
"StopECSServiceDocument",
{
name: "StopECSService",
documentType: "Automation",
content: {
schemaVersion: "0.3",
description: "ECS service stop automation runbook",
parameters: {
EcsClusterName: {
type: "String",
},
EcsServiceName: {
type: "String",
},
},
mainSteps: [
{
name: "ECS",
action: "aws:executeAwsApi",
inputs: {
Service: "ecs",
Api: "UpdateService",
cluster: "{{ EcsClusterName }}", // ECSクラスター名を指定
service: "{{ EcsServiceName }}", // ECSサービス名を指定
desiredCount: 0, // 起動時のタスク数を指定(停止する場合、タスク数は常に0を指定)
},
},
],
},
}
);
this.outputs = {
ssmDocuments: {
startECSServiceDocument: startECSServiceDocument,
stopECSServiceDocument: stopECSServiceDocument,
},
};
}
}
lib/Events/EventsStack.ts
lib/Events/EventsStack.ts
import * as cdk from "aws-cdk-lib";
import {
PolicyStatement,
Role,
ServicePrincipal,
ManagedPolicy,
Effect,
} from "aws-cdk-lib/aws-iam";
import { CfnDocument } from "aws-cdk-lib/aws-ssm";
import { CfnRule } from "aws-cdk-lib/aws-events";
import { Construct } from "constructs";
/**
* 構築プロパティ。
*/
export interface EventsProps extends cdk.StackProps {
/**
* SSMドキュメント情報。
*/
ssmDocuments: {
/**
* ECS起動向けSSMドキュメント
*/
startECSServiceDocument: CfnDocument;
/**
* ECS停止向けSSMドキュメント
*/
stopECSServiceDocument: CfnDocument;
};
}
/**
* イベントを作成。
* スケジュールによるECSの起動および停止を実行。
*/
export class EventsStack extends cdk.Stack {
private readonly _props: EventsProps;
constructor(scope: Construct, props: EventsProps) {
super(scope);
// イベント向けロールを作成
const eventsRole = new Role(this, "EventsRole", {
assumedBy: new ServicePrincipal("events.amazonaws.com"),
});
// イベント向けAWSマネージドポリシーを付与
eventsRole.addManagedPolicy(
ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AmazonSSMAutomationRole"
)
);
// イベント向けポリシーを付与
eventsRole.addToPolicy(
new PolicyStatement({
actions: ["ecs:UpdateService", "ecs:DescribeServices"],
effect: Effect.ALLOW,
resources: ["*"],
})
);
// イベントを作成
if (props.ssmDocuments) {
// ECS起動イベント
new CfnRule(this, "StartECSEvent", {
description: `Start rule for ECS`,
name: "StartECSEvent",
scheduleExpression: "cron(00 00 ? * MON-FRI *)", // 起動時刻(例では平日のJST 09:00(GMT 00:00)に起動)
state: "ENABLED",
roleArn: eventsRole.roleArn,
targets: [
{
id: "StartECSEvent",
roleArn: eventsRole.roleArn,
arn: `arn:aws:ssm:${props.env?.region}:${props.env?.account}:automation-definition/${props.ssmDocuments.startECSServiceDocument.name}:$DEFAULT`,
input: JSON.stringify({
EcsClusterName: ["ECSクラスター名"], // ECSクラスター名を指定
EcsServiceName: ["ECSサービス名"], // ECSサービス名を指定
DesiredCount: [起動時のタスク数], // 起動時のタスク数を指定
}),
},
],
});
// ECS停止イベント
new CfnRule(this, "StopECSEvent", {
description: `Stop rule for ECS`,
name: "StopECSEvent",
scheduleExpression: "cron(00 13 ? * MON-FRI *)", // 停止時刻(例では平日のJST 22:00(GMT 13:00)に停止)
state: "ENABLED",
roleArn: eventsRole.roleArn,
targets: [
{
id: "StopECSEvent",
roleArn: eventsRole.roleArn,
arn: `arn:aws:ssm:${props.env?.region}:${props.env?.account}:automation-definition/${props.ssmDocuments.stopECSServiceDocument.name}:$DEFAULT`,
input: JSON.stringify({
EcsClusterName: ["ECSクラスター名"], // ECSクラスター名を指定
EcsServiceName: ["ECSサービス名"], // ECSサービス名を指定
}),
},
],
});
}
}
}
上記コードの"起動時刻"、"停止時刻"、"ECSクラスター名"、"ECSサービス名"、"起動時のタスク数"にはご利用のECS情報を入力してください。
実行前
「デプロイとタスク」が「1/1件の実行中の...」と表示されています。
実行後
「デプロイとタスク」が「0/0件の実行中の...」という表示に変わります。