Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rds): region replication for generated secrets #16497

Merged
merged 4 commits into from
Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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