概要
カスタム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件の実行中の...」という表示に変わります。

