Skip to content

Commit

Permalink
feat(rds): make VPC optional for serverless Clusters (#17413)
Browse files Browse the repository at this point in the history
Fixes #17401

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
CorentinDoue committed Feb 22, 2022
1 parent 7441418 commit 4f7818d
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 97 deletions.
10 changes: 9 additions & 1 deletion packages/@aws-cdk/aws-rds/README.md
Expand Up @@ -639,7 +639,7 @@ declare const vpc: ec2.Vpc;

const cluster = new rds.ServerlessCluster(this, 'AnotherCluster', {
engine: rds.DatabaseClusterEngine.AURORA_MYSQL,
vpc,
vpc, // this parameter is optional for serverless Clusters
enableDataApi: true, // Optional - will be automatically set if you call grantDataApiAccess()
});

Expand All @@ -659,3 +659,11 @@ cluster.grantDataApiAccess(fn);
**Note**: To invoke the Data API, the resource will need to read the secret associated with the cluster.

To learn more about using the Data API, see the [documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html).

### Default VPC

The `vpc` parameter is optional.

If not provided, the cluster will be created in the default VPC of the account and region.
As this VPC is not deployed with AWS CDK, you can't configure the `vpcSubnets`, `subnetGroup` or `securityGroups` of the Aurora Serverless Cluster.
If you want to provide one of `vpcSubnets`, `subnetGroup` or `securityGroups` parameter, please provide a `vpc`.
79 changes: 55 additions & 24 deletions packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts
Expand Up @@ -99,11 +99,14 @@ interface ServerlessClusterNewProps {

/**
* The VPC that this Aurora Serverless cluster has been created in.
*
* @default - the default VPC in the account and region will be used
*/
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 provided, the `vpc` property must also be specified.
*
* @default - the VPC default strategy if not specified.
*/
Expand All @@ -129,7 +132,8 @@ interface ServerlessClusterNewProps {
/**
* Security group.
*
* @default - a new security group is created.
* @default - a new security group is created if `vpc` was provided.
* If the `vpc` property was not provided, no VPC security groups will be associated with the DB cluster.
*/
readonly securityGroups?: ec2.ISecurityGroup[];

Expand All @@ -143,7 +147,8 @@ interface ServerlessClusterNewProps {
/**
* Existing subnet group for the cluster.
*
* @default - a new subnet group will be created.
* @default - a new subnet group is created if `vpc` was provided.
* If the `vpc` property was not provided, no subnet group will be associated with the DB cluster
*/
readonly subnetGroup?: ISubnetGroup;
}
Expand Down Expand Up @@ -351,19 +356,42 @@ abstract class ServerlessClusterNew extends ServerlessClusterBase {
constructor(scope: Construct, id: string, props: ServerlessClusterNewProps) {
super(scope, id);

const { subnetIds } = props.vpc.selectSubnets(props.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 (props.vpc === undefined) {
if (props.vpcSubnets !== undefined) {
throw new Error('A VPC is required to use vpcSubnets in ServerlessCluster. Please add a VPC or remove vpcSubnets');
}
if (props.subnetGroup !== undefined) {
throw new Error('A VPC is required to use subnetGroup in ServerlessCluster. Please add a VPC or remove subnetGroup');
}
if (props.securityGroups !== undefined) {
throw new Error('A VPC is required to use securityGroups in ServerlessCluster. Please add a VPC or remove securityGroups');
}
}

const 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,
});
let subnetGroup: ISubnetGroup | undefined = props.subnetGroup;
this.securityGroups = props.securityGroups ?? [];
if (props.vpc !== undefined) {
const { subnetIds } = props.vpc.selectSubnets(props.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}`);
}

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,
});

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

if (props.backupRetention) {
const backupRetentionDays = props.backupRetention.toDays();
Expand All @@ -379,12 +407,6 @@ abstract class ServerlessClusterNew extends ServerlessClusterBase {
const clusterParameterGroup = props.parameterGroup ?? clusterEngineBindConfig.parameterGroup;
const clusterParameterGroupConfig = clusterParameterGroup?.bindToCluster({});

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

const clusterIdentifier = FeatureFlags.of(this).isEnabled(cxapi.RDS_LOWERCASE_DB_IDENTIFIER)
? props.clusterIdentifier?.toLowerCase()
Expand All @@ -395,7 +417,7 @@ abstract class ServerlessClusterNew extends ServerlessClusterBase {
databaseName: props.defaultDatabaseName,
dbClusterIdentifier: clusterIdentifier,
dbClusterParameterGroupName: clusterParameterGroupConfig?.parameterGroupName,
dbSubnetGroupName: subnetGroup.subnetGroupName,
dbSubnetGroupName: subnetGroup?.subnetGroupName,
deletionProtection: defaultDeletionProtection(props.deletionProtection, props.removalPolicy),
engine: props.engine.engineType,
engineVersion: props.engine.engineVersion?.fullVersion,
Expand Down Expand Up @@ -476,7 +498,7 @@ export class ServerlessCluster extends ServerlessClusterNew {

public readonly secret?: secretsmanager.ISecret;

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

private readonly singleUserRotationApplication: secretsmanager.SecretRotationApplication;
Expand Down Expand Up @@ -525,6 +547,10 @@ export class ServerlessCluster extends ServerlessClusterNew {
throw new Error('Cannot add single user rotation for a cluster without secret.');
}

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

const id = 'RotationSingleUser';
const existing = this.node.tryFindChild(id);
if (existing) {
Expand All @@ -549,6 +575,11 @@ export class ServerlessCluster extends ServerlessClusterNew {
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 Expand Up @@ -680,4 +711,4 @@ export class ServerlessClusterFromSnapshot extends ServerlessClusterNew {
this.secret = secret.attach(this);
}
}
}
}
@@ -0,0 +1,18 @@
{
"Resources": {
"ServerlessDatabaseWithoutVPC93F9A752": {
"Type": "AWS::RDS::DBCluster",
"Properties": {
"Engine": "aurora-mysql",
"DBClusterParameterGroupName": "default.aurora-mysql5.7",
"EngineMode": "serverless",
"MasterUsername": "admin",
"MasterUserPassword": "7959866cacc02c2d243ecfe177464fe6",
"StorageEncrypted": true,
"VpcSecurityGroupIds": []
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
}
}
}
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-rds/test/integ.serverless-cluster-no-vpc.ts
@@ -0,0 +1,17 @@
import * as cdk from '@aws-cdk/core';
import * as rds from '../lib';

const app = new cdk.App();
const stack = new cdk.Stack(app, 'aws-cdk-sls-cluster-no-vpc-integ');

const cluster = new rds.ServerlessCluster(stack, 'Serverless Database Without VPC', {
engine: rds.DatabaseClusterEngine.AURORA_MYSQL,
credentials: {
username: 'admin',
password: cdk.SecretValue.plainText('7959866cacc02c2d243ecfe177464fe6'),
},
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world');

app.synth();

0 comments on commit 4f7818d

Please sign in to comment.