Skip to content

Commit

Permalink
feat: rds-scheduler
Browse files Browse the repository at this point in the history
  • Loading branch information
badmintoncryer committed Feb 13, 2024
1 parent a18fd88 commit 8627d3a
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 5 deletions.
82 changes: 82 additions & 0 deletions src/cron.ts
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})`;
}
}
7 changes: 2 additions & 5 deletions src/index.ts
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';
145 changes: 145 additions & 0 deletions src/rds-scheduler.ts
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,
}),
}),
},
});
}
});
}
}

0 comments on commit 8627d3a

Please sign in to comment.