LoginSignup
10
2

AWSリソースの定期起動・停止をCDKの EventBridge Scheduler L1 Construct で実装する

Last updated at Posted at 2023-05-19

はじめに

EventBridge Scheduler 、設定が簡単で便利ですよね。
AWSの各種リソースを定期的に起動・停止するには、今まではLambda作ったり、System Manager 参照して そこから EventBridge のルールを作っていました。
EventBridge Scheduler は、実行する操作を EventBridge 内で指定できるので、作成とメンテナンスが楽です。

今回は、そんな EventBridge Scheduler を CDK で設定してみました。

EventBridge Scheduler は L1 Construct しかない

と思ってこの記事を書き始めたのですが、なんともうすぐできそうじゃないですか。

先週末にL1で実装して面倒だなーと思っていたので、待ち遠しいです。

ハマったところ

さて、今回ハマったポイントは各種リソースの操作の指定のしかたでした。

この記事と EventBridge Scheduler の画面とにらめっこして、以下のように指定すれば良さそうということで試したら、無事動きました。(後で見たら公式にユニバーサルターゲットの指定の仕方が書いてありました。)

arn:aws:scheduler:::aws-sdk:${service}:${EventBridge Scheduler で指定する操作の名前}

aws-sdk: 以降はPolicyで指定している値と同じですね。(ユニバーサルターゲットを指しています。)

ユニバーサルターゲットがわからんという場合は、
以下の様にEventBridge Scheduler の画面を見ると良いかもしれません。
(他に良い調べ方があればご指摘お願いします。)

EventBridgeSchedulerTarget.png

ただ、若干の例外もあるようです。
RDS の StartDBCluster, StopDBCluster は、Policy で指定する値では rds:StartDBCluster だったのですが、 EventBridge Scheduler の target.arn では rds:startDBCluster (Start の s が小文字) みたいに設定しないとダメでした。(ここにハマりました!!)

実装

以下は RDS Cluster / EC2 / ECS の起動・停止をスケジュールする EventBridge Scheduler を 作る Construct のサンプルです。

作りとしては以下の様になっています。

  • 複数のリソースで一つのサービスなので、Scheduler Group を使って Scheduler をまとめた
  • 起動・停止対象の各リソースは今回作った Construct の外側で定義している
  • 各スケジューラーの Construct は リソースを受け取って、その中で各種ターゲットを指定する
  • 1 Construct で 1 サービスの起動・停止用スケジューラー(つまり 2 つのスケジューラー)を定義している
  • 午前9時に起動、午後10時に停止(RDS は AP の起動停止と少しずらしている)

ライブラリのバージョンは以下の通りです。

  • aws-cdk: "2.69.0"
  • aws-cdk-lib : "2.69.0"
  • aws-sdk: "2.1319.0"
  • constructs: "10.1.244"

Scheduler Group の Construct

schedulerGroup.ts
import { CfnScheduleGroup } from 'aws-cdk-lib/aws-scheduler';
import { Construct } from 'constructs';

export class SchedulerGroup extends Construct {
  public readonly group: CfnScheduleGroup;
  constructor(scope: Construct, id: string) {
    super(scope, id);
    this.group = new CfnScheduleGroup(this, 'ScheduleGroup', {
      name: 'Schedulers',
    });
  }
}

RDS 用 Scheduler の Construct

rdsScheduler.ts
import { Construct } from 'constructs';
import { CfnSchedule, type CfnScheduleGroup } from 'aws-cdk-lib/aws-scheduler';
import {
  Effect,
  Policy,
  PolicyStatement,
  Role,
  ServicePrincipal,
} from 'aws-cdk-lib/aws-iam';
import { Queue } from 'aws-cdk-lib/aws-sqs';
import { ACCOUNT_ID, REGION } from '../../env';
import { type DatabaseCluster } from 'aws-cdk-lib/aws-rds';

interface RdsSchedulerProps {
  schedulerGroup: CfnScheduleGroup;
  rdsCluster: DatabaseCluster;
}

export class RdsScheduler extends Construct {
  constructor(
    scope: Construct,
    id: string,
    { schedulerGroup, rdsCluster }: RdsSchedulerProps
  ) {
    super(scope, id);

    const schedulerRole = new Role(this, 'SchedulerRoleForRDS', {
      assumedBy: new ServicePrincipal('scheduler.amazonaws.com'),
      description: 'Role for starting and stopping RDS Cluster',
    });
    new Policy(this, 'SchedulerPolicyForRDS', {
      policyName: 'RDSStartStop',
      roles: [schedulerRole],
      statements: [
        new PolicyStatement({
          effect: Effect.ALLOW,
          actions: ['rds:StartDBCluster', 'rds:StopDBCluster'],
          resources: [
            `arn:aws:rds:${REGION}:${ACCOUNT_ID}:cluster:${rdsCluster.clusterResourceIdentifier}`,
          ],
        }),
      ],
    });

    const dlq = new Queue(this, 'queue', {
      queueName: 'RdsManagerDlq',
    });

    new CfnSchedule(this, 'RdsStart', {
      description: 'Start RDS Cluster',
      groupName: schedulerGroup.name,
      flexibleTimeWindow: {
        mode: 'OFF',
      },
      scheduleExpressionTimezone: 'Asia/Tokyo',
      scheduleExpression: 'cron(45 08 ? * MON-FRI *)',
      target: {
        deadLetterConfig: { arn: dlq.queueArn },
        arn: 'arn:aws:scheduler:::aws-sdk:rds:startDBCluster',
        roleArn: schedulerRole.roleArn,
        input: JSON.stringify({
          DbClusterIdentifier: rdsCluster.clusterIdentifier,
        }),
      },
    });

    new CfnSchedule(this, 'RdsStop', {
      description: 'Stopp RDS Cluster',
      groupName: schedulerGroup.name,
      flexibleTimeWindow: {
        mode: 'OFF',
      },
      scheduleExpressionTimezone: 'Asia/Tokyo',
      scheduleExpression: 'cron(15 22 ? * MON-FRI *)',
      target: {
        deadLetterConfig: { arn: dlq.queueArn },
        arn: 'arn:aws:scheduler:::aws-sdk:rds:stopDBCluster',
        roleArn: schedulerRole.roleArn,
        input: JSON.stringify({
          DbClusterIdentifier: rdsCluster.clusterIdentifier,
        }),
      },
    });
  }
}

EC2 用 Scheduler の Construct

ec2Scheduler.ts
import { Construct } from 'constructs';
import { CfnSchedule, type CfnScheduleGroup } from 'aws-cdk-lib/aws-scheduler';
import {
  Effect,
  Policy,
  PolicyStatement,
  Role,
  ServicePrincipal,
} from 'aws-cdk-lib/aws-iam';
import { Queue } from 'aws-cdk-lib/aws-sqs';
import { type Instance } from 'aws-cdk-lib/aws-ec2';
import { ACCOUNT_ID, REGION } from '../../env';

interface Ec2SchedulerProps {
  schedulerGroup: CfnScheduleGroup;
  ec2Instance: Instance;
}
export class Ec2Scheduler extends Construct {
  constructor(
    scope: Construct,
    id: string,
    { schedulerGroup, ec2Instance }: Ec2SchedulerProps
  ) {
    super(scope, id);

    const schedulerRole = new Role(this, 'SchedulerRole', {
      assumedBy: new ServicePrincipal('scheduler.amazonaws.com'),
      description: 'Role for starting and stopping EC2',
    });
    new Policy(this, 'SchedulerPolicy', {
      policyName: 'EC2StartStop',
      roles: [schedulerRole],
      statements: [
        new PolicyStatement({
          effect: Effect.ALLOW,
          actions: ['ec2:startInstances', 'ec2:stopInstances'],
          resources: [
            `arn:aws:ec2:${REGION}:${ACCOUNT_ID}:instance/${ec2Instance.instance.ref}`,
          ],
        }),
      ],
    });

    const dlq = new Queue(this, 'queue', {
      queueName: 'Ec2ManagerDlq',
    });

    new CfnSchedule(this, 'Ec2Start', {
      description: 'Start EC2 Instance',
      groupName: schedulerGroup.name,
      flexibleTimeWindow: {
        mode: 'OFF',
      },
      scheduleExpressionTimezone: 'Asia/Tokyo',
      scheduleExpression: 'cron(00 09 ? * MON-FRI *)',
      target: {
        deadLetterConfig: { arn: dlq.queueArn },
        arn: 'arn:aws:scheduler:::aws-sdk:ec2:startInstances',
        roleArn: schedulerRole.roleArn,
        input: JSON.stringify({ InstanceIds: [ec2Instance.instanceId] }),
      },
    });

    new CfnSchedule(this, 'Ec2Stop', {
      description: 'Stop EC2 Instance',
      groupName: schedulerGroup.name,
      flexibleTimeWindow: {
        mode: 'OFF',
      },
      scheduleExpressionTimezone: 'Asia/Tokyo',
      scheduleExpression: 'cron(00 22 ? * MON-FRI *)',
      target: {
        deadLetterConfig: { arn: dlq.queueArn },
        arn: 'arn:aws:scheduler:::aws-sdk:ec2:stopInstances',
        roleArn: schedulerRole.roleArn,
        input: JSON.stringify({ InstanceIds: [ec2Instance.instanceId] }),
      },
    });
  }
}

ECS 用 Scheduler の Construct

ecsScheduler.ts
import { Construct } from 'constructs';
import { CfnSchedule, type CfnScheduleGroup } from 'aws-cdk-lib/aws-scheduler';
import {
  Effect,
  Policy,
  PolicyStatement,
  Role,
  ServicePrincipal,
} from 'aws-cdk-lib/aws-iam';
import { Queue } from 'aws-cdk-lib/aws-sqs';
import { type FargateService } from 'aws-cdk-lib/aws-ecs';

interface EcsSchedulerProps {
  schedulerGroup: CfnScheduleGroup;
  ecsService: FargateService;
}
export class EcsScheduler extends Construct {
  constructor(
    scope: Construct,
    id: string,
    { schedulerGroup, ecsService }: EcsSchedulerProps
  ) {
    super(scope, id);

    const schedulerRole = new Role(this, 'SchedulerRole', {
      assumedBy: new ServicePrincipal('scheduler.amazonaws.com'),
      description: 'Role for starting and stopping ECS',
    });
    new Policy(this, 'SchedulerPolicy', {
      policyName: 'ECSStartStop',
      roles: [schedulerRole],
      statements: [
        new PolicyStatement({
          effect: Effect.ALLOW,
          actions: ['ecs:UpdateService'],
          resources: [ecsService.serviceArn],
        }),
      ],
    });

    const dlq = new Queue(this, 'queue', {
      queueName: 'EcsManagerDlq',
    });

    new CfnSchedule(this, 'EcsStart', {
      description: 'Start ECS',
      groupName: schedulerGroup.name,
      flexibleTimeWindow: {
        mode: 'OFF',
      },
      scheduleExpressionTimezone: 'Asia/Tokyo',
      scheduleExpression: 'cron(00 09 ? * MON-FRI *)',
      target: {
        deadLetterConfig: { arn: dlq.queueArn },
        arn: 'arn:aws:scheduler:::aws-sdk:ecs:updateService',
        roleArn: schedulerRole.roleArn,
        input: JSON.stringify({
          Service: ecsService.serviceName,
          Cluster: ecsService.cluster.clusterName,
          DesiredCount: 1,
        }),
      },
    });

    new CfnSchedule(this, 'EcsStop', {
      description: 'Stop ECS',
      groupName: schedulerGroup.name,
      flexibleTimeWindow: {
        mode: 'OFF',
      },
      scheduleExpressionTimezone: 'Asia/Tokyo',
      scheduleExpression: 'cron(00 22 ? * MON-FRI *)',
      target: {
        deadLetterConfig: { arn: dlq.queueArn },
        arn: 'arn:aws:scheduler:::aws-sdk:ecs:updateService',
        roleArn: schedulerRole.roleArn,
        input: JSON.stringify({
          Service: ecsService.serviceName,
          Cluster: ecsService.cluster.clusterName,
          DesiredCount: 0,
        }),
      },
    });
  }
}

上記全てをまとめたアプリ用 Construct

appScheduler.ts
import { Construct } from 'constructs';
import { SchedulerGroup } from './schedulerGroup';
import { Ec2Scheduler } from './ec2Scheduler';
import { type Instance } from 'aws-cdk-lib/aws-ec2';
import { type DatabaseCluster } from 'aws-cdk-lib/aws-rds';
import { RdsScheduler } from './RdsScheduler';
import { EcsScheduler } from './ecsScheduler';
import { type FargateService } from 'aws-cdk-lib/aws-ecs';

interface AppSchedulersProps {
  ec2Instance: Instance;
  ecsService: FargateService;
  rdsCluster: DatabaseCluster;
}

export class AppSchedulers extends Construct {
  constructor(
    scope: Construct,
    id: string,
    { ec2Instance, ecsService, rdsCluster }: AppSchedulersProps
  ) {
    super(scope, id);
    const { group } = new AppSchedulerGroup(this, 'Group');
    new Ec2Scheduler(this, 'Ec2Scheduler', {
      schedulerGroup: group,
      ec2Instance,
    });
    new RdsScheduler(this, 'RdsScheduler', {
      schedulerGroup: group,
      rdsCluster,
    });
    new EcsScheduler(this, 'EcsScheduler', {
      schedulerGroup: group,
      ecsService,
    });
  }
}

最後に

CDK で書けるのは良いですが、やはり L1 Construct だと何を指定してよいか分かりづらいし、
記述するボリュームも多くなりがちだなぁと思いました。

もうすぐ L2 Construct がリリースされるので楽しみにしています。

最後まで読んでくださった方、ありがとうございました。

10
2
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
10
2