Skip to content

Commit

Permalink
feat(rds): region replication for generated secrets (aws#16497)
Browse files Browse the repository at this point in the history
Add a `replicaRegions` option to `fromGeneratedSecret()` both in
`Credentials` and `SnapshotCredentials`.

Closes aws#16480


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
jogold authored Sep 20, 2021
1 parent 68fb63e commit 1e9d8be
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 0 deletions.
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-rds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-rds/lib/database-secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

/**
Expand All @@ -77,6 +84,7 @@ export class DatabaseSecret extends secretsmanager.Secret {
generateStringKey: 'password',
excludeCharacters,
},
replicaRegions: props.replicaRegions,
});

if (props.replaceOnPasswordCriteriaChanges) {
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-rds/lib/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1065,6 +1065,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme
encryptionKey: credentials.encryptionKey,
excludeCharacters: credentials.excludeCharacters,
replaceOnPasswordCriteriaChanges: credentials.replaceOnPasswordCriteriaChanges,
replicaRegions: credentials.replicaRegions,
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-rds/lib/private/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 28 additions & 0 deletions packages/@aws-cdk/aws-rds/lib/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

/**
Expand Down Expand Up @@ -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[];
}

/**
Expand All @@ -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[];
}

/**
Expand Down Expand Up @@ -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[];
}

/**
Expand Down
24 changes: 24 additions & 0 deletions packages/@aws-cdk/aws-rds/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
35 changes: 35 additions & 0 deletions packages/@aws-cdk/aws-rds/test/instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down

0 comments on commit 1e9d8be

Please sign in to comment.