LoginSignup
0
0

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

Last updated at Posted at 2023-07-30

概要

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

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件の実行中の...」と表示されています。

2023073001.jpg

実行後

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

2023073002.jpg

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0