Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/@aws-cdk/aws-redshift-alpha/awslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"props-physical-name:@aws-cdk/aws-redshift-alpha.ClusterSubnetGroupProps",
"props-physical-name:@aws-cdk/aws-redshift-alpha.DatabaseSecretProps",
"attribute-tag:@aws-cdk/aws-redshift-alpha.DatabaseSecret.secretName",
"attribute-tag:@aws-cdk/aws-redshift-alpha.DatabaseSecret.secretFullArn"
"attribute-tag:@aws-cdk/aws-redshift-alpha.DatabaseSecret.secretFullArn",
"props-no-arn-refs:@aws-cdk/aws-redshift-alpha.RedshiftQuerySchedulerProps.clusterArn",
"props-no-arn-refs:@aws-cdk/aws-redshift-alpha.RedshiftQuerySchedulerProps.workgroupArn",
"props-no-arn-refs:@aws-cdk/aws-redshift-alpha.RedshiftQuerySchedulerProps.secretArn"
]
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-redshift-alpha/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export * from './parameter-group';
export * from './database-options';
export * from './database-secret';
export * from './endpoint';
export * from './redshift-query-scheduler';
export * from './subnet-group';
export * from './table';
export * from './user';
Expand Down
202 changes: 202 additions & 0 deletions packages/@aws-cdk/aws-redshift-alpha/lib/redshift-query-scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
// ABOUTME: Ultra-simplified RedshiftQueryScheduler using only scheduled EventBridge Rule
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as scheduler from 'aws-cdk-lib/aws-scheduler';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import { Construct } from 'constructs';
import { ValidationError } from 'aws-cdk-lib/core';

/**
* Properties for the RedshiftQueryScheduler construct.
*/
export interface RedshiftQuerySchedulerProps {
/**
* The name of the scheduler. This will be used to create the EventBridge Scheduler rule name
* and the StatementName in RedshiftDataParameters, both prefixed with "QS2-".
*/
readonly schedulerName: string;

/**
* The name of the Redshift database to execute queries against.
*/
readonly database: string;

/**
* The ARN of the Redshift cluster. Cannot be used together with workgroupArn.
*
* @default - No cluster ARN specified
*/
readonly clusterArn?: string;

/**
* The ARN of the Redshift Serverless workgroup. Cannot be used together with clusterArn.
* When using workgroupArn, dbUser cannot be specified.
*
* @default - No workgroup ARN specified
*/
readonly workgroupArn?: string;

/**
* The database user name for authentication. Cannot be used together with secretArn.
* Cannot be used when workgroupArn is specified.
*
* @default - No database user specified
*/
readonly dbUser?: string;

/**
* The ARN of the secret containing database credentials. Cannot be used together with dbUser.
*
* @default - No secret ARN specified
*/
readonly secretArn?: string;

/**
* A single SQL statement to execute. Cannot be used together with sqls.
*
* @default - No single SQL statement specified
*/
readonly sql?: string;

/**
* Multiple SQL statements to execute as a transaction. Cannot be used together with sql.
*
* @default - No SQL statements array specified
*/
readonly sqls?: string[];

/**
* The schedule expression that defines when the query should be executed.
*/
readonly schedule: scheduler.ScheduleExpression;

/**
* A description for the scheduler.
*
* @default - No description
*/
readonly description?: string;

/**
* Whether the scheduler is enabled.
*
* @default true
*/
readonly enabled?: boolean;

/**
* The IAM role to use for executing the Redshift query.
* This role must have permissions to execute queries on the specified Redshift cluster or workgroup.
*
* @default - A new role will be created with the necessary permissions
*/
readonly role?: iam.IRole;
}

/**
* A construct that creates a scheduled EventBridge Rule to execute Redshift queries using the Redshift Data API.
*
* This construct uses the simplest possible approach: a single EventBridge Rule with a schedule
* that directly targets RedshiftQuery. No EventBridge Scheduler needed.
*
* The EventBridge rule name and StatementName are both set to "QS2-{schedulerName}" to enable
* identification in the Amazon Redshift console.
*
* @example
* new RedshiftQueryScheduler(this, 'MyScheduler', {
* schedulerName: 'daily-report',
* database: 'analytics',
* clusterArn: 'arn:aws:redshift:us-east-1:123456789012:cluster:my-cluster',
* dbUser: 'scheduler_user',
* sql: 'CALL generate_daily_report()',
* schedule: ScheduleExpression.rate(Duration.days(1)),
* });
*/
export class RedshiftQueryScheduler extends Construct {
/**
* The EventBridge Rule created by this construct.
*/
public readonly rule: events.Rule;

constructor(scope: Construct, id: string, props: RedshiftQuerySchedulerProps) {
super(scope, id);

// Validate input parameters
this.validateProps(props);

const ruleName = `QS2-${props.schedulerName}`;
const statementName = `QS2-${props.schedulerName}`;
const targetArn = props.clusterArn || props.workgroupArn!;

// Create scheduled EventBridge Rule
this.rule = new events.Rule(this, 'Rule', {
ruleName: ruleName,
schedule: events.Schedule.expression(props.schedule.expressionString),
description: props.description,
enabled: props.enabled ?? true,
});

// Create RedshiftQuery target with parameter mapping
const redshiftQueryProps: targets.RedshiftQueryProps = {
database: props.database,
dbUser: props.dbUser,
secret: props.secretArn ? secretsmanager.Secret.fromSecretCompleteArn(this, 'Secret', props.secretArn) : undefined,
sql: props.sql ? [props.sql] : props.sqls!,
statementName: statementName,
sendEventBridgeEvent: true, // Maps to WithEvent: true
role: props.role,
};

const redshiftTarget = new targets.RedshiftQuery(targetArn, redshiftQueryProps);
this.rule.addTarget(redshiftTarget);
}

private validateProps(props: RedshiftQuerySchedulerProps): void {
// Validate target ARN (cluster or workgroup)
const hasCluster = !!props.clusterArn;
const hasWorkgroup = !!props.workgroupArn;

if (hasCluster && hasWorkgroup) {
throw new ValidationError('Cannot specify both clusterArn and workgroupArn. Choose exactly one.', this);
}

if (!hasCluster && !hasWorkgroup) {
throw new ValidationError('Must specify exactly one of clusterArn or workgroupArn.', this);
}

// Validate authentication
const hasDbUser = !!props.dbUser;
const hasSecretArn = !!props.secretArn;

if (hasDbUser && hasSecretArn) {
throw new ValidationError('Cannot specify both dbUser and secretArn. Choose exactly one.', this);
}

// Validate workgroup + dbUser restriction
if (hasWorkgroup && hasDbUser) {
throw new ValidationError('Cannot specify dbUser when using workgroupArn.', this);
}

// Validate SQL content first
if (props.sql !== undefined && props.sql.trim() === '') {
throw new ValidationError('sql cannot be empty.', this);
}

if (props.sqls !== undefined && props.sqls.length === 0) {
throw new ValidationError('sqls array cannot be empty.', this);
}

// Validate SQL presence
const hasSql = !!props.sql && props.sql.trim() !== '';
const hasSqls = !!props.sqls && props.sqls.length > 0;

if (hasSql && hasSqls) {
throw new ValidationError('Cannot specify both sql and sqls. Choose exactly one.', this);
}

if (!hasSql && !hasSqls) {
throw new ValidationError('Must specify exactly one of sql or sqls.', this);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading