Skip to content

Commit

Permalink
feat(aws-rds): make serverless-cluster vpc optional
Browse files Browse the repository at this point in the history
  • Loading branch information
CorentinDoue committed Nov 9, 2021
1 parent cac5726 commit ce03d19
Show file tree
Hide file tree
Showing 2 changed files with 214 additions and 27 deletions.
71 changes: 45 additions & 26 deletions packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts
Expand Up @@ -106,11 +106,15 @@ export interface ServerlessClusterProps {

/**
* The VPC that this Aurora Serverless cluster has been created in.
*
* @default - No VPC related construct will be created:
* - If subnetGroup is not provided, no DB subnet group will be associated with the DB cluster.
* - If securityGroups is not provided, no VPC security groups will be associated with the DB cluster.
*/
readonly vpc: ec2.IVpc;
readonly vpc?: ec2.IVpc;

/**
* Where to place the instances within the VPC
* Where to place the instances within the VPC. If set, a VPC must be specified.
*
* @default - the VPC default strategy if not specified.
*/
Expand All @@ -136,7 +140,7 @@ export interface ServerlessClusterProps {
/**
* Security group.
*
* @default - a new security group is created.
* @default - a new security group is created if a vpc is provided.
*/
readonly securityGroups?: ec2.ISecurityGroup[];

Expand All @@ -157,7 +161,7 @@ export interface ServerlessClusterProps {
/**
* Existing subnet group for the cluster.
*
* @default - a new subnet group will be created.
* @default - a new subnet group will be created if a vpc is provided.
*/
readonly subnetGroup?: ISubnetGroup;
}
Expand Down Expand Up @@ -377,8 +381,8 @@ export class ServerlessCluster extends ServerlessClusterBase {

protected enableDataApi?: boolean

private readonly subnetGroup: ISubnetGroup;
private readonly vpc: ec2.IVpc;
private readonly subnetGroup?: ISubnetGroup;
private readonly vpc?: ec2.IVpc;
private readonly vpcSubnets?: ec2.SubnetSelection;

private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication;
Expand All @@ -389,25 +393,40 @@ export class ServerlessCluster extends ServerlessClusterBase {

this.vpc = props.vpc;
this.vpcSubnets = props.vpcSubnets;
this.subnetGroup = props.subnetGroup;
let securityGroups: ec2.ISecurityGroup[] | undefined = props.securityGroups;

this.singleUserRotationApplication = props.engine.singleUserRotationApplication;
this.multiUserRotationApplication = props.engine.multiUserRotationApplication;

this.enableDataApi = props.enableDataApi;

const { subnetIds } = this.vpc.selectSubnets(this.vpcSubnets);

// Cannot test whether the subnets are in different AZs, but at least we can test the amount.
if (subnetIds.length < 2) {
Annotations.of(this).addError(`Cluster requires at least 2 subnets, got ${subnetIds.length}`);
if (this.vpc === undefined && this.vpcSubnets !== undefined) {
throw new Error('A vpc is required to use vpcSubnets in ServerlessCluster. Please add a vpc or remove vpcSubnets');
}

this.subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'Subnets', {
description: `Subnets for ${id} database`,
vpc: props.vpc,
vpcSubnets: props.vpcSubnets,
removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined,
});
if (this.vpc !== undefined) {
const { subnetIds } = this.vpc.selectSubnets(this.vpcSubnets);

// Cannot test whether the subnets are in different AZs, but at least we can test the amount.
if (subnetIds.length < 2) {
Annotations.of(this).addError(`Cluster requires at least 2 subnets, got ${subnetIds.length}`);
}

this.subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'Subnets', {
description: `Subnets for ${id} database`,
vpc: this.vpc,
vpcSubnets: this.vpcSubnets,
removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined,
});

securityGroups = props.securityGroups ?? [
new ec2.SecurityGroup(this, 'SecurityGroup', {
description: 'RDS security group',
vpc: this.vpc,
}),
];
}

if (props.backupRetention) {
const backupRetentionDays = props.backupRetention.toDays();
Expand All @@ -426,13 +445,6 @@ export class ServerlessCluster extends ServerlessClusterBase {
const clusterParameterGroup = props.parameterGroup ?? clusterEngineBindConfig.parameterGroup;
const clusterParameterGroupConfig = clusterParameterGroup?.bindToCluster({});

const securityGroups = props.securityGroups ?? [
new ec2.SecurityGroup(this, 'SecurityGroup', {
description: 'RDS security group',
vpc: this.vpc,
}),
];

const clusterIdentifier = FeatureFlags.of(this).isEnabled(cxapi.RDS_LOWERCASE_DB_IDENTIFIER)
? props.clusterIdentifier?.toLowerCase()
: props.clusterIdentifier;
Expand All @@ -442,7 +454,7 @@ export class ServerlessCluster extends ServerlessClusterBase {
databaseName: props.defaultDatabaseName,
dbClusterIdentifier: clusterIdentifier,
dbClusterParameterGroupName: clusterParameterGroupConfig?.parameterGroupName,
dbSubnetGroupName: this.subnetGroup.subnetGroupName,
dbSubnetGroupName: this.subnetGroup?.subnetGroupName,
deletionProtection: defaultDeletionProtection(props.deletionProtection, props.removalPolicy),
engine: props.engine.engineType,
engineVersion: props.engine.engineVersion?.fullVersion,
Expand All @@ -453,7 +465,7 @@ export class ServerlessCluster extends ServerlessClusterBase {
masterUserPassword: credentials.password?.toString(),
scalingConfiguration: props.scaling ? this.renderScalingConfiguration(props.scaling) : undefined,
storageEncrypted: true,
vpcSecurityGroupIds: securityGroups.map(sg => sg.securityGroupId),
vpcSecurityGroupIds: securityGroups?.map(sg => sg.securityGroupId),
});

this.clusterIdentifier = cluster.ref;
Expand Down Expand Up @@ -488,6 +500,10 @@ export class ServerlessCluster extends ServerlessClusterBase {
throw new Error('A single user rotation was already added to this cluster.');
}

if (this.vpc === undefined) {
throw new Error('Cannot add single user rotation for a cluster without vpc.');
}

return new secretsmanager.SecretRotation(this, id, {
secret: this.secret,
application: this.singleUserRotationApplication,
Expand All @@ -506,6 +522,9 @@ export class ServerlessCluster extends ServerlessClusterBase {
if (!this.secret) {
throw new Error('Cannot add multi user rotation for a cluster without secret.');
}
if (this.vpc === undefined) {
throw new Error('Cannot add multi user rotation for a cluster without vpc.');
}
return new secretsmanager.SecretRotation(this, id, {
...options,
excludeCharacters: options.excludeCharacters ?? DEFAULT_PASSWORD_EXCLUDE_CHARS,
Expand Down
170 changes: 169 additions & 1 deletion packages/@aws-cdk/aws-rds/test/serverless-cluster.test.ts
Expand Up @@ -5,7 +5,7 @@ import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as cdk from '@aws-cdk/core';
import * as cxapi from '@aws-cdk/cx-api';
import { AuroraPostgresEngineVersion, ServerlessCluster, DatabaseClusterEngine, ParameterGroup, AuroraCapacityUnit, DatabaseSecret } from '../lib';
import { AuroraPostgresEngineVersion, ServerlessCluster, DatabaseClusterEngine, ParameterGroup, AuroraCapacityUnit, DatabaseSecret, SubnetGroup } from '../lib';

describe('serverless cluster', () => {
test('can create a Serverless Cluster with Aurora Postgres database engine', () => {
Expand Down Expand Up @@ -413,6 +413,22 @@ describe('serverless cluster', () => {
expect(() => cluster.addRotationSingleUser()).toThrow(/A single user rotation was already added to this cluster/);


});

test('throws when trying to add rotation to a serverless cluster without vpc', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
const cluster = new ServerlessCluster(stack, 'Database', {
engine: DatabaseClusterEngine.AURORA_MYSQL,
credentials: { username: 'admin' },
});

// THEN
expect(() => cluster.addRotationSingleUser()).toThrow(/Cannot add single user rotation for a cluster without vpc/);


});

test('can set deletion protection', () => {
Expand Down Expand Up @@ -863,6 +879,158 @@ describe('serverless cluster', () => {


});

test('can create a Serverless cluster without vpc', () => {
// GIVEN
const stack = testStack();

// WHEN
new ServerlessCluster(stack, 'Database', {
engine: DatabaseClusterEngine.AURORA_POSTGRESQL,
parameterGroup: ParameterGroup.fromParameterGroupName(stack, 'ParameterGroup', 'default.aurora-postgresql10'),
});

// THEN
expect(stack).toHaveResource('AWS::RDS::DBCluster', {
Engine: 'aurora-postgresql',
DBClusterParameterGroupName: 'default.aurora-postgresql10',
EngineMode: 'serverless',
MasterUsername: {
'Fn::Join': [
'',
[
'{{resolve:secretsmanager:',
{
Ref: 'DatabaseSecret3B817195',
},
':SecretString:username::}}',
],
],
},
MasterUserPassword: {
'Fn::Join': [
'',
[
'{{resolve:secretsmanager:',
{
Ref: 'DatabaseSecret3B817195',
},
':SecretString:password::}}',
],
],
},
});


});

test('can create a Serverless cluster without vpc but with imported security group', () => {
// GIVEN
const stack = testStack();
const sg = ec2.SecurityGroup.fromSecurityGroupId(stack, 'SG', 'SecurityGroupId12345');

// WHEN
new ServerlessCluster(stack, 'Database', {
engine: DatabaseClusterEngine.AURORA_POSTGRESQL,
securityGroups: [sg],
parameterGroup: ParameterGroup.fromParameterGroupName(stack, 'ParameterGroup', 'default.aurora-postgresql10'),
});

// THEN
expect(stack).toHaveResource('AWS::RDS::DBCluster', {
Engine: 'aurora-postgresql',
DBClusterParameterGroupName: 'default.aurora-postgresql10',
EngineMode: 'serverless',
MasterUsername: {
'Fn::Join': [
'',
[
'{{resolve:secretsmanager:',
{
Ref: 'DatabaseSecret3B817195',
},
':SecretString:username::}}',
],
],
},
MasterUserPassword: {
'Fn::Join': [
'',
[
'{{resolve:secretsmanager:',
{
Ref: 'DatabaseSecret3B817195',
},
':SecretString:password::}}',
],
],
},
VpcSecurityGroupIds: ['SecurityGroupId12345'],
});
});

test('can create a Serverless cluster without vpc but with imported subnet group', () => {
// GIVEN
const stack = testStack();
const subnetGroup = SubnetGroup.fromSubnetGroupName(stack, 'SubnetGroup12345', 'SubnetGroupId12345');

// WHEN
new ServerlessCluster(stack, 'Database', {
engine: DatabaseClusterEngine.AURORA_POSTGRESQL,
parameterGroup: ParameterGroup.fromParameterGroupName(stack, 'ParameterGroup', 'default.aurora-postgresql10'),
subnetGroup,
});

// THEN
expect(stack).toHaveResource('AWS::RDS::DBCluster', {
Engine: 'aurora-postgresql',
DBClusterParameterGroupName: 'default.aurora-postgresql10',
EngineMode: 'serverless',
DBSubnetGroupName: { Ref: 'SubnetGroup12345' },
MasterUsername: {
'Fn::Join': [
'',
[
'{{resolve:secretsmanager:',
{
Ref: 'DatabaseSecret3B817195',
},
':SecretString:username::}}',
],
],
},
MasterUserPassword: {
'Fn::Join': [
'',
[
'{{resolve:secretsmanager:',
{
Ref: 'DatabaseSecret3B817195',
},
':SecretString:password::}}',
],
],
},
VpcSecurityGroupIds: ['SecurityGroupId12345'],
});
});

test('can\'t create a Serverless cluster without vpc but with imported vpc subnets', () => {
// GIVEN
const stack = testStack();

// WHEN
const vpcSubnets = {
subnetName: 'AVpcSubnet',
};

// THEN
expect(() => new ServerlessCluster(stack, 'Database', {
engine: DatabaseClusterEngine.AURORA_POSTGRESQL,
parameterGroup: ParameterGroup.fromParameterGroupName(stack, 'ParameterGroup', 'default.aurora-postgresql10'),
vpcSubnets,
})).toThrow(/vpc is required/);
});
});

function testStack(app?: cdk.App, id?: string): cdk.Stack {
Expand Down

0 comments on commit ce03d19

Please sign in to comment.