カスタムSSMドキュメント + EventBridgeでECSのスケジュール起動・停止（CDK）

# 概要
カスタム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

# サンプルコード

## bin/main.ts

```typescript: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

```typescript: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

```typescript: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サービス名を指定
            }),
          },
        ],
      });
    }
  }
}
```
:::note info
上記コードの"起動時刻"、"停止時刻"、"ECSクラスター名"、"ECSサービス名"、"起動時のタスク数"にはご利用のECS情報を入力してください。
:::

# 実行前
「デプロイとタスク」が「1/1件の実行中の...」と表示されています。

![2023073001.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143947/ba376346-dae5-177f-1f34-9655377b823c.jpeg)

# 実行後
「デプロイとタスク」が「0/0件の実行中の...」という表示に変わります。

![2023073002.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143947/7bebea0a-168c-34b4-1ca0-480b0a93d113.jpeg)