From 99ab46d8682ec4b39b19aba98567623e07540e60 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 26 Mar 2019 14:26:49 +0100 Subject: [PATCH] feat(rds): cluster retention, reference KMS key by object (#2063) Add control over DeletionPolicy and UpdateReplacePolicy, both defaulting to Retain to avoid data loss. Reference encryption key reference with key interface. BREAKING CHANGE: Replaced `kmsKeyArn: string` by `kmsKey: kms.IEncryptionKey` in `DatabaseClusterProps` --- packages/@aws-cdk/aws-rds/README.md | 2 +- packages/@aws-cdk/aws-rds/lib/cluster.ts | 34 ++++++++++-- .../integ.cluster-rotation.lit.expected.json | 13 +++-- .../aws-rds/test/integ.cluster.expected.json | 14 +++-- .../@aws-cdk/aws-rds/test/integ.cluster.ts | 2 +- .../@aws-cdk/aws-rds/test/test.cluster.ts | 55 ++++++++++++++++--- 6 files changed, 99 insertions(+), 21 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 6fe8b9ba3cbd6..958fe1e3f4988 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -68,7 +68,7 @@ When the master password is generated and stored in AWS Secrets Manager, it can Rotation of the master password is also supported for an existing cluster: ```ts -new rds.RotationSingleUser(stack, 'Rotation', { +new RotationSingleUser(stack, 'Rotation', { secret: importedSecret, engine: DatabaseEngine.Oracle, target: importedCluster, diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 39465a1231a5c..e44168872f4fc 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -1,4 +1,5 @@ import ec2 = require('@aws-cdk/aws-ec2'); +import kms = require('@aws-cdk/aws-kms'); import secretsmanager = require('@aws-cdk/aws-secretsmanager'); import cdk = require('@aws-cdk/cdk'); import { IClusterParameterGroup } from './cluster-parameter-group'; @@ -72,9 +73,19 @@ export interface DatabaseClusterProps { defaultDatabaseName?: string; /** - * ARN of KMS key if you want to enable storage encryption + * Whether to enable storage encryption + * + * @default false + */ + storageEncrypted?: boolean + + /** + * The KMS key for storage encryption. If specified `storageEncrypted` + * will be set to `true`. + * + * @default default master key */ - kmsKeyArn?: string; + kmsKey?: kms.IEncryptionKey; /** * A daily time range in 24-hours UTC format in which backups preferably execute. @@ -91,6 +102,14 @@ export interface DatabaseClusterProps { * @default No parameter group */ parameterGroup?: IClusterParameterGroup; + + /** + * The CloudFormation policy to apply when the cluster and its instances + * are removed from the stack or replaced during an update. + * + * @default Retain + */ + deleteReplacePolicy?: cdk.DeletionPolicy } /** @@ -261,10 +280,14 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu preferredMaintenanceWindow: props.preferredMaintenanceWindow, databaseName: props.defaultDatabaseName, // Encryption - kmsKeyId: props.kmsKeyArn, - storageEncrypted: props.kmsKeyArn ? true : false, + kmsKeyId: props.kmsKey && props.kmsKey.keyArn, + storageEncrypted: props.kmsKey ? true : props.storageEncrypted }); + const deleteReplacePolicy = props.deleteReplacePolicy || cdk.DeletionPolicy.Retain; + cluster.options.deletionPolicy = deleteReplacePolicy; + cluster.options.updateReplacePolicy = deleteReplacePolicy; + this.clusterIdentifier = cluster.ref; this.clusterEndpoint = new Endpoint(cluster.dbClusterEndpointAddress, cluster.dbClusterEndpointPort); this.readerEndpoint = new Endpoint(cluster.dbClusterReadEndpointAddress, cluster.dbClusterEndpointPort); @@ -303,6 +326,9 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu dbSubnetGroupName: subnetGroup.ref, }); + instance.options.deletionPolicy = deleteReplacePolicy; + instance.options.updateReplacePolicy = deleteReplacePolicy; + // We must have a dependency on the NAT gateway provider here to create // things in the right order. instance.node.addDependency(internetConnected); diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json index 6f80809f40e1e..774d57074bb82 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster-rotation.lit.expected.json @@ -650,7 +650,6 @@ ] ] }, - "StorageEncrypted": false, "VpcSecurityGroupIds": [ { "Fn::GetAtt": [ @@ -659,7 +658,9 @@ ] } ] - } + }, + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -677,7 +678,9 @@ "VPCPrivateSubnet1DefaultRouteAE1D6490", "VPCPrivateSubnet2DefaultRouteF4F5CFD2", "VPCPrivateSubnet3DefaultRoute27F311AE" - ] + ], + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -695,7 +698,9 @@ "VPCPrivateSubnet1DefaultRouteAE1D6490", "VPCPrivateSubnet2DefaultRouteF4F5CFD2", "VPCPrivateSubnet3DefaultRoute27F311AE" - ] + ], + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" }, "DatabaseRotationSecurityGroup17736B63": { "Type": "AWS::EC2::SecurityGroup", diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json index d8bb54664a09b..6f09d047317fe 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.expected.json @@ -484,7 +484,9 @@ ] } ] - } + }, + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -502,7 +504,9 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ] + ], + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -520,7 +524,9 @@ "DependsOn": [ "VPCPublicSubnet1DefaultRoute91CEF279", "VPCPublicSubnet2DefaultRouteB7481BBA" - ] + ], + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts index 8a787668a2533..281f22ed4e89b 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts @@ -28,7 +28,7 @@ const cluster = new DatabaseCluster(stack, 'Database', { vpc }, parameterGroup: params, - kmsKeyArn: kmsKey.keyArn, + kmsKey, }); cluster.connections.allowDefaultPortFromAnyIpv4('Open to the world'); diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index cda81e806128f..ce5d39e104573 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -1,5 +1,6 @@ -import { expect, haveResource } from '@aws-cdk/assert'; +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; import ec2 = require('@aws-cdk/aws-ec2'); +import kms = require('@aws-cdk/aws-kms'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import { ClusterParameterGroup, DatabaseCluster, DatabaseClusterEngine } from '../lib'; @@ -25,12 +26,21 @@ export = { // THEN expect(stack).to(haveResource('AWS::RDS::DBCluster', { - Engine: "aurora", - DBSubnetGroupName: { Ref: "DatabaseSubnets56F17B9A" }, - MasterUsername: "admin", - MasterUserPassword: "tooshort", - VpcSecurityGroupIds: [ {"Fn::GetAtt": ["DatabaseSecurityGroup5C91FDCB", "GroupId"]}] - })); + Properties: { + Engine: "aurora", + DBSubnetGroupName: { Ref: "DatabaseSubnets56F17B9A" }, + MasterUsername: "admin", + MasterUserPassword: "tooshort", + VpcSecurityGroupIds: [ {"Fn::GetAtt": ["DatabaseSecurityGroup5C91FDCB", "GroupId"]}] + }, + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain' + }, ResourcePart.CompleteDefinition)); + + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + DeletionPolicy: 'Retain', + UpdateReplacePolicy: 'Retain' + }, ResourcePart.CompleteDefinition)); test.done(); }, @@ -232,6 +242,37 @@ export = { } })); + test.done(); + }, + + 'create an encrypted cluster with custom KMS key'(test: Test) { + // GIVEN + const stack = testStack(); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AuroraMysql, + masterUser: { + username: 'admin' + }, + instanceProps: { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Burstable2, ec2.InstanceSize.Small), + vpc + }, + kmsKey: new kms.EncryptionKey(stack, 'Key') + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBCluster', { + KmsKeyId: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn' + ] + } + })); + test.done(); } };