-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a18fd88
commit 8627d3a
Showing
3 changed files
with
229 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/** | ||
* Options to configure a cron expression | ||
* | ||
* All fields are strings so you can use complex expressions. Absence of | ||
* a field implies '*' or '?', whichever one is appropriate. | ||
* | ||
* @see https://docs.aws.amazon.com/eventbridge/latest/userguide/scheduled-events.html#cron-expressions | ||
*/ | ||
export interface CronOptions { | ||
/** | ||
* The minute to run this rule at | ||
* | ||
* @default - Every minute | ||
*/ | ||
readonly minute?: string; | ||
|
||
/** | ||
* The hour to run this rule at | ||
* | ||
* @default - Every hour | ||
*/ | ||
readonly hour?: string; | ||
|
||
/** | ||
* The day of the month to run this rule at | ||
* | ||
* @default - Every day of the month | ||
*/ | ||
readonly day?: string; | ||
|
||
/** | ||
* The month to run this rule at | ||
* | ||
* @default - Every month | ||
*/ | ||
readonly month?: string; | ||
|
||
/** | ||
* The year to run this rule at | ||
* | ||
* @default - Every year | ||
*/ | ||
readonly year?: string; | ||
|
||
/** | ||
* The day of the week to run this rule at | ||
* | ||
* @default - Any day of the week | ||
*/ | ||
readonly weekDay?: string; | ||
} | ||
|
||
export class Cron { | ||
private readonly minute: string; | ||
private readonly hour: string; | ||
private readonly day: string; | ||
private readonly month: string; | ||
private readonly weekDay: string; | ||
private readonly year: string; | ||
|
||
/** | ||
* Create a cron expression | ||
*/ | ||
constructor(options: CronOptions) { | ||
// TODO - validate that the expression is valid | ||
// @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html?icmpid=docs_console_unmapped#cron-based | ||
|
||
this.minute = options.minute ?? '*'; | ||
this.hour = options.hour ?? '*'; | ||
this.day = options.day ?? '*'; | ||
this.month = options.month ?? '*'; | ||
this.year = options.year ?? '*'; | ||
this.weekDay = options.weekDay ?? '?'; | ||
} | ||
|
||
/** | ||
* Return the cron expression | ||
*/ | ||
public toString() { | ||
return `cron(${this.minute} ${this.hour} ${this.day} ${this.month} ${this.weekDay} ${this.year})`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,2 @@ | ||
export class Hello { | ||
public sayHello() { | ||
return 'hello, world!'; | ||
} | ||
} | ||
export * from './rds-scheduler'; | ||
export * from './cron'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { Stack, TimeZone } from 'aws-cdk-lib'; | ||
import * as iam from 'aws-cdk-lib/aws-iam'; | ||
import * as rds from 'aws-cdk-lib/aws-rds'; | ||
import * as scheduler from 'aws-cdk-lib/aws-scheduler'; | ||
import { Construct } from 'constructs'; | ||
import { Cron } from './cron'; | ||
|
||
export interface Schedule { | ||
/** | ||
* The start schedule | ||
* | ||
* @default - no start schedule. The RDS instance or cluster will not be started automatically. | ||
*/ | ||
readonly start?: Cron; | ||
/** | ||
* The stop schedule | ||
* | ||
* @default - no stop schedule. The RDS instance or cluster will not be stopped automatically. | ||
*/ | ||
readonly stop?: Cron; | ||
/** | ||
* The timezone for the cron expression | ||
* | ||
* @default UTC | ||
*/ | ||
readonly timezone?: TimeZone; | ||
} | ||
|
||
/** | ||
* Properties for the RdsScheduler | ||
*/ | ||
export interface RdsSchedulerProps { | ||
/** | ||
* The RDS cluster to start and stop. | ||
* If you specify a cluster, you cannot specify an instance. | ||
* | ||
* @default - no cluster is specified and you must specify an instance | ||
*/ | ||
readonly cluster?: rds.IDatabaseCluster; | ||
/** | ||
* The RDS instance to start and stop. | ||
* If you specify an instance, you cannot specify a cluster. | ||
* | ||
* @default - no instance is specified and you must specify a cluster | ||
*/ | ||
readonly instance?: rds.IDatabaseInstance; | ||
/** | ||
* The schedule for starting and stopping the RDS instance or cluster | ||
*/ | ||
readonly schedule: Schedule[]; | ||
} | ||
|
||
/** | ||
* A scheduler for RDS instances or clusters | ||
*/ | ||
export class RdsScheduler extends Construct { | ||
constructor (scope: Construct, id: string, props: RdsSchedulerProps) { | ||
super(scope, id); | ||
|
||
if (props.cluster && props.instance) { | ||
throw new Error('You can only specify either a cluster or an instance, not both.'); | ||
} | ||
if (!props.cluster && !props.instance) { | ||
throw new Error('You must specify either a cluster or an instance.'); | ||
} | ||
if (props.schedule.length === 0) { | ||
throw new Error('You must specify at least one schedule.'); | ||
} | ||
const isCluster = !!props.cluster; | ||
|
||
const identifier = isCluster ? props.cluster!.clusterIdentifier : props.instance!.instanceIdentifier; | ||
const schedulerRole = new iam.Role(this, 'SchedulerRole', { | ||
assumedBy: new iam.ServicePrincipal('scheduler.amazonaws.com'), | ||
inlinePolicies: { | ||
SchedulerPolicy: new iam.PolicyDocument({ | ||
statements: [ | ||
new iam.PolicyStatement({ | ||
effect: iam.Effect.ALLOW, | ||
actions: isCluster ? ['rds:StartDBCluster', 'rds:StopDBCluster'] : ['rds:StartDBInstance', 'rds:StopDBInstance'], | ||
resources: [ | ||
Stack.of(this).formatArn({ | ||
service: 'rds', | ||
resource: isCluster ? 'cluster' : 'db', | ||
resourceName: identifier, | ||
}), | ||
], | ||
}), | ||
], | ||
}), | ||
}, | ||
}); | ||
|
||
props.schedule.forEach((schedule, index) => { | ||
if (schedule.start) { | ||
new scheduler.CfnSchedule(this, `StartSchedule${index}`, { | ||
flexibleTimeWindow: { | ||
mode: 'OFF', | ||
}, | ||
scheduleExpression: schedule.start.toString(), | ||
scheduleExpressionTimezone: schedule.timezone?.timezoneName ?? TimeZone.ETC_UTC.timezoneName, | ||
target: { | ||
arn: Stack.of(this).formatArn({ | ||
service: 'scheduler', | ||
resource: 'aws-sdk:rds', | ||
resourceName: isCluster ? 'startDBCluster' : 'startDBInstance', | ||
}), | ||
roleArn: schedulerRole.roleArn, | ||
input: JSON.stringify({ | ||
...(isCluster ? { | ||
DBClusterIdentifier: identifier, | ||
} : { | ||
DBInstanceIdentifier: identifier, | ||
}), | ||
}), | ||
}, | ||
}); | ||
} | ||
|
||
if (schedule.stop) { | ||
new scheduler.CfnSchedule(this, `StopSchedule${index}`, { | ||
flexibleTimeWindow: { | ||
mode: 'OFF', | ||
}, | ||
scheduleExpression: schedule.stop.toString(), | ||
scheduleExpressionTimezone: schedule.timezone?.timezoneName ?? TimeZone.ETC_UTC.timezoneName, | ||
target: { | ||
arn: Stack.of(this).formatArn({ | ||
service: 'scheduler', | ||
resource: 'aws-sdk:rds', | ||
resourceName: isCluster ? 'stopDBCluster' : 'stopDBInstance', | ||
}), | ||
roleArn: schedulerRole.roleArn, | ||
input: JSON.stringify({ | ||
...(isCluster ? { | ||
DBClusterIdentifier: identifier, | ||
} : { | ||
DBInstanceIdentifier: identifier, | ||
}), | ||
}), | ||
}, | ||
}); | ||
} | ||
}); | ||
} | ||
} |