はじめに
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 の画面を見ると良いかもしれません。
(他に良い調べ方があればご指摘お願いします。)
ただ、若干の例外もあるようです。
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
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
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
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
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
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 がリリースされるので楽しみにしています。
最後まで読んでくださった方、ありがとうございました。