-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.ts
155 lines (136 loc) · 6.59 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { Construct } from 'constructs';
import * as pathlib from 'path';
import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import type * as rds from 'aws-cdk-lib/aws-rds';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import type * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as cr from 'aws-cdk-lib/custom-resources';
import * as lambda from 'aws-cdk-lib/aws-lambda';
export interface PostgresUserAndDatabaseProps {
dbCluster: rds.IDatabaseCluster;
dbSecret: secretsmanager.ISecret;
// Must have a secretString with the following keys:
// - username
// - password
userSecret?: secretsmanager.ISecret;
// Must set this if userSecret is not provided, and a password
// will be generated with keys username and password (as well as
// dbname, host, and port)
username?: string;
databaseName: string;
vpc: ec2.IVpc;
// Defaults to Fail
onCreateIfExists?: 'Fail' | 'Adopt' | 'DeleteAndRecreate';
// Defaults to Delete
onDelete?: 'Delete' | 'Retain';
// Defaults to Ignore
onUpdateIfUserDoesNotExist?: 'Ignore' | 'Create';
// Defaults to Ignore
onUpdateIfDatabaseDoesNotExist?: 'Ignore' | 'Create';
// Defaults to Never
onUpdateSetUserPassword?: 'Always' | 'Never';
// Defaults to Never
onUpdateSetUserPermissions?: 'Always' | 'Never';
// Defaults to Never
onUpdateSetDatabaseOwnership?: 'Always' | 'Never';
// Check for a new secret version every time the custom resource is updated
// Defaults to false
onUpdateCheckSecretVersion?: boolean;
}
export const DEFAULT_PASSWORD_EXCLUDE_CHARS = ' %+~`#$&*()|[]{}:;<>?!\'/@"\\';
export class PostgresUserAndDatabase extends Construct {
readonly userSecret: secretsmanager.ISecret;
constructor(scope: Construct, id: string, props: PostgresUserAndDatabaseProps) {
super(scope, id);
// Using a custom resource, create a user and database in the RDS cluster
// https://docs.aws.amazon.com/cdk/api/latest/docs/custom-resources-readme.html
const handler = new lambda.Function(this, 'OnEvent', {
code: lambda.Code.fromAsset(pathlib.join(__dirname, 'handler')),
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'main.handler',
vpc: props.vpc,
timeout: cdk.Duration.seconds(30),
});
if (props.userSecret) {
this.userSecret = props.userSecret;
} else if (props.username) {
this.userSecret =
props.userSecret ??
new secretsmanager.Secret(this, 'UserSecret', {
generateSecretString: {
passwordLength: 30,
secretStringTemplate: JSON.stringify({
username: props.username,
dbname: props.databaseName,
host: props.dbCluster.clusterEndpoint.hostname,
port: props.dbCluster.clusterEndpoint.port,
}),
generateStringKey: 'password',
excludeCharacters: DEFAULT_PASSWORD_EXCLUDE_CHARS,
},
});
} else {
throw new Error('Must provide either userSecret or username');
}
props.dbSecret.grantRead(handler);
this.userSecret.grantRead(handler);
let secretLatestVersion: string | undefined = undefined;
if (props.onUpdateCheckSecretVersion) {
const secretLatestVersionHandler = new lambda.Function(this, 'OnEventSecretLatestVersion', {
code: lambda.Code.fromAsset(pathlib.join(__dirname, 'latest_secret_version_handler')),
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'main.handler',
timeout: cdk.Duration.seconds(30),
});
// Grant the function secretmanager:ListSecretVersionIds
if (!secretLatestVersionHandler.role) {
throw new Error('Lambda for SecretLatestVersion has no role');
}
const policyResult = secretLatestVersionHandler.role.addToPrincipalPolicy(
new iam.PolicyStatement({
actions: ['secretsmanager:ListSecretVersionIds'],
resources: [this.userSecret.secretArn],
}),
);
const secretLatestVersionProvider = new cr.Provider(this, 'SecretLatestVersionProvider', {
onEventHandler: secretLatestVersionHandler,
});
const secretLatestVersionCustomResource = new cdk.CustomResource(this, 'SecretLatestVersionResource', {
serviceToken: secretLatestVersionProvider.serviceToken,
properties: {
secretArn: this.userSecret.secretArn,
datetime: `onUpdateCheckSecretVersion: ${new Date().toISOString()}`,
},
});
if (policyResult.policyDependable) {
secretLatestVersionCustomResource.node.addDependency(policyResult.policyDependable);
}
secretLatestVersion = secretLatestVersionCustomResource.getAttString('LatestVersionId');
}
handler.connections.allowToDefaultPort(props.dbCluster);
const provider = new cr.Provider(this, 'Provider', {
onEventHandler: handler,
});
const customResource = new cdk.CustomResource(this, 'Resource', {
serviceToken: provider.serviceToken,
properties: {
dbClusterHostname: props.dbCluster.clusterEndpoint.hostname,
dbClusterPort: props.dbCluster.clusterEndpoint.port,
dbSecretArn: props.dbSecret.secretArn,
userSecretArn: this.userSecret.secretArn,
databaseName: props.databaseName,
onDelete: props.onDelete ?? 'Delete',
onCreateIfExists: props.onCreateIfExists ?? 'Fail',
onUpdateIfUserDoesNotExist: props.onUpdateIfUserDoesNotExist ?? 'Ignore',
onUpdateIfDatabaseDoesNotExist: props.onUpdateIfDatabaseDoesNotExist ?? 'Ignore',
onUpdateSetUserPassword: props.onUpdateSetUserPassword ?? 'Never',
onUpdateSetUserPermissions: props.onUpdateSetUserPermissions ?? 'Never',
onUpdateSetDatabaseOwnership: props.onUpdateSetDatabaseOwnership ?? 'Never',
...(secretLatestVersion ? { secretLatestVersion } : {}),
},
});
customResource.node.addDependency(...handler.connections.securityGroups);
customResource.node.addDependency(this.userSecret);
}
}