Skip to content

Commit

Permalink
feat(rds): support existing cluster subnet groups (#10391)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
njlynch authored Sep 18, 2020
1 parent 75811c1 commit a1df511
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 20 deletions.
29 changes: 18 additions & 11 deletions packages/@aws-cdk/aws-rds/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import { IParameterGroup } from './parameter-group';
import { applyRemovalPolicy, defaultDeletionProtection, setupS3ImportExport } from './private/util';
import { BackupProps, InstanceProps, Login, PerformanceInsightRetention, RotationMultiUserOptions } from './props';
import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy';
import { CfnDBCluster, CfnDBClusterProps, CfnDBInstance, CfnDBSubnetGroup } from './rds.generated';
import { CfnDBCluster, CfnDBClusterProps, CfnDBInstance } from './rds.generated';
import { ISubnetGroup, SubnetGroup } from './subnet-group';

/**
* Common properties for a new database cluster or cluster from snapshot.
Expand Down Expand Up @@ -213,6 +214,13 @@ interface DatabaseClusterBaseProps {
* @default - None
*/
readonly s3ExportBuckets?: s3.IBucket[];

/**
* Existing subnet group for the cluster.
*
* @default - a new subnet group will be created.
*/
readonly subnetGroup?: ISubnetGroup;
}

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

protected readonly newCfnProps: CfnDBClusterProps;
protected readonly subnetGroup: CfnDBSubnetGroup;
protected readonly securityGroups: ec2.ISecurityGroup[];
protected readonly subnetGroup: ISubnetGroup;

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

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

this.securityGroups = props.instanceProps.securityGroups ?? [
new ec2.SecurityGroup(this, 'SecurityGroup', {
Expand Down Expand Up @@ -330,7 +337,7 @@ abstract class DatabaseClusterNew extends DatabaseClusterBase {
engine: props.engine.engineType,
engineVersion: props.engine.engineVersion?.fullVersion,
dbClusterIdentifier: props.clusterIdentifier,
dbSubnetGroupName: this.subnetGroup.ref,
dbSubnetGroupName: this.subnetGroup.subnetGroupName,
vpcSecurityGroupIds: this.securityGroups.map(sg => sg.securityGroupId),
port: props.port ?? clusterEngineBindConfig.port,
dbClusterParameterGroupName: clusterParameterGroupConfig?.parameterGroupName,
Expand Down Expand Up @@ -641,7 +648,7 @@ interface InstanceConfig {
* A function rather than a protected method on ``DatabaseClusterNew`` to avoid exposing
* ``DatabaseClusterNew`` and ``DatabaseClusterBaseProps`` in the API.
*/
function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBaseProps, subnetGroup: CfnDBSubnetGroup): InstanceConfig {
function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBaseProps, subnetGroup: ISubnetGroup): InstanceConfig {
const instanceCount = props.instances != null ? props.instances : 2;
if (instanceCount < 1) {
throw new Error('At least one instance is required');
Expand Down Expand Up @@ -696,7 +703,7 @@ function createInstances(cluster: DatabaseClusterNew, props: DatabaseClusterBase
? (instanceProps.performanceInsightRetention || PerformanceInsightRetention.DEFAULT)
: undefined,
// This is already set on the Cluster. Unclear to me whether it should be repeated or not. Better yes.
dbSubnetGroupName: subnetGroup.ref,
dbSubnetGroupName: subnetGroup.subnetGroupName,
dbParameterGroupName: instanceParameterGroupConfig?.parameterGroupName,
monitoringInterval: props.monitoringInterval && props.monitoringInterval.toSeconds(),
monitoringRoleArn: monitoringRole && monitoringRole.roleArn,
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-rds/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './endpoint';
export * from './option-group';
export * from './instance';
export * from './proxy';
export * from './subnet-group';

// AWS::RDS CloudFormation Resources:
export * from './rds.generated';
Expand Down
22 changes: 15 additions & 7 deletions packages/@aws-cdk/aws-rds/lib/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import { IParameterGroup } from './parameter-group';
import { applyRemovalPolicy, defaultDeletionProtection, engineDescription, setupS3ImportExport } from './private/util';
import { PerformanceInsightRetention, RotationMultiUserOptions } from './props';
import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy';
import { CfnDBInstance, CfnDBInstanceProps, CfnDBSubnetGroup } from './rds.generated';
import { CfnDBInstance, CfnDBInstanceProps } from './rds.generated';
import { ISubnetGroup, SubnetGroup } from './subnet-group';

/**
* A database instance
Expand Down Expand Up @@ -502,6 +503,13 @@ export interface DatabaseInstanceNewProps {
*/
readonly domainRole?: iam.IRole;

/**
* Existing subnet group for the instance.
*
* @default - a new subnet group will be created.
*/
readonly subnetGroup?: ISubnetGroup;

/**
* Role that will be associated with this DB instance to enable S3 import.
* This feature is only supported by the Microsoft SQL Server, Oracle, and PostgreSQL engines.
Expand Down Expand Up @@ -601,11 +609,11 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData
}
this.vpcPlacement = props.vpcSubnets ?? props.vpcPlacement;

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

const subnetGroup = new CfnDBSubnetGroup(this, 'SubnetGroup', {
dbSubnetGroupDescription: `Subnet group for ${this.node.id} database`,
subnetIds,
const subnetGroup = props.subnetGroup ?? new SubnetGroup(this, 'SubnetGroup', {
description: `Subnet group for ${this.node.id} database`,
vpc: this.vpc,
vpcSubnets: this.vpcPlacement,
removalPolicy: props.removalPolicy === RemovalPolicy.RETAIN ? props.removalPolicy : undefined,
});

const securityGroups = props.securityGroups || [new ec2.SecurityGroup(this, 'SecurityGroup', {
Expand Down Expand Up @@ -657,7 +665,7 @@ abstract class DatabaseInstanceNew extends DatabaseInstanceBase implements IData
copyTagsToSnapshot: props.copyTagsToSnapshot !== undefined ? props.copyTagsToSnapshot : true,
dbInstanceClass: Lazy.stringValue({ produce: () => `db.${this.instanceType}` }),
dbInstanceIdentifier: props.instanceIdentifier,
dbSubnetGroupName: subnetGroup.ref,
dbSubnetGroupName: subnetGroup.subnetGroupName,
deleteAutomatedBackups: props.deleteAutomatedBackups,
deletionProtection: defaultDeletionProtection(props.deletionProtection, props.removalPolicy),
enableCloudwatchLogsExports: this.cloudwatchLogsExports,
Expand Down
89 changes: 89 additions & 0 deletions packages/@aws-cdk/aws-rds/lib/subnet-group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as ec2 from '@aws-cdk/aws-ec2';
import { Construct, IResource, RemovalPolicy, Resource } from '@aws-cdk/core';
import { CfnDBSubnetGroup } from './rds.generated';

/**
* Interface for a subnet group.
*/
export interface ISubnetGroup extends IResource {
/**
* The name of the subnet group.
* @attribute
*/
readonly subnetGroupName: string;
}

/**
* Properties for creating a SubnetGroup.
*/
export interface SubnetGroupProps {
/**
* Description of the subnet group.
*/
readonly description: string;

/**
* The VPC to place the subnet group in.
*/
readonly vpc: ec2.IVpc;

/**
* The name of the subnet group.
*
* @default - a name is generated
*/
readonly subnetGroupName?: string;

/**
* Which subnets within the VPC to associate with this group.
*
* @default - private subnets
*/
readonly vpcSubnets?: ec2.SubnetSelection;

/**
* The removal policy to apply when the subnet group are removed
* from the stack or replaced during an update.
*
* @default RemovalPolicy.DESTROY
*/
readonly removalPolicy?: RemovalPolicy
}

/**
* Class for creating a RDS DB subnet group
*
* @resource AWS::RDS::DBSubnetGroup
*/
export class SubnetGroup extends Resource implements ISubnetGroup {

/**
* Imports an existing subnet group by name.
*/
public static fromSubnetGroupName(scope: Construct, id: string, subnetGroupName: string): ISubnetGroup {
return new class extends Resource implements ISubnetGroup {
public readonly subnetGroupName = subnetGroupName;
}(scope, id);
}

public readonly subnetGroupName: string;

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

const { subnetIds } = props.vpc.selectSubnets(props.vpcSubnets ?? { subnetType: ec2.SubnetType.PRIVATE });

// Using 'Default' as the resource id for historical reasons (usage from `Instance` and `Cluster`).
const subnetGroup = new CfnDBSubnetGroup(this, 'Default', {
dbSubnetGroupDescription: props.description,
dbSubnetGroupName: props.subnetGroupName,
subnetIds,
});

if (props.removalPolicy) {
subnetGroup.applyRemovalPolicy(props.removalPolicy);
}

this.subnetGroupName = subnetGroup.ref;
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-rds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"props-physical-name:@aws-cdk/aws-rds.DatabaseInstanceReadReplicaProps",
"props-physical-name:@aws-cdk/aws-rds.DatabaseSecretProps",
"props-physical-name:@aws-cdk/aws-rds.OptionGroupProps",
"props-physical-name:@aws-cdk/aws-rds.SubnetGroupProps",
"docs-public-apis:@aws-cdk/aws-rds.SecretRotationApplication.semanticVersion",
"docs-public-apis:@aws-cdk/aws-rds.SecretRotationApplication.applicationId",
"docs-public-apis:@aws-cdk/aws-rds.SecretRotationApplication.SQLSERVER_ROTATION_SINGLE_USER",
Expand Down
52 changes: 50 additions & 2 deletions packages/@aws-cdk/aws-rds/test/test.cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import * as s3 from '@aws-cdk/aws-s3';
import * as cdk from '@aws-cdk/core';
import { Test } from 'nodeunit';
import {
AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, DatabaseCluster, DatabaseClusterEngine,
DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention,
AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, DatabaseCluster, DatabaseClusterEngine,
DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, SubnetGroup,
} from '../lib';

export = {
Expand Down Expand Up @@ -1595,6 +1595,54 @@ export = {

test.done();
},

'reuse an existing subnet group'(test: Test) {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
new DatabaseCluster(stack, 'Database', {
engine: DatabaseClusterEngine.aurora({ version: AuroraEngineVersion.VER_1_22_2 }),
masterUser: {
username: 'admin',
},
instanceProps: {
vpc,
},
subnetGroup: SubnetGroup.fromSubnetGroupName(stack, 'SubnetGroup', 'my-subnet-group'),
});

// THEN
expect(stack).to(haveResourceLike('AWS::RDS::DBCluster', {
DBSubnetGroupName: 'my-subnet-group',
}));
expect(stack).to(countResources('AWS::RDS::DBSubnetGroup', 0));

test.done();
},

'defaultChild returns the DB Cluster'(test: Test) {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
const cluster = new DatabaseCluster(stack, 'Database', {
engine: DatabaseClusterEngine.aurora({ version: AuroraEngineVersion.VER_1_22_2 }),
masterUser: {
username: 'admin',
},
instanceProps: {
vpc,
},
});

// THEN
test.ok(cluster.node.defaultChild instanceof CfnDBCluster);

test.done();
},
};

function testStack() {
Expand Down
28 changes: 28 additions & 0 deletions packages/@aws-cdk/aws-rds/test/test.instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,34 @@ export = {
},
},

'reuse an existing subnet group'(test: Test) {
new rds.DatabaseInstance(stack, 'Database', {
engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }),
masterUsername: 'admin',
vpc,
subnetGroup: rds.SubnetGroup.fromSubnetGroupName(stack, 'SubnetGroup', 'my-subnet-group'),
});

expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', {
DBSubnetGroupName: 'my-subnet-group',
}));
expect(stack).to(countResources('AWS::RDS::DBSubnetGroup', 0));

test.done();
},

'defaultChild returns the DB Instance'(test: Test) {
const instance = new rds.DatabaseInstance(stack, 'Database', {
engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }),
masterUsername: 'admin',
vpc,
});

// THEN
test.ok(instance.node.defaultChild instanceof rds.CfnDBInstance);

test.done();
},
'S3 Import/Export': {
'instance with s3 import and export buckets'(test: Test) {
new rds.DatabaseInstance(stack, 'DB', {
Expand Down
Loading

0 comments on commit a1df511

Please sign in to comment.