Skip to content

Commit a1df511

Browse files
authored
feat(rds): support existing cluster subnet groups (#10391)
Enable users with existing cluster subnet groups to specify an existing group, rather than creating a new group. _Note: Marked as exempt-readme because I don't think this deserves its own README section. Feel free to disagree._ fixes #9991 BREAKING CHANGE: removed protected member `subnetGroup` from DatabaseCluster classes ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 75811c1 commit a1df511

File tree

8 files changed

+300
-20
lines changed

8 files changed

+300
-20
lines changed

packages/@aws-cdk/aws-rds/lib/cluster.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import { IParameterGroup } from './parameter-group';
1313
import { applyRemovalPolicy, defaultDeletionProtection, setupS3ImportExport } from './private/util';
1414
import { BackupProps, InstanceProps, Login, PerformanceInsightRetention, RotationMultiUserOptions } from './props';
1515
import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy';
16-
import { CfnDBCluster, CfnDBClusterProps, CfnDBInstance, CfnDBSubnetGroup } from './rds.generated';
16+
import { CfnDBCluster, CfnDBClusterProps, CfnDBInstance } from './rds.generated';
17+
import { ISubnetGroup, SubnetGroup } from './subnet-group';
1718

1819
/**
1920
* Common properties for a new database cluster or cluster from snapshot.
@@ -213,6 +214,13 @@ interface DatabaseClusterBaseProps {
213214
* @default - None
214215
*/
215216
readonly s3ExportBuckets?: s3.IBucket[];
217+
218+
/**
219+
* Existing subnet group for the cluster.
220+
*
221+
* @default - a new subnet group will be created.
222+
*/
223+
readonly subnetGroup?: ISubnetGroup;
216224
}
217225

218226
/**
@@ -278,8 +286,8 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase {
278286
public readonly instanceEndpoints: Endpoint[] = [];
279287

280288
protected readonly newCfnProps: CfnDBClusterProps;
281-
protected readonly subnetGroup: CfnDBSubnetGroup;
282289
protected readonly securityGroups: ec2.ISecurityGroup[];
290+
protected readonly subnetGroup: ISubnetGroup;
283291

284292
constructor(scope: Construct, id: string, props: DatabaseClusterBaseProps) {
285293
super(scope, id);
@@ -291,13 +299,12 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase {
291299
Annotations.of(this).addError(`Cluster requires at least 2 subnets, got ${subnetIds.length}`);
292300
}
293301

294-
this.subnetGroup = new CfnDBSubnetGroup(this, 'Subnets', {
295-
dbSubnetGroupDescription: `Subnets for ${id} database`,
296-
subnetIds,
302+
this.subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'Subnets', {
303+
description: `Subnets for ${id} database`,
304+
vpc: props.instanceProps.vpc,
305+
vpcSubnets: props.instanceProps.vpcSubnets,
306+
removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined,
297307
});
298-
if (props.removalPolicy === RemovalPolicy.RETAIN) {
299-
this.subnetGroup.applyRemovalPolicy(RemovalPolicy.RETAIN);
300-
}
301308

302309
this.securityGroups = props.instanceProps.securityGroups ?? [
303310
new ec2.SecurityGroup(this, 'SecurityGroup', {
@@ -330,7 +337,7 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase {
330337
engine: props.engine.engineType,
331338
engineVersion: props.engine.engineVersion?.fullVersion,
332339
dbClusterIdentifier: props.clusterIdentifier,
333-
dbSubnetGroupName: this.subnetGroup.ref,
340+
dbSubnetGroupName: this.subnetGroup.subnetGroupName,
334341
vpcSecurityGroupIds: this.securityGroups.map(sg => sg.securityGroupId),
335342
port: props.port ?? clusterEngineBindConfig.port,
336343
dbClusterParameterGroupName: clusterParameterGroupConfig?.parameterGroupName,
@@ -641,7 +648,7 @@ interface InstanceConfig {
641648
* A function rather than a protected method on ``DatabaseClusterNew`` to avoid exposing
642649
* ``DatabaseClusterNew`` and ``DatabaseClusterBaseProps`` in the API.
643650
*/
644-
function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBaseProps, subnetGroup: CfnDBSubnetGroup): InstanceConfig {
651+
function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBaseProps, subnetGroup: ISubnetGroup): InstanceConfig {
645652
const instanceCount = props.instances != null ? props.instances : 2;
646653
if (instanceCount < 1) {
647654
throw new Error('At least one instance is required');
@@ -696,7 +703,7 @@ function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBase
696703
? (instanceProps.performanceInsightRetention || PerformanceInsightRetention.DEFAULT)
697704
: undefined,
698705
// This is already set on the Cluster. Unclear to me whether it should be repeated or not. Better yes.
699-
dbSubnetGroupName: subnetGroup.ref,
706+
dbSubnetGroupName: subnetGroup.subnetGroupName,
700707
dbParameterGroupName: instanceParameterGroupConfig?.parameterGroupName,
701708
monitoringInterval: props.monitoringInterval && props.monitoringInterval.toSeconds(),
702709
monitoringRoleArn: monitoringRole && monitoringRole.roleArn,

packages/@aws-cdk/aws-rds/lib/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './endpoint';
1111
export * from './option-group';
1212
export * from './instance';
1313
export * from './proxy';
14+
export * from './subnet-group';
1415

1516
// AWS::RDS CloudFormation Resources:
1617
export * from './rds.generated';

packages/@aws-cdk/aws-rds/lib/instance.ts

+15-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { IParameterGroup } from './parameter-group';
1414
import { applyRemovalPolicy, defaultDeletionProtection, engineDescription, setupS3ImportExport } from './private/util';
1515
import { PerformanceInsightRetention, RotationMultiUserOptions } from './props';
1616
import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy';
17-
import { CfnDBInstance, CfnDBInstanceProps, CfnDBSubnetGroup } from './rds.generated';
17+
import { CfnDBInstance, CfnDBInstanceProps } from './rds.generated';
18+
import { ISubnetGroup, SubnetGroup } from './subnet-group';
1819

1920
/**
2021
* A database instance
@@ -502,6 +503,13 @@ export interface DatabaseInstanceNewProps {
502503
*/
503504
readonly domainRole?: iam.IRole;
504505

506+
/**
507+
* Existing subnet group for the instance.
508+
*
509+
* @default - a new subnet group will be created.
510+
*/
511+
readonly subnetGroup?: ISubnetGroup;
512+
505513
/**
506514
* Role that will be associated with this DB instance to enable S3 import.
507515
* This feature is only supported by the Microsoft SQL Server, Oracle, and PostgreSQL engines.
@@ -601,11 +609,11 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData
601609
}
602610
this.vpcPlacement = props.vpcSubnets ?? props.vpcPlacement;
603611

604-
const { subnetIds } = props.vpc.selectSubnets(this.vpcPlacement);
605-
606-
const subnetGroup = new CfnDBSubnetGroup(this, 'SubnetGroup', {
607-
dbSubnetGroupDescription: `Subnet group for ${this.node.id} database`,
608-
subnetIds,
612+
const subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'SubnetGroup', {
613+
description: `Subnet group for ${this.node.id} database`,
614+
vpc: this.vpc,
615+
vpcSubnets: this.vpcPlacement,
616+
removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined,
609617
});
610618

611619
const securityGroups = props.securityGroups || [new ec2.SecurityGroup(this, 'SecurityGroup', {
@@ -657,7 +665,7 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData
657665
copyTagsToSnapshot: props.copyTagsToSnapshot !== undefined ? props.copyTagsToSnapshot : true,
658666
dbInstanceClass: Lazy.stringValue({ produce: () => `db.${this.instanceType}` }),
659667
dbInstanceIdentifier: props.instanceIdentifier,
660-
dbSubnetGroupName: subnetGroup.ref,
668+
dbSubnetGroupName: subnetGroup.subnetGroupName,
661669
deleteAutomatedBackups: props.deleteAutomatedBackups,
662670
deletionProtection: defaultDeletionProtection(props.deletionProtection, props.removalPolicy),
663671
enableCloudwatchLogsExports: this.cloudwatchLogsExports,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import * as ec2 from '@aws-cdk/aws-ec2';
2+
import { Construct, IResource, RemovalPolicy, Resource } from '@aws-cdk/core';
3+
import { CfnDBSubnetGroup } from './rds.generated';
4+
5+
/**
6+
* Interface for a subnet group.
7+
*/
8+
export interface ISubnetGroup extends IResource {
9+
/**
10+
* The name of the subnet group.
11+
* @attribute
12+
*/
13+
readonly subnetGroupName: string;
14+
}
15+
16+
/**
17+
* Properties for creating a SubnetGroup.
18+
*/
19+
export interface SubnetGroupProps {
20+
/**
21+
* Description of the subnet group.
22+
*/
23+
readonly description: string;
24+
25+
/**
26+
* The VPC to place the subnet group in.
27+
*/
28+
readonly vpc: ec2.IVpc;
29+
30+
/**
31+
* The name of the subnet group.
32+
*
33+
* @default - a name is generated
34+
*/
35+
readonly subnetGroupName?: string;
36+
37+
/**
38+
* Which subnets within the VPC to associate with this group.
39+
*
40+
* @default - private subnets
41+
*/
42+
readonly vpcSubnets?: ec2.SubnetSelection;
43+
44+
/**
45+
* The removal policy to apply when the subnet group are removed
46+
* from the stack or replaced during an update.
47+
*
48+
* @default RemovalPolicy.DESTROY
49+
*/
50+
readonly removalPolicy?: RemovalPolicy
51+
}
52+
53+
/**
54+
* Class for creating a RDS DB subnet group
55+
*
56+
* @resource AWS::RDS::DBSubnetGroup
57+
*/
58+
export class SubnetGroup extends Resource implements ISubnetGroup {
59+
60+
/**
61+
* Imports an existing subnet group by name.
62+
*/
63+
public static fromSubnetGroupName(scope: Construct, id: string, subnetGroupName: string): ISubnetGroup {
64+
return new class extends Resource implements ISubnetGroup {
65+
public readonly subnetGroupName = subnetGroupName;
66+
}(scope, id);
67+
}
68+
69+
public readonly subnetGroupName: string;
70+
71+
constructor(scope: Construct, id: string, props: SubnetGroupProps) {
72+
super(scope, id);
73+
74+
const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets ?? { subnetType: ec2.SubnetType.PRIVATE });
75+
76+
// Using 'Default' as the resource id for historical reasons (usage from `Instance` and `Cluster`).
77+
const subnetGroup = new CfnDBSubnetGroup(this, 'Default', {
78+
dbSubnetGroupDescription: props.description,
79+
dbSubnetGroupName: props.subnetGroupName,
80+
subnetIds,
81+
});
82+
83+
if (props.removalPolicy) {
84+
subnetGroup.applyRemovalPolicy(props.removalPolicy);
85+
}
86+
87+
this.subnetGroupName = subnetGroup.ref;
88+
}
89+
}

packages/@aws-cdk/aws-rds/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
"props-physical-name:@aws-cdk/aws-rds.DatabaseInstanceReadReplicaProps",
113113
"props-physical-name:@aws-cdk/aws-rds.DatabaseSecretProps",
114114
"props-physical-name:@aws-cdk/aws-rds.OptionGroupProps",
115+
"props-physical-name:@aws-cdk/aws-rds.SubnetGroupProps",
115116
"docs-public-apis:@aws-cdk/aws-rds.SecretRotationApplication.semanticVersion",
116117
"docs-public-apis:@aws-cdk/aws-rds.SecretRotationApplication.applicationId",
117118
"docs-public-apis:@aws-cdk/aws-rds.SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER",

packages/@aws-cdk/aws-rds/test/test.cluster.ts

+50-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import * as s3 from '@aws-cdk/aws-s3';
77
import * as cdk from '@aws-cdk/core';
88
import { Test } from 'nodeunit';
99
import {
10-
AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, DatabaseCluster, DatabaseClusterEngine,
11-
DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention,
10+
AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, DatabaseCluster, DatabaseClusterEngine,
11+
DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, SubnetGroup,
1212
} from '../lib';
1313

1414
export = {
@@ -1595,6 +1595,54 @@ export = {
15951595

15961596
test.done();
15971597
},
1598+
1599+
'reuse an existing subnet group'(test: Test) {
1600+
// GIVEN
1601+
const stack = testStack();
1602+
const vpc = new ec2.Vpc(stack, 'VPC');
1603+
1604+
// WHEN
1605+
new DatabaseCluster(stack, 'Database', {
1606+
engine: DatabaseClusterEngine.aurora({ version: AuroraEngineVersion.VER_1_22_2 }),
1607+
masterUser: {
1608+
username: 'admin',
1609+
},
1610+
instanceProps: {
1611+
vpc,
1612+
},
1613+
subnetGroup: SubnetGroup.fromSubnetGroupName(stack, 'SubnetGroup', 'my-subnet-group'),
1614+
});
1615+
1616+
// THEN
1617+
expect(stack).to(haveResourceLike('AWS::RDS::DBCluster', {
1618+
DBSubnetGroupName: 'my-subnet-group',
1619+
}));
1620+
expect(stack).to(countResources('AWS::RDS::DBSubnetGroup', 0));
1621+
1622+
test.done();
1623+
},
1624+
1625+
'defaultChild returns the DB Cluster'(test: Test) {
1626+
// GIVEN
1627+
const stack = testStack();
1628+
const vpc = new ec2.Vpc(stack, 'VPC');
1629+
1630+
// WHEN
1631+
const cluster = new DatabaseCluster(stack, 'Database', {
1632+
engine: DatabaseClusterEngine.aurora({ version: AuroraEngineVersion.VER_1_22_2 }),
1633+
masterUser: {
1634+
username: 'admin',
1635+
},
1636+
instanceProps: {
1637+
vpc,
1638+
},
1639+
});
1640+
1641+
// THEN
1642+
test.ok(cluster.node.defaultChild instanceof CfnDBCluster);
1643+
1644+
test.done();
1645+
},
15981646
};
15991647

16001648
function testStack() {

packages/@aws-cdk/aws-rds/test/test.instance.ts

+28
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,34 @@ export = {
965965
},
966966
},
967967

968+
'reuse an existing subnet group'(test: Test) {
969+
new rds.DatabaseInstance(stack, 'Database', {
970+
engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }),
971+
masterUsername: 'admin',
972+
vpc,
973+
subnetGroup: rds.SubnetGroup.fromSubnetGroupName(stack, 'SubnetGroup', 'my-subnet-group'),
974+
});
975+
976+
expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', {
977+
DBSubnetGroupName: 'my-subnet-group',
978+
}));
979+
expect(stack).to(countResources('AWS::RDS::DBSubnetGroup', 0));
980+
981+
test.done();
982+
},
983+
984+
'defaultChild returns the DB Instance'(test: Test) {
985+
const instance = new rds.DatabaseInstance(stack, 'Database', {
986+
engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }),
987+
masterUsername: 'admin',
988+
vpc,
989+
});
990+
991+
// THEN
992+
test.ok(instance.node.defaultChild instanceof rds.CfnDBInstance);
993+
994+
test.done();
995+
},
968996
'S3 Import/Export': {
969997
'instance with s3 import and export buckets'(test: Test) {
970998
new rds.DatabaseInstance(stack, 'DB', {

0 commit comments

Comments
 (0)