-
Notifications
You must be signed in to change notification settings - Fork 21
/
PostgresDatabase.java
342 lines (294 loc) · 14.3 KB
/
PostgresDatabase.java
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
package dev.stratospheric.cdk;
import java.util.Collections;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
import software.amazon.awscdk.Environment;
import software.amazon.awscdk.services.ec2.CfnSecurityGroup;
import software.amazon.awscdk.services.rds.CfnDBInstance;
import software.amazon.awscdk.services.rds.CfnDBSubnetGroup;
import software.amazon.awscdk.services.secretsmanager.CfnSecretTargetAttachment;
import software.amazon.awscdk.services.secretsmanager.ISecret;
import software.amazon.awscdk.services.secretsmanager.Secret;
import software.amazon.awscdk.services.secretsmanager.SecretStringGenerator;
import software.amazon.awscdk.services.ssm.StringParameter;
import software.constructs.Construct;
/**
* Creates a Postgres database in the isolated subnets of a given VPC.
* <p>
* The following parameters need to exist in the SSM parameter store for this stack to successfully deploy:
* <ul>
* <li><strong><environmentName>-Network-vpcId:</strong> ID of the VPC to deploy the database into.</li>
* <li><strong><environmentName>-Network-isolatedSubnetOne:</strong> ID of the first isolated subnet to deploy the database into.</li>
* <li><strong><environmentName>-Network-isolatedSubnetTwo:</strong> ID of the first isolated subnet to deploy the database into.</li>
* <li><strong><environmentName>-Network-availabilityZoneOne:</strong> ID of the first AZ to deploy the database into.</li>
* <li><strong><environmentName>-Network-availabilityZoneTwo:</strong> ID of the second AZ to deploy the database into.</li>
* </ul>
* <p>
* The stack exposes the following output parameters in the SSM parameter store to be used in other stacks:
* <ul>
* <li><strong><environmentName>-<applicationName>-Database-endpointAddress:</strong> URL of the database</li>
* <li><strong><environmentName>-<applicationName>-Database-endpointPort:</strong> port to access the database</li>
* <li><strong><environmentName>-<applicationName>-Database-databaseName:</strong> name of the database</li>
* <li><strong><environmentName>-<applicationName>-Database-securityGroupId:</strong> ID of the database's security group</li>
* <li><strong><environmentName>-<applicationName>-Database-secretArn:</strong> ARN of the secret that stores the fields "username" and "password"</li>
* <li><strong><environmentName>-<applicationName>-Database-instanceId:</strong> ID of the database</li>
* </ul>
* The static getter methods provide a convenient access to retrieve these parameters from the parameter store
* for use in other stacks.
*/
public class PostgresDatabase extends Construct {
private static final String PARAMETER_ENDPOINT_ADDRESS = "endpointAddress";
private static final String PARAMETER_ENDPOINT_PORT = "endpointPort";
private static final String PARAMETER_DATABASE_NAME = "databaseName";
private static final String PARAMETER_SECURITY_GROUP_ID = "securityGroupId";
private static final String PARAMETER_SECRET_ARN = "secretArn";
private static final String PARAMETER_INSTANCE_ID = "instanceId";
private final CfnSecurityGroup databaseSecurityGroup;
private final CfnDBInstance dbInstance;
private final ISecret databaseSecret;
private final ApplicationEnvironment applicationEnvironment;
public PostgresDatabase(
final Construct scope,
final String id,
final Environment awsEnvironment,
final ApplicationEnvironment applicationEnvironment,
final DatabaseInputParameters databaseInputParameters) {
super(scope, id);
this.applicationEnvironment = applicationEnvironment;
// Sadly, we cannot use VPC.fromLookup() to resolve a VPC object from this VpcId, because it's broken
// (https://github.com/aws/aws-cdk/issues/3600). So, we have to resolve all properties we need from the VPC
// via SSM parameter store.
Network.NetworkOutputParameters networkOutputParameters = Network.getOutputParametersFromParameterStore(this, applicationEnvironment.getEnvironmentName());
String username = sanitizeDbParameterName(applicationEnvironment.prefix("dbUser"));
databaseSecurityGroup = CfnSecurityGroup.Builder.create(this, "databaseSecurityGroup")
.vpcId(networkOutputParameters.getVpcId())
.groupDescription("Security Group for the database instance")
.groupName(applicationEnvironment.prefix("dbSecurityGroup"))
.build();
// This will generate a JSON object with the keys "username" and "password".
databaseSecret = Secret.Builder.create(this, "databaseSecret")
.secretName(applicationEnvironment.prefix("DatabaseSecret"))
.description("Credentials to the RDS instance")
.generateSecretString(SecretStringGenerator.builder()
.secretStringTemplate(String.format("{\"username\": \"%s\"}", username))
.generateStringKey("password")
.passwordLength(32)
.excludeCharacters("@/\\\" ")
.build())
.build();
CfnDBSubnetGroup subnetGroup = CfnDBSubnetGroup.Builder.create(this, "dbSubnetGroup")
.dbSubnetGroupDescription("Subnet group for the RDS instance")
.dbSubnetGroupName(applicationEnvironment.prefix("dbSubnetGroup"))
.subnetIds(networkOutputParameters.getIsolatedSubnets())
.build();
dbInstance = CfnDBInstance.Builder.create(this, "postgresInstance")
.dbInstanceIdentifier(applicationEnvironment.prefix("database"))
.allocatedStorage(String.valueOf(databaseInputParameters.storageInGb))
.availabilityZone(networkOutputParameters.getAvailabilityZones().get(0))
.dbInstanceClass(databaseInputParameters.instanceClass)
.dbName(sanitizeDbParameterName(applicationEnvironment.prefix("database")))
.dbSubnetGroupName(subnetGroup.getDbSubnetGroupName())
.engine("postgres")
.engineVersion(databaseInputParameters.postgresVersion)
.masterUsername(username)
.masterUserPassword(databaseSecret.secretValueFromJson("password").unsafeUnwrap())
.publiclyAccessible(false)
.vpcSecurityGroups(Collections.singletonList(databaseSecurityGroup.getAttrGroupId()))
.build();
CfnSecretTargetAttachment.Builder.create(this, "secretTargetAttachment")
.secretId(databaseSecret.getSecretArn())
.targetId(dbInstance.getRef())
.targetType("AWS::RDS::DBInstance")
.build();
createOutputParameters();
applicationEnvironment.tag(this);
}
@NotNull
private static String createParameterName(ApplicationEnvironment applicationEnvironment, String parameterName) {
return applicationEnvironment.getEnvironmentName() + "-" + applicationEnvironment.getApplicationName() + "-Database-" + parameterName;
}
/**
* Collects the output parameters of an already deployed {@link PostgresDatabase} construct from the parameter store. This requires
* that a {@link PostgresDatabase} construct has been deployed previously. If you want to access the parameters from the same
* stack that the {@link PostgresDatabase} construct is in, use the plain {@link #getOutputParameters()} method.
*
* @param scope the construct in which we need the output parameters
* @param environment the environment for which to load the output parameters. The deployed {@link PostgresDatabase}
* construct must have been deployed into this environment.
*/
public static DatabaseOutputParameters getOutputParametersFromParameterStore(Construct scope, ApplicationEnvironment environment) {
return new DatabaseOutputParameters(
getEndpointAddress(scope, environment),
getEndpointPort(scope, environment),
getDbName(scope, environment),
getDatabaseSecretArn(scope, environment),
getDatabaseSecurityGroupId(scope, environment),
getDatabaseIdentifier(scope, environment));
}
private static String getDatabaseIdentifier(Construct scope, ApplicationEnvironment environment) {
return StringParameter.fromStringParameterName(scope, PARAMETER_INSTANCE_ID, createParameterName(environment, PARAMETER_INSTANCE_ID))
.getStringValue();
}
private static String getEndpointAddress(Construct scope, ApplicationEnvironment environment) {
return StringParameter.fromStringParameterName(scope, PARAMETER_ENDPOINT_ADDRESS, createParameterName(environment, PARAMETER_ENDPOINT_ADDRESS))
.getStringValue();
}
private static String getEndpointPort(Construct scope, ApplicationEnvironment environment) {
return StringParameter.fromStringParameterName(scope, PARAMETER_ENDPOINT_PORT, createParameterName(environment, PARAMETER_ENDPOINT_PORT))
.getStringValue();
}
private static String getDbName(Construct scope, ApplicationEnvironment environment) {
return StringParameter.fromStringParameterName(scope, PARAMETER_DATABASE_NAME, createParameterName(environment, PARAMETER_DATABASE_NAME))
.getStringValue();
}
private static String getDatabaseSecretArn(Construct scope, ApplicationEnvironment environment) {
String secretArn = StringParameter.fromStringParameterName(scope, PARAMETER_SECRET_ARN, createParameterName(environment, PARAMETER_SECRET_ARN))
.getStringValue();
return secretArn;
}
private static String getDatabaseSecurityGroupId(Construct scope, ApplicationEnvironment environment) {
String securityGroupId = StringParameter.fromStringParameterName(scope, PARAMETER_SECURITY_GROUP_ID, createParameterName(environment, PARAMETER_SECURITY_GROUP_ID))
.getStringValue();
return securityGroupId;
}
/**
* Creates the outputs of this stack to be consumed by other stacks.
*/
private void createOutputParameters() {
StringParameter endpointAddress = StringParameter.Builder.create(this, "endpointAddress")
.parameterName(createParameterName(this.applicationEnvironment, PARAMETER_ENDPOINT_ADDRESS))
.stringValue(this.dbInstance.getAttrEndpointAddress())
.build();
StringParameter endpointPort = StringParameter.Builder.create(this, "endpointPort")
.parameterName(createParameterName(this.applicationEnvironment, PARAMETER_ENDPOINT_PORT))
.stringValue(this.dbInstance.getAttrEndpointPort())
.build();
StringParameter databaseName = StringParameter.Builder.create(this, "databaseName")
.parameterName(createParameterName(this.applicationEnvironment, PARAMETER_DATABASE_NAME))
.stringValue(this.dbInstance.getDbName())
.build();
StringParameter securityGroupId = StringParameter.Builder.create(this, "securityGroupId")
.parameterName(createParameterName(this.applicationEnvironment, PARAMETER_SECURITY_GROUP_ID))
.stringValue(this.databaseSecurityGroup.getAttrGroupId())
.build();
StringParameter secret = StringParameter.Builder.create(this, "secret")
.parameterName(createParameterName(this.applicationEnvironment, PARAMETER_SECRET_ARN))
.stringValue(this.databaseSecret.getSecretArn())
.build();
StringParameter instanceId = StringParameter.Builder.create(this, "instanceId")
.parameterName(createParameterName(this.applicationEnvironment, PARAMETER_INSTANCE_ID))
.stringValue(this.dbInstance.getDbInstanceIdentifier())
.build();
}
private String sanitizeDbParameterName(String dbParameterName) {
return dbParameterName
// db name must have only alphanumerical characters
.replaceAll("[^a-zA-Z0-9]", "")
// db name must start with a letter
.replaceAll("^[0-9]", "a");
}
/**
* Collects the output parameters that other constructs might be interested in.
*/
public DatabaseOutputParameters getOutputParameters() {
return new DatabaseOutputParameters(
this.dbInstance.getAttrEndpointAddress(),
this.dbInstance.getAttrEndpointPort(),
this.dbInstance.getDbName(),
this.databaseSecret.getSecretArn(),
this.databaseSecurityGroup.getAttrGroupId(),
this.dbInstance.getDbInstanceIdentifier());
}
public static class DatabaseInputParameters {
private int storageInGb = 20;
private String instanceClass = "db.t2.micro";
private String postgresVersion = "12.9";
/**
* The storage allocated for the database in GB.
* <p>
* Default: 20.
*/
public DatabaseInputParameters withStorageInGb(int storageInGb) {
this.storageInGb = storageInGb;
return this;
}
/**
* The class of the database instance.
* <p>
* Default: "db.t2.micro".
*/
public DatabaseInputParameters withInstanceClass(String instanceClass) {
Objects.requireNonNull(instanceClass);
this.instanceClass = instanceClass;
return this;
}
/**
* The version of the PostGres database.
* <p>
* Default: "11.5".
*/
public DatabaseInputParameters withPostgresVersion(String postgresVersion) {
Objects.requireNonNull(postgresVersion);
this.postgresVersion = postgresVersion;
return this;
}
}
public static class DatabaseOutputParameters {
private final String endpointAddress;
private final String endpointPort;
private final String dbName;
private final String databaseSecretArn;
private final String databaseSecurityGroupId;
private final String instanceId;
public DatabaseOutputParameters(
String endpointAddress,
String endpointPort,
String dbName,
String databaseSecretArn,
String databaseSecurityGroupId,
String instanceId) {
this.endpointAddress = endpointAddress;
this.endpointPort = endpointPort;
this.dbName = dbName;
this.databaseSecretArn = databaseSecretArn;
this.databaseSecurityGroupId = databaseSecurityGroupId;
this.instanceId = instanceId;
}
/**
* The URL of the Postgres instance.
*/
public String getEndpointAddress() {
return endpointAddress;
}
/**
* The port of the Postgres instance.
*/
public String getEndpointPort() {
return endpointPort;
}
/**
* The database name of the Postgres instance.
*/
public String getDbName() {
return dbName;
}
/**
* The secret containing username and password.
*/
public String getDatabaseSecretArn() {
return databaseSecretArn;
}
/**
* The database's security group.
*/
public String getDatabaseSecurityGroupId() {
return databaseSecurityGroupId;
}
/**
* The database's identifier.
*/
public String getInstanceId() {
return instanceId;
}
}
}