From 6ea3ede6304ebe017e7c8f9fe64a8d8bf141a2a0 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 20 Mar 2019 21:23:26 +0100 Subject: [PATCH 1/3] feat(rds): enable storage encryption by default Storage encryption with the default master key is now enabled by default when creating a new cluster. BREAKING CHANGE: Storage encryption is enabled by default (update requires replacement) BREAKING CHANGE: Replaced `kmsKeyArn: string` by `kmsKey: kms.IEncryptionKey` in `DatabaseClusterProps` --- packages/@aws-cdk/aws-rds/README.md | 4 +-- packages/@aws-cdk/aws-rds/lib/cluster.ts | 18 +++++++--- .../integ.cluster-rotation.lit.expected.json | 2 +- .../@aws-cdk/aws-rds/test/integ.cluster.ts | 2 +- .../@aws-cdk/aws-rds/test/test.cluster.ts | 36 ++++++++++++++++++- 5 files changed, 53 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 74c6a8f402e93..a7e3f92c06050 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -33,7 +33,7 @@ const cluster = new DatabaseCluster(this, 'Database', { } }); ``` -By default, the master password will be generated and stored in AWS Secrets Manager. +By default, the master password will be generated and stored in AWS Secrets Manager and the storage will be encrypted with the default master key. Your cluster will be empty by default. To add a default database upon construction, specify the `defaultDatabaseName` attribute. @@ -61,7 +61,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 66fda81a3fac2..0bdde32378f4a 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,18 @@ export interface DatabaseClusterProps { defaultDatabaseName?: string; /** - * ARN of KMS key if you want to enable storage encryption + * Whether to enable storage encryption + * + * @default true + */ + storageEncrypted?: boolean + + /** + * The KMS key for storage encryption + * + * @default default master key */ - kmsKeyArn?: string; + kmsKey?: kms.IEncryptionKey; /** * A daily time range in 24-hours UTC format in which backups preferably execute. @@ -261,8 +271,8 @@ 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 !== false, }); this.clusterIdentifier = cluster.ref; 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..f52a905ffd2a4 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,7 @@ ] ] }, - "StorageEncrypted": false, + "StorageEncrypted": true, "VpcSecurityGroupIds": [ { "Fn::GetAtt": [ diff --git a/packages/@aws-cdk/aws-rds/test/integ.cluster.ts b/packages/@aws-cdk/aws-rds/test/integ.cluster.ts index 468b1d442ce77..5bc68d55df7fe 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..e0406bcf407af 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 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'; @@ -29,7 +30,8 @@ export = { DBSubnetGroupName: { Ref: "DatabaseSubnets56F17B9A" }, MasterUsername: "admin", MasterUserPassword: "tooshort", - VpcSecurityGroupIds: [ {"Fn::GetAtt": ["DatabaseSecurityGroup5C91FDCB", "GroupId"]}] + VpcSecurityGroupIds: [ {"Fn::GetAtt": ["DatabaseSecurityGroup5C91FDCB", "GroupId"]}], + StorageEncrypted: true })); test.done(); @@ -232,6 +234,38 @@ 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' + ] + }, + StorageEncrypted: true + })); + test.done(); } }; From ba7282a35222fd689ca2bfd141fbe89c32c87b2b Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Wed, 20 Mar 2019 22:45:08 +0100 Subject: [PATCH 2/3] Remove default encryption --- packages/@aws-cdk/aws-rds/README.md | 2 +- packages/@aws-cdk/aws-rds/lib/cluster.ts | 7 ++++--- .../aws-rds/test/integ.cluster-rotation.lit.expected.json | 1 - packages/@aws-cdk/aws-rds/test/test.cluster.ts | 6 ++---- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index a7e3f92c06050..0b0faa5d70771 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -33,7 +33,7 @@ const cluster = new DatabaseCluster(this, 'Database', { } }); ``` -By default, the master password will be generated and stored in AWS Secrets Manager and the storage will be encrypted with the default master key. +By default, the master password will be generated and stored in AWS Secrets Manager. Your cluster will be empty by default. To add a default database upon construction, specify the `defaultDatabaseName` attribute. diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 0bdde32378f4a..6a0c77528f368 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -75,12 +75,13 @@ export interface DatabaseClusterProps { /** * Whether to enable storage encryption * - * @default true + * @default false */ storageEncrypted?: boolean /** - * The KMS key for storage encryption + * The KMS key for storage encryption. If specified `storageEncrypted` + * will be set to `true`. * * @default default master key */ @@ -272,7 +273,7 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu databaseName: props.defaultDatabaseName, // Encryption kmsKeyId: props.kmsKey && props.kmsKey.keyArn, - storageEncrypted: props.kmsKey ? true : props.storageEncrypted !== false, + storageEncrypted: props.kmsKey ? true : props.storageEncrypted }); this.clusterIdentifier = cluster.ref; 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 f52a905ffd2a4..778bcbeb859d7 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": true, "VpcSecurityGroupIds": [ { "Fn::GetAtt": [ diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index e0406bcf407af..b402ccda35e7d 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -30,8 +30,7 @@ export = { DBSubnetGroupName: { Ref: "DatabaseSubnets56F17B9A" }, MasterUsername: "admin", MasterUserPassword: "tooshort", - VpcSecurityGroupIds: [ {"Fn::GetAtt": ["DatabaseSecurityGroup5C91FDCB", "GroupId"]}], - StorageEncrypted: true + VpcSecurityGroupIds: [ {"Fn::GetAtt": ["DatabaseSecurityGroup5C91FDCB", "GroupId"]}] })); test.done(); @@ -262,8 +261,7 @@ export = { 'Key961B73FD', 'Arn' ] - }, - StorageEncrypted: true + } })); test.done(); From 4920f07916c64d5ec6d0fc98679b52c873e76af6 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Fri, 22 Mar 2019 15:05:28 +0100 Subject: [PATCH 3/3] Add DeletionPolicy and UpdateReplacePolicy --- packages/@aws-cdk/aws-rds/lib/cluster.ts | 15 ++++++++++++ .../integ.cluster-rotation.lit.expected.json | 12 +++++++--- .../aws-rds/test/integ.cluster.expected.json | 14 +++++++---- .../@aws-cdk/aws-rds/test/test.cluster.ts | 23 +++++++++++++------ 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index 617729064db42..e44168872f4fc 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -102,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 } /** @@ -276,6 +284,10 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu 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); @@ -314,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 778bcbeb859d7..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 @@ -658,7 +658,9 @@ ] } ] - } + }, + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" }, "DatabaseInstance1844F58FD": { "Type": "AWS::RDS::DBInstance", @@ -676,7 +678,9 @@ "VPCPrivateSubnet1DefaultRouteAE1D6490", "VPCPrivateSubnet2DefaultRouteF4F5CFD2", "VPCPrivateSubnet3DefaultRoute27F311AE" - ] + ], + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain" }, "DatabaseInstance2AA380DEE": { "Type": "AWS::RDS::DBInstance", @@ -694,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/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index b402ccda35e7d..ce5d39e104573 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -1,4 +1,4 @@ -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'); @@ -26,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(); },