From 1e9d8be0a81e1f875bf8b31c701e1069bb98728e Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Mon, 20 Sep 2021 20:59:50 +0200 Subject: [PATCH] feat(rds): region replication for generated secrets (#16497) Add a `replicaRegions` option to `fromGeneratedSecret()` both in `Credentials` and `SnapshotCredentials`. Closes #16480 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-rds/README.md | 17 +++++++++ .../@aws-cdk/aws-rds/lib/database-secret.ts | 8 +++++ packages/@aws-cdk/aws-rds/lib/instance.ts | 1 + packages/@aws-cdk/aws-rds/lib/private/util.ts | 1 + packages/@aws-cdk/aws-rds/lib/props.ts | 28 +++++++++++++++ .../@aws-cdk/aws-rds/test/cluster.test.ts | 24 +++++++++++++ .../@aws-cdk/aws-rds/test/instance.test.ts | 35 +++++++++++++++++++ 7 files changed, 114 insertions(+) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 054a660f3529e..cad785735dfb6 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -200,6 +200,23 @@ new rds.DatabaseInstance(this, 'InstanceWithSecretLogin', { }); ``` +Secrets generated by `fromGeneratedSecret()` can be customized: + +```ts +const myKey = kms.Key(this, 'MyKey'); + +new rds.DatabaseInstance(this, 'InstanceWithCustomizedSecret', { + engine, + vpc, + credentials: rds.Credentials.fromGeneratedSecret('postgres', { + secretName: 'my-cool-name', + encryptionKey: myKey, + excludeCharacters: ['!&*^#@()'], + replicaRegions: [{ region: 'eu-west-1' }, { region: 'eu-west-2' }], + }), +}); +``` + ## Connecting To control who can access the cluster or instance, use the `.connections` attribute. RDS databases have diff --git a/packages/@aws-cdk/aws-rds/lib/database-secret.ts b/packages/@aws-cdk/aws-rds/lib/database-secret.ts index 0a0537b23a63e..25b691c92f86b 100644 --- a/packages/@aws-cdk/aws-rds/lib/database-secret.ts +++ b/packages/@aws-cdk/aws-rds/lib/database-secret.ts @@ -53,6 +53,13 @@ export interface DatabaseSecretProps { * @default false */ readonly replaceOnPasswordCriteriaChanges?: boolean; + + /** + * A list of regions where to replicate this secret. + * + * @default - Secret is not replicated + */ + readonly replicaRegions?: secretsmanager.ReplicaRegion[]; } /** @@ -77,6 +84,7 @@ export class DatabaseSecret extends secretsmanager.Secret { generateStringKey: 'password', excludeCharacters, }, + replicaRegions: props.replicaRegions, }); if (props.replaceOnPasswordCriteriaChanges) { diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 6ea2418ee1a97..8d285b7375006 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -1065,6 +1065,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme encryptionKey: credentials.encryptionKey, excludeCharacters: credentials.excludeCharacters, replaceOnPasswordCriteriaChanges: credentials.replaceOnPasswordCriteriaChanges, + replicaRegions: credentials.replicaRegions, }); } diff --git a/packages/@aws-cdk/aws-rds/lib/private/util.ts b/packages/@aws-cdk/aws-rds/lib/private/util.ts index df9ace2dbd3bc..c142a903bad7a 100644 --- a/packages/@aws-cdk/aws-rds/lib/private/util.ts +++ b/packages/@aws-cdk/aws-rds/lib/private/util.ts @@ -100,6 +100,7 @@ export function renderCredentials(scope: Construct, engine: IEngine, credentials // if username must be referenced as a string we can safely replace the // secret when customization options are changed without risking a replacement replaceOnPasswordCriteriaChanges: credentials?.usernameAsString, + replicaRegions: renderedCredentials.replicaRegions, }), // pass username if it must be referenced as a string credentials?.usernameAsString ? renderedCredentials.username : undefined, diff --git a/packages/@aws-cdk/aws-rds/lib/props.ts b/packages/@aws-cdk/aws-rds/lib/props.ts index c57e8ccee8fd6..e9ed3c126cd65 100644 --- a/packages/@aws-cdk/aws-rds/lib/props.ts +++ b/packages/@aws-cdk/aws-rds/lib/props.ts @@ -147,6 +147,13 @@ export interface CredentialsBaseOptions { * @default - the DatabaseSecret default exclude character set (" %+~`#$&*()|[]{}:;<>?!'/@\"\\") */ readonly excludeCharacters?: string; + + /** + * A list of regions where to replicate this secret. + * + * @default - Secret is not replicated + */ + readonly replicaRegions?: secretsmanager.ReplicaRegion[]; } /** @@ -285,6 +292,13 @@ export abstract class Credentials { * @default - the DatabaseSecret default exclude character set (" %+~`#$&*()|[]{}:;<>?!'/@\"\\") */ public abstract readonly excludeCharacters?: string; + + /** + * A list of regions where to replicate the generated secret. + * + * @default - Secret is not replicated + */ + public abstract readonly replicaRegions?: secretsmanager.ReplicaRegion[]; } /** @@ -304,6 +318,13 @@ export interface SnapshotCredentialsFromGeneratedPasswordOptions { * @default - the DatabaseSecret default exclude character set (" %+~`#$&*()|[]{}:;<>?!'/@\"\\") */ readonly excludeCharacters?: string; + + /** + * A list of regions where to replicate this secret. + * + * @default - Secret is not replicated + */ + readonly replicaRegions?: secretsmanager.ReplicaRegion[]; } /** @@ -420,6 +441,13 @@ export abstract class SnapshotCredentials { * @default - the DatabaseSecret default exclude character set (" %+~`#$&*()|[]{}:;<>?!'/@\"\\") */ public abstract readonly excludeCharacters?: string; + + /** + * A list of regions where to replicate the generated secret. + * + * @default - Secret is not replicated + */ + public abstract readonly replicaRegions?: secretsmanager.ReplicaRegion[]; } /** diff --git a/packages/@aws-cdk/aws-rds/test/cluster.test.ts b/packages/@aws-cdk/aws-rds/test/cluster.test.ts index 4e9de20a9fa5d..ed3ec587d7c86 100644 --- a/packages/@aws-cdk/aws-rds/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-rds/test/cluster.test.ts @@ -1784,8 +1784,32 @@ describe('cluster', () => { ], }, }); + }); + test('fromGeneratedSecret with replica regions', () => { + // 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 }), + credentials: Credentials.fromGeneratedSecret('admin', { + replicaRegions: [{ region: 'eu-west-1' }], + }), + instanceProps: { + vpc, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + ReplicaRegions: [ + { + Region: 'eu-west-1', + }, + ], + }); }); test('can set custom name to database secret by fromSecret', () => { diff --git a/packages/@aws-cdk/aws-rds/test/instance.test.ts b/packages/@aws-cdk/aws-rds/test/instance.test.ts index f0591eb2469d1..7cabc2258dc6b 100644 --- a/packages/@aws-cdk/aws-rds/test/instance.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance.test.ts @@ -348,8 +348,25 @@ describe('instance', () => { 'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'InstanceSecretB6DFA6BE8ee0a797cad8a68dbeb85f8698cdb5bb' }, ':SecretString:password::}}']], }, }); + }); + test('fromGeneratedSecret with replica regions', () => { + new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { + snapshotIdentifier: 'my-snapshot', + engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), + vpc, + credentials: rds.SnapshotCredentials.fromGeneratedSecret('admin', { + replicaRegions: [{ region: 'eu-west-1' }], + }), + }); + expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + ReplicaRegions: [ + { + Region: 'eu-west-1', + }, + ], + }); }); test('throws if generating a new password without a username', () => { @@ -1227,8 +1244,26 @@ describe('instance', () => { ], }, }); + }); + test('fromGeneratedSecret with replica regions', () => { + // WHEN + new rds.DatabaseInstance(stack, 'Database', { + engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }), + vpc, + credentials: rds.Credentials.fromGeneratedSecret('postgres', { + replicaRegions: [{ region: 'eu-west-1' }], + }), + }); + // THEN + expect(stack).toHaveResource('AWS::SecretsManager::Secret', { + ReplicaRegions: [ + { + Region: 'eu-west-1', + }, + ], + }); }); test('fromPassword', () => {