Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rds): make VPC optional for serverless Clusters #17413

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's revert these changes. It's enough to mention that vpc is optional below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example shows the minimal configuration to enable the data API. As the vpc is now optional, I think it shouldn't be in the example

});

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'),
},
CorentinDoue marked this conversation as resolved.
Show resolved Hide resolved
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world');

app.synth();