@aws/nx-plugin 0.111.0 → 0.112.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/generators.json +6 -0
- package/package.json +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +2 -0
- package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +4822 -0
- package/src/ts/rdb/files/Dockerfile.template +15 -0
- package/src/ts/rdb/files/prisma/models/example.prisma.template +5 -0
- package/src/ts/rdb/files/prisma/schema.prisma.template +9 -0
- package/src/ts/rdb/files/prisma.config.ts.template +24 -0
- package/src/ts/rdb/files/scripts/docker-pull.ts.template +15 -0
- package/src/ts/rdb/files/scripts/docker-start.ts.template +81 -0
- package/src/ts/rdb/files/scripts/wait-for-db.ts.template +55 -0
- package/src/ts/rdb/files/src/constants.ts.template +8 -0
- package/src/ts/rdb/files/src/create-db-user-handler.ts.template +128 -0
- package/src/ts/rdb/files/src/index.ts.template +2 -0
- package/src/ts/rdb/files/src/migration-handler.ts.template +54 -0
- package/src/ts/rdb/files/src/prisma.ts.template +104 -0
- package/src/ts/rdb/files/src/utils.ts.template +145 -0
- package/src/ts/rdb/generator.d.ts +10 -0
- package/src/ts/rdb/generator.js +206 -0
- package/src/ts/rdb/generator.js.map +1 -0
- package/src/ts/rdb/schema.d.ts +17 -0
- package/src/ts/rdb/schema.json +73 -0
- package/src/utils/rdb-constructs/files/cdk/app/dbs/__nameKebabCase__.ts.template +43 -0
- package/src/utils/rdb-constructs/files/cdk/core/rdb/aurora.ts.template +380 -0
- package/src/utils/rdb-constructs/files/terraform/app/dbs/__nameKebabCase__/__nameKebabCase__.tf.template +732 -0
- package/src/utils/rdb-constructs/files/terraform/core/rdb/aurora/aurora.tf.template +742 -0
- package/src/utils/rdb-constructs/rdb-constructs.d.ts +23 -0
- package/src/utils/rdb-constructs/rdb-constructs.js +59 -0
- package/src/utils/rdb-constructs/rdb-constructs.js.map +1 -0
- package/src/utils/versions.d.ts +12 -1
- package/src/utils/versions.js +11 -0
- package/src/utils/versions.js.map +1 -1
|
@@ -0,0 +1,4822 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/prisma/models/example.prisma 1`] = `
|
|
4
|
+
"model ExampleTable {
|
|
5
|
+
id Int @id @default(autoincrement())
|
|
6
|
+
column1 String
|
|
7
|
+
column2 String
|
|
8
|
+
}
|
|
9
|
+
"
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/prisma/schema.prisma 1`] = `
|
|
13
|
+
"generator client {
|
|
14
|
+
provider = "prisma-client"
|
|
15
|
+
output = "../generated/prisma"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
datasource db {
|
|
19
|
+
provider = "mysql"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
"
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/scripts/docker-pull.ts 1`] = `
|
|
26
|
+
"import Docker from 'dockerode';
|
|
27
|
+
import { promisify } from 'util';
|
|
28
|
+
|
|
29
|
+
const docker = new Docker();
|
|
30
|
+
const image = process.argv[2];
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await docker.getImage(image).inspect();
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if ((e as { statusCode?: number }).statusCode !== 404) throw e;
|
|
36
|
+
const stream = (await promisify(docker.pull.bind(docker))(
|
|
37
|
+
image,
|
|
38
|
+
)) as NodeJS.ReadableStream;
|
|
39
|
+
await promisify(docker.modem.followProgress.bind(docker.modem))(stream);
|
|
40
|
+
}
|
|
41
|
+
"
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/scripts/docker-start.ts 1`] = `
|
|
45
|
+
"import Docker from 'dockerode';
|
|
46
|
+
|
|
47
|
+
const [containerName, image, hostPort, dbName, dbPassword] =
|
|
48
|
+
process.argv.slice(2);
|
|
49
|
+
|
|
50
|
+
const docker = new Docker();
|
|
51
|
+
|
|
52
|
+
let container: Docker.Container;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const existing = docker.getContainer(containerName);
|
|
56
|
+
const info = await existing.inspect();
|
|
57
|
+
container = existing;
|
|
58
|
+
if (!info.State.Running) {
|
|
59
|
+
await container.start();
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if ((e as { statusCode?: number }).statusCode !== 404) throw e;
|
|
63
|
+
container = await docker.createContainer({
|
|
64
|
+
name: containerName,
|
|
65
|
+
Image: image,
|
|
66
|
+
Env: [\`MYSQL_DATABASE=\${dbName}\`, \`MYSQL_ROOT_PASSWORD=\${dbPassword}\`],
|
|
67
|
+
ExposedPorts: { '3306/tcp': {} },
|
|
68
|
+
HostConfig: {
|
|
69
|
+
AutoRemove: true,
|
|
70
|
+
PortBindings: { '3306/tcp': [{ HostPort: hostPort }] },
|
|
71
|
+
Binds: [\`\${containerName}-data:/var/lib/mysql\`],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
await container.start();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const stream = await container.attach({
|
|
78
|
+
stream: true,
|
|
79
|
+
stdout: true,
|
|
80
|
+
stderr: true,
|
|
81
|
+
});
|
|
82
|
+
container.modem.demuxStream(stream, process.stdout, process.stderr);
|
|
83
|
+
|
|
84
|
+
let exiting = false;
|
|
85
|
+
|
|
86
|
+
async function cleanup() {
|
|
87
|
+
if (exiting) return;
|
|
88
|
+
exiting = true;
|
|
89
|
+
try {
|
|
90
|
+
await container.stop();
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if ((e as { statusCode?: number }).statusCode !== 404) console.error(e);
|
|
93
|
+
}
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
process.on('SIGTERM', () => void cleanup());
|
|
98
|
+
process.on('SIGINT', () => void cleanup());
|
|
99
|
+
process.on('SIGHUP', () => void cleanup());
|
|
100
|
+
|
|
101
|
+
const { StatusCode } = await container.wait();
|
|
102
|
+
if (!exiting) process.exit(StatusCode);
|
|
103
|
+
"
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/scripts/wait-for-db.ts 1`] = `
|
|
107
|
+
"import { createConnection, Connection } from 'mariadb';
|
|
108
|
+
|
|
109
|
+
const noop = () => undefined;
|
|
110
|
+
|
|
111
|
+
const [portArg, dbArg, userArg, passwordArg] = process.argv.slice(2);
|
|
112
|
+
const timeoutAt = Date.now() + 60000;
|
|
113
|
+
|
|
114
|
+
while (Date.now() < timeoutAt) {
|
|
115
|
+
let conn: Connection | undefined;
|
|
116
|
+
try {
|
|
117
|
+
conn = await createConnection({
|
|
118
|
+
host: 'localhost',
|
|
119
|
+
port: parseInt(portArg),
|
|
120
|
+
user: userArg,
|
|
121
|
+
password: passwordArg,
|
|
122
|
+
database: dbArg,
|
|
123
|
+
connectTimeout: 500,
|
|
124
|
+
allowPublicKeyRetrieval: true,
|
|
125
|
+
});
|
|
126
|
+
await conn.end();
|
|
127
|
+
console.log('Database is ready.');
|
|
128
|
+
process.exit(0);
|
|
129
|
+
} catch {
|
|
130
|
+
console.log('Database is not ready.');
|
|
131
|
+
await conn?.end().catch(noop);
|
|
132
|
+
}
|
|
133
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new Error(\`Timed out waiting for mysql on port \${portArg}\`);
|
|
137
|
+
"
|
|
138
|
+
`;
|
|
139
|
+
|
|
140
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/src/constants.ts 1`] = `
|
|
141
|
+
"export const DB_PACKAGE_NAME = 'Db';
|
|
142
|
+
|
|
143
|
+
// Local development connection details (used when SERVE_LOCAL=true, see serve-local Nx target)
|
|
144
|
+
export const LOCAL_DB_PORT = 3306;
|
|
145
|
+
export const LOCAL_DB_HOST = 'localhost';
|
|
146
|
+
export const LOCAL_DB_NAME = 'database_name';
|
|
147
|
+
export const LOCAL_DB_USER = 'root';
|
|
148
|
+
export const LOCAL_DB_PASSWORD = 'password';
|
|
149
|
+
"
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/src/create-db-user-handler.ts 1`] = `
|
|
153
|
+
"import type { CloudFormationCustomResourceEvent } from 'aws-lambda';
|
|
154
|
+
import { randomBytes } from 'node:crypto';
|
|
155
|
+
import { createPool, type PoolConnection } from 'mariadb';
|
|
156
|
+
import { getDatabaseSecret, withConnectionRetry } from './utils.js';
|
|
157
|
+
|
|
158
|
+
type OnEventResult = {
|
|
159
|
+
PhysicalResourceId: string;
|
|
160
|
+
Data?: Record<string, unknown>;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const physicalResourceIdPrefix = 'db-user:';
|
|
164
|
+
|
|
165
|
+
const resolveDbUser = (physicalResourceId?: string): string =>
|
|
166
|
+
physicalResourceId?.startsWith(physicalResourceIdPrefix)
|
|
167
|
+
? physicalResourceId.slice(physicalResourceIdPrefix.length)
|
|
168
|
+
: \`db_\${randomBytes(8).toString('hex')}\`;
|
|
169
|
+
|
|
170
|
+
const ensureDatabaseUser = async (dbUser: string): Promise<void> => {
|
|
171
|
+
const { dbname, username, password, host, port } = await getDatabaseSecret();
|
|
172
|
+
const pool = createPool({
|
|
173
|
+
host,
|
|
174
|
+
port,
|
|
175
|
+
database: dbname,
|
|
176
|
+
user: username,
|
|
177
|
+
password,
|
|
178
|
+
ssl: {
|
|
179
|
+
rejectUnauthorized: true,
|
|
180
|
+
},
|
|
181
|
+
connectionLimit: 1,
|
|
182
|
+
multipleStatements: true,
|
|
183
|
+
connectTimeout: 10_000,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const quotedDbName = pool.escapeId(dbname);
|
|
187
|
+
const quotedUser = pool.escape(dbUser);
|
|
188
|
+
const quotedHost = pool.escape('%');
|
|
189
|
+
|
|
190
|
+
let connection: PoolConnection | undefined;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
connection = await pool.getConnection();
|
|
194
|
+
await connection.query(
|
|
195
|
+
[
|
|
196
|
+
\`CREATE USER IF NOT EXISTS \${quotedUser}@\${quotedHost} IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS'\`,
|
|
197
|
+
\`ALTER USER \${quotedUser}@\${quotedHost} IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS' REQUIRE SSL\`,
|
|
198
|
+
\`GRANT ALL PRIVILEGES ON \${quotedDbName}.* TO \${quotedUser}@\${quotedHost}\`,
|
|
199
|
+
].join(';\\n'),
|
|
200
|
+
);
|
|
201
|
+
} finally {
|
|
202
|
+
await connection?.release();
|
|
203
|
+
await pool.end();
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const handler = async (
|
|
208
|
+
event: CloudFormationCustomResourceEvent,
|
|
209
|
+
): Promise<OnEventResult> => {
|
|
210
|
+
const dbUser = resolveDbUser(
|
|
211
|
+
'PhysicalResourceId' in event ? event.PhysicalResourceId : undefined,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (event.RequestType !== 'Delete') {
|
|
215
|
+
await withConnectionRetry(() => ensureDatabaseUser(dbUser));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
PhysicalResourceId: \`\${physicalResourceIdPrefix}\${dbUser}\`,
|
|
220
|
+
Data: {
|
|
221
|
+
dbUser,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
"
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/src/index.ts 1`] = `
|
|
229
|
+
"export { DB_PACKAGE_NAME } from './constants.js';
|
|
230
|
+
export { getPrisma } from './prisma.js';
|
|
231
|
+
"
|
|
232
|
+
`;
|
|
233
|
+
|
|
234
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/src/migration-handler.ts 1`] = `
|
|
235
|
+
"import { execFile } from 'node:child_process';
|
|
236
|
+
import { promisify } from 'node:util';
|
|
237
|
+
import { getDatabaseSecret, withConnectionRetry } from './utils.js';
|
|
238
|
+
|
|
239
|
+
const buildDatabaseUrl = async (): Promise<string> => {
|
|
240
|
+
const { host, port, dbname, username, password } = await getDatabaseSecret();
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
\`mysql://\${encodeURIComponent(username)}\` +
|
|
244
|
+
\`:\${encodeURIComponent(password)}\` +
|
|
245
|
+
\`@\${host}:\${port}/\${dbname}\` +
|
|
246
|
+
\`?sslaccept=strict\`
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const handler = async () => {
|
|
251
|
+
await withConnectionRetry(async () => {
|
|
252
|
+
const databaseUrl = await buildDatabaseUrl();
|
|
253
|
+
await promisify(execFile)('npx', ['prisma', 'migrate', 'deploy'], {
|
|
254
|
+
cwd: __dirname,
|
|
255
|
+
env: {
|
|
256
|
+
...process.env,
|
|
257
|
+
DATABASE_URL: databaseUrl,
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
"
|
|
263
|
+
`;
|
|
264
|
+
|
|
265
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/src/prisma.ts 1`] = `
|
|
266
|
+
"import { PrismaMariaDb } from '@prisma/adapter-mariadb';
|
|
267
|
+
import { Signer } from '@aws-sdk/rds-signer';
|
|
268
|
+
import { PrismaClient } from '../generated/prisma/client.js';
|
|
269
|
+
import {
|
|
270
|
+
DB_PACKAGE_NAME,
|
|
271
|
+
LOCAL_DB_HOST,
|
|
272
|
+
LOCAL_DB_NAME,
|
|
273
|
+
LOCAL_DB_PASSWORD,
|
|
274
|
+
LOCAL_DB_PORT,
|
|
275
|
+
LOCAL_DB_USER,
|
|
276
|
+
} from './constants.js';
|
|
277
|
+
import { getDatabaseConfig } from './utils.js';
|
|
278
|
+
|
|
279
|
+
export const getPrisma = async (): Promise<PrismaClient> => {
|
|
280
|
+
if (process.env.SERVE_LOCAL === 'true') {
|
|
281
|
+
const adapter = new PrismaMariaDb({
|
|
282
|
+
host: LOCAL_DB_HOST,
|
|
283
|
+
port: LOCAL_DB_PORT,
|
|
284
|
+
database: LOCAL_DB_NAME,
|
|
285
|
+
user: LOCAL_DB_USER,
|
|
286
|
+
password: LOCAL_DB_PASSWORD,
|
|
287
|
+
allowPublicKeyRetrieval: true,
|
|
288
|
+
});
|
|
289
|
+
return new PrismaClient({ adapter });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const { hostname, port, database, dbUser, region } =
|
|
293
|
+
await getDatabaseConfig(DB_PACKAGE_NAME);
|
|
294
|
+
const iamAuthToken = await new Signer({
|
|
295
|
+
hostname,
|
|
296
|
+
port,
|
|
297
|
+
region,
|
|
298
|
+
username: dbUser,
|
|
299
|
+
}).getAuthToken();
|
|
300
|
+
|
|
301
|
+
const adapter = new PrismaMariaDb({
|
|
302
|
+
host: hostname,
|
|
303
|
+
port,
|
|
304
|
+
database,
|
|
305
|
+
user: dbUser,
|
|
306
|
+
password: iamAuthToken,
|
|
307
|
+
ssl: {
|
|
308
|
+
rejectUnauthorized: true,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return new PrismaClient({ adapter });
|
|
313
|
+
};
|
|
314
|
+
"
|
|
315
|
+
`;
|
|
316
|
+
|
|
317
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL > packages/db/src/utils.ts 1`] = `
|
|
318
|
+
"import { getAppConfig } from '@aws-lambda-powertools/parameters/appconfig';
|
|
319
|
+
import {
|
|
320
|
+
GetSecretValueCommand,
|
|
321
|
+
SecretsManagerClient,
|
|
322
|
+
} from '@aws-sdk/client-secrets-manager';
|
|
323
|
+
|
|
324
|
+
export type DatabaseConfig = {
|
|
325
|
+
hostname: string;
|
|
326
|
+
port: number;
|
|
327
|
+
database: string;
|
|
328
|
+
adminUser: string;
|
|
329
|
+
dbUser: string;
|
|
330
|
+
region: string;
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
export type DatabaseSecret = {
|
|
334
|
+
dbname: string;
|
|
335
|
+
username: string;
|
|
336
|
+
password: string;
|
|
337
|
+
host: string;
|
|
338
|
+
port: number;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const databaseConfigPromises: Record<string, Promise<DatabaseConfig>> = {};
|
|
342
|
+
|
|
343
|
+
const getSecretValue = async (secretArn: string): Promise<string> => {
|
|
344
|
+
const client = new SecretsManagerClient();
|
|
345
|
+
const data = await client.send(
|
|
346
|
+
new GetSecretValueCommand({
|
|
347
|
+
SecretId: secretArn,
|
|
348
|
+
}),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
if (!data.SecretString) {
|
|
352
|
+
throw new Error('Database secret does not contain SecretString.');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return data.SecretString;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export const getDatabaseSecret = async (): Promise<DatabaseSecret> => {
|
|
359
|
+
const secretArn = process.env.DATABASE_SECRET_ARN;
|
|
360
|
+
|
|
361
|
+
if (!secretArn) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
'Missing required environment variable DATABASE_SECRET_ARN.',
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return JSON.parse(await getSecretValue(secretArn)) as DatabaseSecret;
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const loadDatabaseConfig = async (
|
|
371
|
+
runtimeConfigKey: string,
|
|
372
|
+
): Promise<DatabaseConfig> => {
|
|
373
|
+
const appId = process.env.RUNTIME_CONFIG_APP_ID;
|
|
374
|
+
|
|
375
|
+
if (!appId) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
'Missing required environment variable RUNTIME_CONFIG_APP_ID.',
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const config = await getAppConfig<{
|
|
382
|
+
[key: string]: DatabaseConfig | undefined;
|
|
383
|
+
}>('database', {
|
|
384
|
+
application: appId,
|
|
385
|
+
environment: 'default',
|
|
386
|
+
transform: 'json',
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const databaseConfig = config?.[runtimeConfigKey];
|
|
390
|
+
|
|
391
|
+
if (!databaseConfig) {
|
|
392
|
+
throw new Error(\`RuntimeConfig is missing database.\${runtimeConfigKey}.\`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return databaseConfig;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
export const getDatabaseConfig = (
|
|
399
|
+
runtimeConfigKey: string,
|
|
400
|
+
): Promise<DatabaseConfig> => {
|
|
401
|
+
databaseConfigPromises[runtimeConfigKey] ??=
|
|
402
|
+
loadDatabaseConfig(runtimeConfigKey);
|
|
403
|
+
return databaseConfigPromises[runtimeConfigKey];
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// Aurora's writer endpoint is briefly unreachable from within the VPC right
|
|
407
|
+
// after the cluster reports ready, and IAM policy attachments can take tens
|
|
408
|
+
// of seconds to propagate before RDS accepts an IAM auth token. Both surface
|
|
409
|
+
// as errors that resolve on retry.
|
|
410
|
+
const transientErrorPatterns = [
|
|
411
|
+
'ETIMEDOUT',
|
|
412
|
+
'ECONNREFUSED',
|
|
413
|
+
'ENOTFOUND',
|
|
414
|
+
'P1000', // Prisma: authentication failed
|
|
415
|
+
'ER_ACCESS_DENIED_ERROR', // MySQL: access denied
|
|
416
|
+
];
|
|
417
|
+
|
|
418
|
+
const asString = (value: unknown): string => {
|
|
419
|
+
if (value == null) return '';
|
|
420
|
+
if (typeof value === 'string') return value;
|
|
421
|
+
if (Buffer.isBuffer(value)) return value.toString('utf-8');
|
|
422
|
+
return String(value);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
export const isTransientConnectionError = (error: unknown): boolean => {
|
|
426
|
+
const err = error as {
|
|
427
|
+
message?: unknown;
|
|
428
|
+
code?: unknown;
|
|
429
|
+
stderr?: unknown;
|
|
430
|
+
stdout?: unknown;
|
|
431
|
+
};
|
|
432
|
+
const haystack = [
|
|
433
|
+
error instanceof Error ? error.message : asString(err?.message),
|
|
434
|
+
asString(err?.code),
|
|
435
|
+
asString(err?.stderr),
|
|
436
|
+
asString(err?.stdout),
|
|
437
|
+
].join('\\n');
|
|
438
|
+
return transientErrorPatterns.some((p) => haystack.includes(p));
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
export const withConnectionRetry = async <T>(
|
|
442
|
+
fn: () => Promise<T>,
|
|
443
|
+
{
|
|
444
|
+
maxAttempts = 6,
|
|
445
|
+
delayMs = 10_000,
|
|
446
|
+
}: { maxAttempts?: number; delayMs?: number } = {},
|
|
447
|
+
): Promise<T> => {
|
|
448
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
449
|
+
try {
|
|
450
|
+
return await fn();
|
|
451
|
+
} catch (error) {
|
|
452
|
+
if (attempt === maxAttempts || !isTransientConnectionError(error)) {
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
const wait = delayMs * attempt;
|
|
456
|
+
console.log(
|
|
457
|
+
\`Transient connection error (attempt \${attempt}/\${maxAttempts}), retrying in \${wait}ms: \${
|
|
458
|
+
error instanceof Error ? error.message : String(error)
|
|
459
|
+
}\`,
|
|
460
|
+
);
|
|
461
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
throw new Error('unreachable');
|
|
465
|
+
};
|
|
466
|
+
"
|
|
467
|
+
`;
|
|
468
|
+
|
|
469
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL 1`] = `
|
|
470
|
+
"import { CfnOutput, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
|
|
471
|
+
import { CustomResource } from 'aws-cdk-lib';
|
|
472
|
+
import { IConnectable, IVpc, Port, SubnetType } from 'aws-cdk-lib/aws-ec2';
|
|
473
|
+
import { IGrantable, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
|
|
474
|
+
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
475
|
+
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
|
|
476
|
+
import {
|
|
477
|
+
Architecture,
|
|
478
|
+
Code,
|
|
479
|
+
DockerImageCode,
|
|
480
|
+
DockerImageFunction,
|
|
481
|
+
Function,
|
|
482
|
+
Runtime,
|
|
483
|
+
Tracing,
|
|
484
|
+
} from 'aws-cdk-lib/aws-lambda';
|
|
485
|
+
import {
|
|
486
|
+
AuroraMysqlEngineVersion,
|
|
487
|
+
AuroraPostgresEngineVersion,
|
|
488
|
+
ClusterInstance,
|
|
489
|
+
Credentials,
|
|
490
|
+
DatabaseCluster,
|
|
491
|
+
DatabaseClusterEngine,
|
|
492
|
+
DatabaseClusterProps,
|
|
493
|
+
DatabaseProxy,
|
|
494
|
+
DefaultAuthScheme,
|
|
495
|
+
IClusterEngine,
|
|
496
|
+
} from 'aws-cdk-lib/aws-rds';
|
|
497
|
+
import { Provider } from 'aws-cdk-lib/custom-resources';
|
|
498
|
+
import { Trigger } from 'aws-cdk-lib/triggers';
|
|
499
|
+
import { Construct } from 'constructs';
|
|
500
|
+
import { RuntimeConfig } from '../runtime-config.js';
|
|
501
|
+
|
|
502
|
+
export type AuroraDatabaseEngineVersion =
|
|
503
|
+
| AuroraMysqlEngineVersion
|
|
504
|
+
| AuroraPostgresEngineVersion;
|
|
505
|
+
|
|
506
|
+
export interface AuroraDatabaseEngine {
|
|
507
|
+
/**
|
|
508
|
+
* Discriminant identifying the engine type.
|
|
509
|
+
*/
|
|
510
|
+
readonly type: 'mysql' | 'postgres';
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Default Aurora engine version used when one is not explicitly provided.
|
|
514
|
+
*/
|
|
515
|
+
readonly defaultVersion: AuroraDatabaseEngineVersion;
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Builds the CDK cluster engine for the provided engine version.
|
|
519
|
+
*/
|
|
520
|
+
clusterEngine(version: AuroraDatabaseEngineVersion): IClusterEngine;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export class AuroraDatabaseEngines {
|
|
524
|
+
public static mysql({
|
|
525
|
+
defaultVersion = AuroraMysqlEngineVersion.VER_3_12_0,
|
|
526
|
+
}: {
|
|
527
|
+
defaultVersion?: AuroraMysqlEngineVersion;
|
|
528
|
+
}): AuroraDatabaseEngine {
|
|
529
|
+
return {
|
|
530
|
+
type: 'mysql',
|
|
531
|
+
defaultVersion,
|
|
532
|
+
clusterEngine: (version) =>
|
|
533
|
+
DatabaseClusterEngine.auroraMysql({
|
|
534
|
+
version: version as AuroraMysqlEngineVersion,
|
|
535
|
+
}),
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
public static postgres({
|
|
540
|
+
defaultVersion = AuroraPostgresEngineVersion.VER_17_7,
|
|
541
|
+
}: {
|
|
542
|
+
defaultVersion?: AuroraPostgresEngineVersion;
|
|
543
|
+
}): AuroraDatabaseEngine {
|
|
544
|
+
return {
|
|
545
|
+
type: 'postgres',
|
|
546
|
+
defaultVersion,
|
|
547
|
+
clusterEngine: (version) =>
|
|
548
|
+
DatabaseClusterEngine.auroraPostgres({
|
|
549
|
+
version: version as AuroraPostgresEngineVersion,
|
|
550
|
+
}),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
type _AuroraDatabaseProps = Omit<
|
|
556
|
+
DatabaseClusterProps,
|
|
557
|
+
| 'credentials'
|
|
558
|
+
| 'defaultDatabaseName'
|
|
559
|
+
| 'engine'
|
|
560
|
+
| 'iamAuthentication'
|
|
561
|
+
| 'instanceProps'
|
|
562
|
+
| 'writer'
|
|
563
|
+
>;
|
|
564
|
+
|
|
565
|
+
export interface AuroraDatabaseProps extends _AuroraDatabaseProps {
|
|
566
|
+
/**
|
|
567
|
+
* VPC where the Aurora cluster will be deployed.
|
|
568
|
+
*/
|
|
569
|
+
readonly vpc: IVpc;
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Aurora engine preset used to build the cluster.
|
|
573
|
+
*/
|
|
574
|
+
readonly engine: AuroraDatabaseEngine;
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The engine version to deploy.
|
|
578
|
+
*
|
|
579
|
+
* @default - engine.defaultVersion
|
|
580
|
+
*/
|
|
581
|
+
readonly engineVersion?: AuroraDatabaseEngineVersion;
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Admin username used when generating credentials.
|
|
585
|
+
*/
|
|
586
|
+
readonly adminUser: string;
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* The initial database created in the cluster.
|
|
590
|
+
*/
|
|
591
|
+
readonly databaseName: string;
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* RuntimeConfig key used under the \`database\` namespace.
|
|
595
|
+
*/
|
|
596
|
+
readonly runtimeConfigKey: string;
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Migration bundle used to create a migration handler Lambda.
|
|
600
|
+
*/
|
|
601
|
+
readonly migrationBundleDir: string;
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Bundle used to create or reconcile the application database user.
|
|
605
|
+
*/
|
|
606
|
+
readonly createDbUserBundleDir: string;
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Writer instance for the Aurora cluster.
|
|
610
|
+
*/
|
|
611
|
+
readonly writer?: DatabaseClusterProps['writer'];
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Whether to provision an RDS Proxy in front of the Aurora cluster.
|
|
615
|
+
*
|
|
616
|
+
* @default true
|
|
617
|
+
*/
|
|
618
|
+
readonly enableRdsProxy?: boolean;
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Whether to enable automatic credential rotation for the admin secret.
|
|
622
|
+
*
|
|
623
|
+
* @default true
|
|
624
|
+
*/
|
|
625
|
+
readonly enableCredentialRotation?: boolean;
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Whether to enable deletion protection on the Aurora cluster.
|
|
629
|
+
*
|
|
630
|
+
* @default true
|
|
631
|
+
*/
|
|
632
|
+
readonly deletionProtection?: boolean;
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Removal policy applied to the Aurora cluster.
|
|
636
|
+
*
|
|
637
|
+
* @default RemovalPolicy.RETAIN
|
|
638
|
+
*/
|
|
639
|
+
readonly removalPolicy?: RemovalPolicy;
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Whether to enable automatic key rotation on the KMS key used to encrypt the Aurora cluster and its credentials secret.
|
|
643
|
+
*
|
|
644
|
+
* @default true
|
|
645
|
+
*/
|
|
646
|
+
readonly enableKeyRotation?: boolean;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Reusable Aurora database construct that supports different Aurora engines
|
|
651
|
+
* through typed engine presets.
|
|
652
|
+
*/
|
|
653
|
+
export abstract class AuroraDatabase extends Construct {
|
|
654
|
+
public readonly cluster: DatabaseCluster;
|
|
655
|
+
public readonly proxy?: DatabaseProxy;
|
|
656
|
+
private readonly adminUser: string;
|
|
657
|
+
private readonly dbUser: string;
|
|
658
|
+
|
|
659
|
+
constructor(
|
|
660
|
+
scope: Construct,
|
|
661
|
+
id: string,
|
|
662
|
+
{
|
|
663
|
+
vpc,
|
|
664
|
+
adminUser,
|
|
665
|
+
databaseName,
|
|
666
|
+
runtimeConfigKey,
|
|
667
|
+
migrationBundleDir,
|
|
668
|
+
createDbUserBundleDir,
|
|
669
|
+
vpcSubnets,
|
|
670
|
+
writer,
|
|
671
|
+
enableRdsProxy = true,
|
|
672
|
+
enableCredentialRotation = true,
|
|
673
|
+
deletionProtection = true,
|
|
674
|
+
removalPolicy = RemovalPolicy.RETAIN,
|
|
675
|
+
enableKeyRotation = true,
|
|
676
|
+
engine,
|
|
677
|
+
engineVersion,
|
|
678
|
+
...clusterProps
|
|
679
|
+
}: AuroraDatabaseProps,
|
|
680
|
+
) {
|
|
681
|
+
super(scope, id);
|
|
682
|
+
|
|
683
|
+
this.adminUser = adminUser;
|
|
684
|
+
|
|
685
|
+
const key = new Key(this, 'EncryptionKey', { enableKeyRotation });
|
|
686
|
+
this.cluster = new DatabaseCluster(this, 'DatabaseCluster', {
|
|
687
|
+
...clusterProps,
|
|
688
|
+
vpc,
|
|
689
|
+
vpcSubnets,
|
|
690
|
+
engine: engine.clusterEngine(engineVersion ?? engine.defaultVersion),
|
|
691
|
+
writer: writer ?? ClusterInstance.serverlessV2('writer'),
|
|
692
|
+
credentials: Credentials.fromGeneratedSecret(adminUser, {
|
|
693
|
+
encryptionKey: key,
|
|
694
|
+
}),
|
|
695
|
+
iamAuthentication: true,
|
|
696
|
+
monitoringInterval: Duration.seconds(5),
|
|
697
|
+
defaultDatabaseName: databaseName,
|
|
698
|
+
storageEncrypted: true,
|
|
699
|
+
storageEncryptionKey: key,
|
|
700
|
+
deletionProtection,
|
|
701
|
+
removalPolicy,
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
if (enableCredentialRotation) {
|
|
705
|
+
this.cluster.addRotationSingleUser({
|
|
706
|
+
automaticallyAfter: Duration.days(30),
|
|
707
|
+
vpcSubnets: {
|
|
708
|
+
subnetType: SubnetType.PRIVATE_WITH_EGRESS,
|
|
709
|
+
},
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const proxyRole = enableRdsProxy
|
|
714
|
+
? new Role(this, 'DatabaseProxyRole', {
|
|
715
|
+
assumedBy: new ServicePrincipal('rds.amazonaws.com'),
|
|
716
|
+
})
|
|
717
|
+
: undefined;
|
|
718
|
+
|
|
719
|
+
if (enableRdsProxy) {
|
|
720
|
+
this.proxy = this.cluster.addProxy('DatabaseProxy', {
|
|
721
|
+
vpc,
|
|
722
|
+
vpcSubnets,
|
|
723
|
+
defaultAuthScheme: DefaultAuthScheme.IAM_AUTH,
|
|
724
|
+
role: proxyRole,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const databaseHostname =
|
|
729
|
+
this.proxy?.endpoint ?? this.cluster.clusterEndpoint.hostname;
|
|
730
|
+
const databasePort = this.cluster.clusterEndpoint.port;
|
|
731
|
+
|
|
732
|
+
const createDbUserHandler = new Function(this, 'CreateDbUserHandler', {
|
|
733
|
+
code: Code.fromAsset(createDbUserBundleDir),
|
|
734
|
+
handler: 'index.handler',
|
|
735
|
+
runtime: Runtime.NODEJS_LATEST,
|
|
736
|
+
timeout: Duration.minutes(5),
|
|
737
|
+
tracing: Tracing.ACTIVE,
|
|
738
|
+
vpc,
|
|
739
|
+
vpcSubnets: {
|
|
740
|
+
subnetType: SubnetType.PRIVATE_WITH_EGRESS,
|
|
741
|
+
},
|
|
742
|
+
environment: {
|
|
743
|
+
DATABASE_SECRET_ARN: this.cluster.secret!.secretArn,
|
|
744
|
+
NODE_EXTRA_CA_CERTS: '/var/runtime/ca-cert.pem',
|
|
745
|
+
},
|
|
746
|
+
architecture: Architecture.ARM_64,
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
this.cluster.connections.allowDefaultPortFrom(
|
|
750
|
+
createDbUserHandler,
|
|
751
|
+
'Allow the create-db-user handler to connect to the database',
|
|
752
|
+
);
|
|
753
|
+
this.grantSecretRead(createDbUserHandler);
|
|
754
|
+
|
|
755
|
+
const createDbUserProvider = new Provider(this, 'CreateDbUserProvider', {
|
|
756
|
+
onEventHandler: createDbUserHandler,
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
const createDbUserResource = new CustomResource(
|
|
760
|
+
this,
|
|
761
|
+
'CreateDbUserResource',
|
|
762
|
+
{
|
|
763
|
+
serviceToken: createDbUserProvider.serviceToken,
|
|
764
|
+
properties: {
|
|
765
|
+
clusterIdentifier: this.cluster.clusterIdentifier,
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
);
|
|
769
|
+
createDbUserResource.node.addDependency(this.cluster);
|
|
770
|
+
this.dbUser = createDbUserResource.getAttString('dbUser');
|
|
771
|
+
if (this.proxy && proxyRole) {
|
|
772
|
+
this.cluster.grantConnect(proxyRole, this.dbUser);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const rc = RuntimeConfig.ensure(this);
|
|
776
|
+
rc.set('database', runtimeConfigKey, {
|
|
777
|
+
hostname: databaseHostname,
|
|
778
|
+
port: databasePort,
|
|
779
|
+
database: databaseName,
|
|
780
|
+
adminUser: this.adminUser,
|
|
781
|
+
dbUser: this.dbUser,
|
|
782
|
+
region: Stack.of(this).region,
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
const migrationHandler = new DockerImageFunction(this, 'MigrationHandler', {
|
|
786
|
+
code: DockerImageCode.fromImageAsset(migrationBundleDir, {
|
|
787
|
+
platform: Platform.LINUX_ARM64,
|
|
788
|
+
}),
|
|
789
|
+
memorySize: 1024,
|
|
790
|
+
timeout: Duration.minutes(5),
|
|
791
|
+
tracing: Tracing.ACTIVE,
|
|
792
|
+
vpc,
|
|
793
|
+
environment:
|
|
794
|
+
engine.type === 'mysql'
|
|
795
|
+
? {
|
|
796
|
+
DATABASE_SECRET_ARN: this.cluster.secret!.secretArn,
|
|
797
|
+
}
|
|
798
|
+
: {
|
|
799
|
+
HOSTNAME: databaseHostname,
|
|
800
|
+
DATABASE: databaseName,
|
|
801
|
+
PORT: databasePort.toString(),
|
|
802
|
+
DBUSER: this.dbUser,
|
|
803
|
+
},
|
|
804
|
+
architecture: Architecture.ARM_64,
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
if (engine.type === 'mysql') {
|
|
808
|
+
this.cluster.connections.allowDefaultPortFrom(migrationHandler);
|
|
809
|
+
this.grantSecretRead(migrationHandler);
|
|
810
|
+
} else {
|
|
811
|
+
this.allowDefaultPortFrom(migrationHandler);
|
|
812
|
+
this.grantConnect(migrationHandler);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const trigger = new Trigger(this, 'MigrationTrigger', {
|
|
816
|
+
handler: migrationHandler,
|
|
817
|
+
});
|
|
818
|
+
trigger.node.addDependency(createDbUserResource);
|
|
819
|
+
|
|
820
|
+
new CfnOutput(this, 'ClusterEndpoint', {
|
|
821
|
+
value: this.cluster.clusterEndpoint.hostname,
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
if (this.proxy?.endpoint) {
|
|
825
|
+
new CfnOutput(this, 'ProxyEndpoint', {
|
|
826
|
+
value: this.proxy?.endpoint,
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
public allowDefaultPortFrom(other: IConnectable, description?: string): void {
|
|
832
|
+
if (this.proxy) {
|
|
833
|
+
this.proxy.connections.allowFrom(
|
|
834
|
+
other,
|
|
835
|
+
Port.tcp(this.cluster.clusterEndpoint.port),
|
|
836
|
+
description,
|
|
837
|
+
);
|
|
838
|
+
} else {
|
|
839
|
+
this.cluster.connections.allowDefaultPortFrom(other, description);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
public grantSecretRead(grantee: IGrantable) {
|
|
844
|
+
return this.cluster.secret!.grantRead(grantee);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
public grantConnect(grantee: IGrantable) {
|
|
848
|
+
return this.proxy
|
|
849
|
+
? this.proxy.grantConnect(grantee, this.dbUser)
|
|
850
|
+
: this.cluster.grantConnect(grantee, this.dbUser);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
"
|
|
854
|
+
`;
|
|
855
|
+
|
|
856
|
+
exports[`ts#rdb generator > should add mysql prisma dependencies when engine is MySQL 2`] = `
|
|
857
|
+
"import { defineConfig } from 'prisma/config';
|
|
858
|
+
|
|
859
|
+
const getDatabaseUrl = async () => {
|
|
860
|
+
if (process.env.SERVE_LOCAL === 'true') {
|
|
861
|
+
const {
|
|
862
|
+
LOCAL_DB_HOST,
|
|
863
|
+
LOCAL_DB_NAME,
|
|
864
|
+
LOCAL_DB_PASSWORD,
|
|
865
|
+
LOCAL_DB_PORT,
|
|
866
|
+
LOCAL_DB_USER,
|
|
867
|
+
} = await import('./src/constants.js');
|
|
868
|
+
return \`mysql://\${LOCAL_DB_USER}:\${LOCAL_DB_PASSWORD}@\${LOCAL_DB_HOST}:\${LOCAL_DB_PORT}/\${LOCAL_DB_NAME}\`;
|
|
869
|
+
}
|
|
870
|
+
return process.env.DATABASE_URL;
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
export default defineConfig({
|
|
874
|
+
schema: 'prisma/',
|
|
875
|
+
migrations: {
|
|
876
|
+
path: 'prisma/migrations',
|
|
877
|
+
},
|
|
878
|
+
datasource: {
|
|
879
|
+
url: await getDatabaseUrl(),
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
"
|
|
883
|
+
`;
|
|
884
|
+
|
|
885
|
+
exports[`ts#rdb generator > should generate terraform modules when iacProvider is Terraform 1`] = `
|
|
886
|
+
"# Core Aurora module
|
|
887
|
+
# This module creates an Aurora cluster and a generated admin secret.
|
|
888
|
+
|
|
889
|
+
terraform {
|
|
890
|
+
required_version = ">= 1.0"
|
|
891
|
+
|
|
892
|
+
required_providers {
|
|
893
|
+
aws = {
|
|
894
|
+
source = "hashicorp/aws"
|
|
895
|
+
version = "~> 6.33"
|
|
896
|
+
}
|
|
897
|
+
null = {
|
|
898
|
+
source = "hashicorp/null"
|
|
899
|
+
version = ">= 3.0"
|
|
900
|
+
}
|
|
901
|
+
random = {
|
|
902
|
+
source = "hashicorp/random"
|
|
903
|
+
version = ">= 3.0"
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
data "aws_caller_identity" "current" {}
|
|
909
|
+
data "aws_partition" "current" {}
|
|
910
|
+
data "aws_region" "current" {}
|
|
911
|
+
|
|
912
|
+
data "aws_iam_policy" "enhanced_monitoring" {
|
|
913
|
+
arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
variable "name" {
|
|
917
|
+
description = "Base name applied to Aurora resources."
|
|
918
|
+
type = string
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
variable "vpc_id" {
|
|
922
|
+
description = "VPC where the Aurora cluster will be deployed."
|
|
923
|
+
type = string
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
variable "subnet_ids" {
|
|
927
|
+
description = "Subnet IDs for the Aurora DB subnet group and RDS Proxy. These subnets do not need outbound egress — private isolated subnets are fine."
|
|
928
|
+
type = list(string)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
variable "lambda_subnet_ids" {
|
|
932
|
+
description = "Subnet IDs for the credential-rotation Lambda. Must have outbound egress to AWS service endpoints (e.g. Secrets Manager) — use private subnets with a NAT gateway."
|
|
933
|
+
type = list(string)
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
variable "engine" {
|
|
937
|
+
description = "Aurora engine to use."
|
|
938
|
+
type = string
|
|
939
|
+
default = "aurora-postgresql"
|
|
940
|
+
|
|
941
|
+
validation {
|
|
942
|
+
condition = contains(["aurora-postgresql", "aurora-mysql"], var.engine)
|
|
943
|
+
error_message = "engine must be aurora-postgresql or aurora-mysql."
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
variable "engine_version" {
|
|
948
|
+
description = "Aurora engine version."
|
|
949
|
+
type = string
|
|
950
|
+
default = null
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
variable "database_name" {
|
|
954
|
+
description = "Initial database created in the cluster."
|
|
955
|
+
type = string
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
variable "admin_user" {
|
|
959
|
+
description = "Admin username stored in the generated Secrets Manager secret."
|
|
960
|
+
type = string
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
variable "port" {
|
|
964
|
+
description = "Database port for the selected Aurora engine."
|
|
965
|
+
type = number
|
|
966
|
+
default = null
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
variable "serverless_min_capacity" {
|
|
970
|
+
description = "Minimum Aurora Serverless v2 ACUs."
|
|
971
|
+
type = number
|
|
972
|
+
default = 0.5
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
variable "serverless_max_capacity" {
|
|
976
|
+
description = "Maximum Aurora Serverless v2 ACUs."
|
|
977
|
+
type = number
|
|
978
|
+
default = 4
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
variable "instance_count" {
|
|
982
|
+
description = "Number of Aurora instances to create."
|
|
983
|
+
type = number
|
|
984
|
+
default = 1
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
variable "deletion_protection" {
|
|
988
|
+
description = "Whether deletion protection is enabled."
|
|
989
|
+
type = bool
|
|
990
|
+
default = true
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
variable "skip_final_snapshot" {
|
|
994
|
+
description = "Whether to skip the final snapshot on deletion."
|
|
995
|
+
type = bool
|
|
996
|
+
default = false
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
variable "enable_rds_proxy" {
|
|
1000
|
+
description = "Whether to provision an RDS Proxy in front of the Aurora cluster."
|
|
1001
|
+
type = bool
|
|
1002
|
+
default = true
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
variable "enable_credential_rotation" {
|
|
1006
|
+
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
1007
|
+
type = bool
|
|
1008
|
+
default = true
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
variable "enable_cloudwatch_logs" {
|
|
1012
|
+
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL also enables verbose statement logging."
|
|
1013
|
+
type = bool
|
|
1014
|
+
default = false
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
variable "enable_performance_insights" {
|
|
1018
|
+
description = "Whether to enable Performance Insights on Aurora cluster instances."
|
|
1019
|
+
type = bool
|
|
1020
|
+
default = true
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
variable "performance_insights_retention_period" {
|
|
1024
|
+
description = "Retention period, in days, for Performance Insights data when enabled."
|
|
1025
|
+
type = number
|
|
1026
|
+
default = 7
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
variable "enable_backup" {
|
|
1030
|
+
description = "Whether to provision an AWS Backup plan for the Aurora cluster."
|
|
1031
|
+
type = bool
|
|
1032
|
+
default = false
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
variable "enable_key_rotation" {
|
|
1036
|
+
description = "Whether to enable automatic key rotation on the KMS key used to encrypt the Aurora cluster and its credentials secret."
|
|
1037
|
+
type = bool
|
|
1038
|
+
default = true
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
variable "tags" {
|
|
1042
|
+
description = "Tags to apply to all resources."
|
|
1043
|
+
type = map(string)
|
|
1044
|
+
default = {}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
locals {
|
|
1048
|
+
default_port = var.engine == "aurora-mysql" ? 3306 : 5432
|
|
1049
|
+
default_engine_version = var.engine == "aurora-mysql" ? "8.0.mysql_aurora.3.12.0" : "17.7"
|
|
1050
|
+
parameter_group_family = var.engine == "aurora-mysql" ? "aurora-mysql8.0" : "aurora-postgresql\${split(".", coalesce(var.engine_version, local.default_engine_version))[0]}"
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
resource "aws_rds_cluster_parameter_group" "database" {
|
|
1054
|
+
count = var.enable_cloudwatch_logs ? 1 : 0
|
|
1055
|
+
|
|
1056
|
+
name_prefix = "\${var.name}-"
|
|
1057
|
+
family = local.parameter_group_family
|
|
1058
|
+
description = "Parameter group for \${var.name} Aurora cluster"
|
|
1059
|
+
|
|
1060
|
+
dynamic "parameter" {
|
|
1061
|
+
for_each = var.engine == "aurora-postgresql" ? [1] : []
|
|
1062
|
+
content {
|
|
1063
|
+
name = "log_statement"
|
|
1064
|
+
value = "all"
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
tags = var.tags
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
resource "aws_kms_key" "database" {
|
|
1072
|
+
description = "KMS key for Aurora cluster \${var.name}"
|
|
1073
|
+
enable_key_rotation = var.enable_key_rotation
|
|
1074
|
+
|
|
1075
|
+
policy = jsonencode({
|
|
1076
|
+
Version = "2012-10-17"
|
|
1077
|
+
Statement = [
|
|
1078
|
+
{
|
|
1079
|
+
Sid = "EnableRootAccess"
|
|
1080
|
+
Effect = "Allow"
|
|
1081
|
+
Principal = {
|
|
1082
|
+
AWS = "arn:\${data.aws_partition.current.partition}:iam::\${data.aws_caller_identity.current.account_id}:root"
|
|
1083
|
+
}
|
|
1084
|
+
Action = "kms:*"
|
|
1085
|
+
Resource = "*"
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
Sid = "AllowRDSService"
|
|
1089
|
+
Effect = "Allow"
|
|
1090
|
+
Principal = {
|
|
1091
|
+
Service = "rds.amazonaws.com"
|
|
1092
|
+
}
|
|
1093
|
+
Action = [
|
|
1094
|
+
"kms:Encrypt",
|
|
1095
|
+
"kms:Decrypt",
|
|
1096
|
+
"kms:ReEncrypt*",
|
|
1097
|
+
"kms:GenerateDataKey*",
|
|
1098
|
+
"kms:DescribeKey",
|
|
1099
|
+
"kms:CreateGrant"
|
|
1100
|
+
]
|
|
1101
|
+
Resource = "*"
|
|
1102
|
+
}
|
|
1103
|
+
]
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
tags = merge(var.tags, {
|
|
1107
|
+
Name = "\${var.name}-aurora"
|
|
1108
|
+
})
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
resource "aws_iam_role" "enhanced_monitoring" {
|
|
1112
|
+
name_prefix = "\${var.name}-aurora-monitoring-"
|
|
1113
|
+
|
|
1114
|
+
assume_role_policy = jsonencode({
|
|
1115
|
+
Version = "2012-10-17"
|
|
1116
|
+
Statement = [
|
|
1117
|
+
{
|
|
1118
|
+
Effect = "Allow"
|
|
1119
|
+
Principal = {
|
|
1120
|
+
Service = "monitoring.rds.amazonaws.com"
|
|
1121
|
+
}
|
|
1122
|
+
Action = "sts:AssumeRole"
|
|
1123
|
+
}
|
|
1124
|
+
]
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
tags = merge(var.tags, {
|
|
1128
|
+
Name = "\${var.name}-aurora-monitoring"
|
|
1129
|
+
})
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
resource "aws_iam_role_policy_attachment" "enhanced_monitoring" {
|
|
1133
|
+
role = aws_iam_role.enhanced_monitoring.name
|
|
1134
|
+
policy_arn = data.aws_iam_policy.enhanced_monitoring.arn
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
resource "aws_security_group" "database" {
|
|
1138
|
+
name_prefix = "\${var.name}-aurora-"
|
|
1139
|
+
description = "Security group for Aurora cluster \${var.name}"
|
|
1140
|
+
vpc_id = var.vpc_id
|
|
1141
|
+
|
|
1142
|
+
tags = merge(var.tags, {
|
|
1143
|
+
Name = "\${var.name}-aurora"
|
|
1144
|
+
})
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
resource "aws_db_subnet_group" "database" {
|
|
1148
|
+
name = "\${var.name}-aurora"
|
|
1149
|
+
subnet_ids = var.subnet_ids
|
|
1150
|
+
|
|
1151
|
+
tags = merge(var.tags, {
|
|
1152
|
+
Name = "\${var.name}-aurora"
|
|
1153
|
+
})
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
resource "random_password" "master_password" {
|
|
1157
|
+
length = 32
|
|
1158
|
+
special = true
|
|
1159
|
+
override_special = "!#$%&*()-_=+[]{}<>:?"
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
resource "aws_secretsmanager_secret" "credentials" {
|
|
1163
|
+
#checkov:skip=CKV2_AWS_57:Rotation is configured via aws_cloudformation_stack.credentials_rotation using the AWS::SecretsManager-2024-09-16 transform; Checkov cannot resolve rotation schedules created by CloudFormation transforms
|
|
1164
|
+
name_prefix = "\${var.name}-aurora-credentials-"
|
|
1165
|
+
kms_key_id = aws_kms_key.database.arn
|
|
1166
|
+
|
|
1167
|
+
tags = merge(var.tags, {
|
|
1168
|
+
Name = "\${var.name}-aurora-credentials"
|
|
1169
|
+
})
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
resource "aws_secretsmanager_secret_version" "credentials" {
|
|
1173
|
+
secret_id = aws_secretsmanager_secret.credentials.id
|
|
1174
|
+
secret_string = jsonencode({
|
|
1175
|
+
engine = var.engine == "aurora-mysql" ? "mysql" : "postgres"
|
|
1176
|
+
username = var.admin_user
|
|
1177
|
+
password = random_password.master_password.result
|
|
1178
|
+
host = aws_rds_cluster.database.endpoint
|
|
1179
|
+
port = aws_rds_cluster.database.port
|
|
1180
|
+
dbname = var.database_name
|
|
1181
|
+
dbClusterIdentifier = aws_rds_cluster.database.cluster_identifier
|
|
1182
|
+
})
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
resource "aws_rds_cluster" "database" {
|
|
1186
|
+
#checkov:skip=CKV2_AWS_27:Query logging can be enabled with enable_cloudwatch_logs; this module defaults to CDK-equivalent behavior unless logging is requested
|
|
1187
|
+
#checkov:skip=CKV_AWS_139:Deletion protection is enabled but checkov is unable to detect it correctly
|
|
1188
|
+
cluster_identifier = "\${var.name}-aurora"
|
|
1189
|
+
engine = var.engine
|
|
1190
|
+
engine_version = coalesce(var.engine_version, local.default_engine_version)
|
|
1191
|
+
database_name = var.database_name
|
|
1192
|
+
master_username = var.admin_user
|
|
1193
|
+
master_password = random_password.master_password.result
|
|
1194
|
+
db_subnet_group_name = aws_db_subnet_group.database.name
|
|
1195
|
+
db_cluster_parameter_group_name = var.enable_cloudwatch_logs ? aws_rds_cluster_parameter_group.database[0].name : null
|
|
1196
|
+
vpc_security_group_ids = [aws_security_group.database.id]
|
|
1197
|
+
port = coalesce(var.port, local.default_port)
|
|
1198
|
+
storage_encrypted = true
|
|
1199
|
+
kms_key_id = aws_kms_key.database.arn
|
|
1200
|
+
deletion_protection = var.deletion_protection
|
|
1201
|
+
skip_final_snapshot = var.skip_final_snapshot
|
|
1202
|
+
final_snapshot_identifier = var.skip_final_snapshot ? null : "\${var.name}-aurora-final-snapshot"
|
|
1203
|
+
iam_database_authentication_enabled = true
|
|
1204
|
+
monitoring_interval = 5
|
|
1205
|
+
monitoring_role_arn = aws_iam_role.enhanced_monitoring.arn
|
|
1206
|
+
copy_tags_to_snapshot = true
|
|
1207
|
+
enabled_cloudwatch_logs_exports = var.enable_cloudwatch_logs ? (var.engine == "aurora-mysql" ? ["audit", "error", "general", "slowquery"] : ["postgresql"]) : null
|
|
1208
|
+
|
|
1209
|
+
serverlessv2_scaling_configuration {
|
|
1210
|
+
min_capacity = var.serverless_min_capacity
|
|
1211
|
+
max_capacity = var.serverless_max_capacity
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
tags = merge(var.tags, {
|
|
1215
|
+
Name = "\${var.name}-aurora"
|
|
1216
|
+
})
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
resource "aws_rds_cluster_instance" "database" {
|
|
1220
|
+
count = var.instance_count
|
|
1221
|
+
|
|
1222
|
+
identifier = "\${var.name}-aurora-\${count.index + 1}"
|
|
1223
|
+
cluster_identifier = aws_rds_cluster.database.id
|
|
1224
|
+
instance_class = "db.serverless"
|
|
1225
|
+
engine = aws_rds_cluster.database.engine
|
|
1226
|
+
engine_version = aws_rds_cluster.database.engine_version
|
|
1227
|
+
db_subnet_group_name = aws_db_subnet_group.database.name
|
|
1228
|
+
|
|
1229
|
+
auto_minor_version_upgrade = true
|
|
1230
|
+
monitoring_interval = 5
|
|
1231
|
+
monitoring_role_arn = aws_iam_role.enhanced_monitoring.arn
|
|
1232
|
+
performance_insights_enabled = var.enable_performance_insights
|
|
1233
|
+
performance_insights_kms_key_id = var.enable_performance_insights ? aws_kms_key.database.arn : null
|
|
1234
|
+
performance_insights_retention_period = var.enable_performance_insights ? var.performance_insights_retention_period : null
|
|
1235
|
+
|
|
1236
|
+
tags = merge(var.tags, {
|
|
1237
|
+
Name = "\${var.name}-aurora-\${count.index + 1}"
|
|
1238
|
+
})
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
resource "aws_security_group" "rotation" {
|
|
1242
|
+
#checkov:skip=CKV2_AWS_5:Security group is attached to the hosted rotation Lambda created by AWS::SecretsManager::RotationSchedule; Checkov cannot resolve resources created by the CloudFormation transform
|
|
1243
|
+
count = var.enable_credential_rotation ? 1 : 0
|
|
1244
|
+
name_prefix = "\${var.name}-aurora-rotation-"
|
|
1245
|
+
description = "Security group for Aurora secret rotation \${var.name}"
|
|
1246
|
+
vpc_id = var.vpc_id
|
|
1247
|
+
|
|
1248
|
+
tags = merge(var.tags, {
|
|
1249
|
+
Name = "\${var.name}-aurora-rotation"
|
|
1250
|
+
})
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
resource "aws_vpc_security_group_egress_rule" "rotation_to_database" {
|
|
1254
|
+
count = var.enable_credential_rotation ? 1 : 0
|
|
1255
|
+
security_group_id = aws_security_group.rotation[0].id
|
|
1256
|
+
referenced_security_group_id = aws_security_group.database.id
|
|
1257
|
+
from_port = coalesce(var.port, local.default_port)
|
|
1258
|
+
to_port = coalesce(var.port, local.default_port)
|
|
1259
|
+
ip_protocol = "tcp"
|
|
1260
|
+
description = "Allow outbound database traffic from secret rotation Lambda"
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
resource "aws_vpc_security_group_ingress_rule" "rotation_to_database" {
|
|
1264
|
+
count = var.enable_credential_rotation ? 1 : 0
|
|
1265
|
+
security_group_id = aws_security_group.database.id
|
|
1266
|
+
referenced_security_group_id = aws_security_group.rotation[0].id
|
|
1267
|
+
from_port = coalesce(var.port, local.default_port)
|
|
1268
|
+
to_port = coalesce(var.port, local.default_port)
|
|
1269
|
+
ip_protocol = "tcp"
|
|
1270
|
+
description = "Allow inbound database traffic from secret rotation Lambda"
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
resource "aws_cloudformation_stack" "credentials_rotation" {
|
|
1274
|
+
#checkov:skip=CKV_AWS_124:SNS event notifications are not required for the credentials rotation stack
|
|
1275
|
+
count = var.enable_credential_rotation ? 1 : 0
|
|
1276
|
+
name = "\${var.name}-aurora-credentials-rotation"
|
|
1277
|
+
|
|
1278
|
+
capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"]
|
|
1279
|
+
|
|
1280
|
+
template_body = jsonencode({
|
|
1281
|
+
AWSTemplateFormatVersion = "2010-09-09"
|
|
1282
|
+
Transform = "AWS::SecretsManager-2024-09-16"
|
|
1283
|
+
Resources = {
|
|
1284
|
+
CredentialsRotationSchedule = {
|
|
1285
|
+
Type = "AWS::SecretsManager::RotationSchedule"
|
|
1286
|
+
Properties = {
|
|
1287
|
+
SecretId = aws_secretsmanager_secret.credentials.arn
|
|
1288
|
+
HostedRotationLambda = {
|
|
1289
|
+
RotationType = var.engine == "aurora-mysql" ? "MySQLSingleUser" : "PostgreSQLSingleUser"
|
|
1290
|
+
RotationLambdaName = "\${var.name}-aurora-credentials-rotation"
|
|
1291
|
+
VpcSecurityGroupIds = aws_security_group.rotation[0].id
|
|
1292
|
+
VpcSubnetIds = join(",", var.lambda_subnet_ids)
|
|
1293
|
+
}
|
|
1294
|
+
RotationRules = {
|
|
1295
|
+
AutomaticallyAfterDays = 30
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
})
|
|
1301
|
+
|
|
1302
|
+
depends_on = [
|
|
1303
|
+
aws_secretsmanager_secret_version.credentials,
|
|
1304
|
+
aws_rds_cluster_instance.database,
|
|
1305
|
+
aws_vpc_security_group_egress_rule.rotation_to_database,
|
|
1306
|
+
aws_vpc_security_group_ingress_rule.rotation_to_database,
|
|
1307
|
+
]
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
resource "aws_iam_role" "proxy" {
|
|
1311
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1312
|
+
|
|
1313
|
+
name_prefix = "\${var.name}-aurora-proxy-"
|
|
1314
|
+
|
|
1315
|
+
assume_role_policy = jsonencode({
|
|
1316
|
+
Version = "2012-10-17"
|
|
1317
|
+
Statement = [
|
|
1318
|
+
{
|
|
1319
|
+
Effect = "Allow"
|
|
1320
|
+
Principal = {
|
|
1321
|
+
Service = "rds.amazonaws.com"
|
|
1322
|
+
}
|
|
1323
|
+
Action = "sts:AssumeRole"
|
|
1324
|
+
}
|
|
1325
|
+
]
|
|
1326
|
+
})
|
|
1327
|
+
|
|
1328
|
+
tags = merge(var.tags, {
|
|
1329
|
+
Name = "\${var.name}-aurora-proxy"
|
|
1330
|
+
})
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
resource "aws_iam_role_policy" "proxy_secret_access" {
|
|
1334
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1335
|
+
role = aws_iam_role.proxy[0].name
|
|
1336
|
+
|
|
1337
|
+
policy = jsonencode({
|
|
1338
|
+
Version = "2012-10-17"
|
|
1339
|
+
Statement = [
|
|
1340
|
+
{
|
|
1341
|
+
Effect = "Allow"
|
|
1342
|
+
Action = ["secretsmanager:GetSecretValue"]
|
|
1343
|
+
Resource = [aws_secretsmanager_secret.credentials.arn]
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
Effect = "Allow"
|
|
1347
|
+
Action = ["kms:Decrypt"]
|
|
1348
|
+
Resource = [aws_kms_key.database.arn]
|
|
1349
|
+
}
|
|
1350
|
+
]
|
|
1351
|
+
})
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
resource "aws_security_group" "proxy" {
|
|
1355
|
+
#checkov:skip=CKV2_AWS_5:Security group is attached to aws_db_proxy.aurora; Checkov cannot resolve count-conditional cross-resource references
|
|
1356
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1357
|
+
|
|
1358
|
+
name_prefix = "\${var.name}-aurora-proxy-"
|
|
1359
|
+
description = "Security group for Aurora proxy \${var.name}"
|
|
1360
|
+
vpc_id = var.vpc_id
|
|
1361
|
+
|
|
1362
|
+
tags = merge(var.tags, {
|
|
1363
|
+
Name = "\${var.name}-aurora-proxy"
|
|
1364
|
+
})
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
resource "aws_db_proxy" "aurora" {
|
|
1368
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1369
|
+
|
|
1370
|
+
name = "\${var.name}-proxy"
|
|
1371
|
+
default_auth_scheme = "IAM_AUTH"
|
|
1372
|
+
engine_family = var.engine == "aurora-mysql" ? "MYSQL" : "POSTGRESQL"
|
|
1373
|
+
role_arn = aws_iam_role.proxy[0].arn
|
|
1374
|
+
vpc_subnet_ids = var.subnet_ids
|
|
1375
|
+
vpc_security_group_ids = [aws_security_group.proxy[0].id]
|
|
1376
|
+
require_tls = true
|
|
1377
|
+
|
|
1378
|
+
auth {
|
|
1379
|
+
auth_scheme = "SECRETS"
|
|
1380
|
+
iam_auth = "REQUIRED"
|
|
1381
|
+
secret_arn = aws_secretsmanager_secret.credentials.arn
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
depends_on = [
|
|
1385
|
+
aws_iam_role_policy.proxy_secret_access,
|
|
1386
|
+
aws_secretsmanager_secret_version.credentials,
|
|
1387
|
+
]
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
resource "aws_db_proxy_default_target_group" "default" {
|
|
1391
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1392
|
+
|
|
1393
|
+
db_proxy_name = aws_db_proxy.aurora[0].name
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
resource "aws_db_proxy_target" "default" {
|
|
1397
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1398
|
+
|
|
1399
|
+
db_proxy_name = aws_db_proxy.aurora[0].name
|
|
1400
|
+
target_group_name = aws_db_proxy_default_target_group.default[0].name
|
|
1401
|
+
db_cluster_identifier = aws_rds_cluster.database.cluster_identifier
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
resource "null_resource" "proxy_target_ready" {
|
|
1405
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1406
|
+
|
|
1407
|
+
triggers = {
|
|
1408
|
+
db_proxy_name = aws_db_proxy.aurora[0].name
|
|
1409
|
+
target_group_name = aws_db_proxy_default_target_group.default[0].name
|
|
1410
|
+
db_cluster_identifier = aws_rds_cluster.database.cluster_identifier
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
provisioner "local-exec" {
|
|
1414
|
+
command = <<-EOT
|
|
1415
|
+
start_time=$(date +%s)
|
|
1416
|
+
deadline=$((start_time + 600))
|
|
1417
|
+
while true; do
|
|
1418
|
+
target_state=$(aws rds describe-db-proxy-targets \\
|
|
1419
|
+
--region \${data.aws_region.current.region} \\
|
|
1420
|
+
--db-proxy-name \${self.triggers.db_proxy_name} \\
|
|
1421
|
+
--target-group-name \${self.triggers.target_group_name} \\
|
|
1422
|
+
--query "Targets[?Type=='RDS_INSTANCE'] | [0].TargetHealth.State" \\
|
|
1423
|
+
--output text)
|
|
1424
|
+
|
|
1425
|
+
if [ "$target_state" = "AVAILABLE" ]; then
|
|
1426
|
+
exit 0
|
|
1427
|
+
fi
|
|
1428
|
+
|
|
1429
|
+
current_time=$(date +%s)
|
|
1430
|
+
if [ "$current_time" -ge "$deadline" ]; then
|
|
1431
|
+
echo "RDS Proxy target \${self.triggers.db_cluster_identifier} did not become AVAILABLE within 10 minutes. Last state: $target_state"
|
|
1432
|
+
exit 1
|
|
1433
|
+
fi
|
|
1434
|
+
|
|
1435
|
+
sleep 10
|
|
1436
|
+
done
|
|
1437
|
+
EOT
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
depends_on = [
|
|
1441
|
+
aws_db_proxy_target.default,
|
|
1442
|
+
aws_rds_cluster_instance.database,
|
|
1443
|
+
aws_vpc_security_group_ingress_rule.proxy_to_database,
|
|
1444
|
+
aws_vpc_security_group_egress_rule.proxy_to_database
|
|
1445
|
+
]
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
resource "null_resource" "cluster_ready" {
|
|
1449
|
+
count = var.enable_rds_proxy ? 0 : 1
|
|
1450
|
+
|
|
1451
|
+
triggers = {
|
|
1452
|
+
cluster_identifier = aws_rds_cluster.database.cluster_identifier
|
|
1453
|
+
instance_ids = join(",", aws_rds_cluster_instance.database[*].id)
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
depends_on = [aws_rds_cluster_instance.database]
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
resource "aws_vpc_security_group_ingress_rule" "proxy_to_database" {
|
|
1460
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1461
|
+
|
|
1462
|
+
security_group_id = aws_security_group.database.id
|
|
1463
|
+
referenced_security_group_id = aws_security_group.proxy[0].id
|
|
1464
|
+
from_port = coalesce(var.port, local.default_port)
|
|
1465
|
+
to_port = coalesce(var.port, local.default_port)
|
|
1466
|
+
ip_protocol = "tcp"
|
|
1467
|
+
description = "Allow inbound database traffic from RDS Proxy"
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
resource "aws_vpc_security_group_egress_rule" "proxy_to_database" {
|
|
1471
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1472
|
+
|
|
1473
|
+
security_group_id = aws_security_group.proxy[0].id
|
|
1474
|
+
referenced_security_group_id = aws_security_group.database.id
|
|
1475
|
+
from_port = coalesce(var.port, local.default_port)
|
|
1476
|
+
to_port = coalesce(var.port, local.default_port)
|
|
1477
|
+
ip_protocol = "tcp"
|
|
1478
|
+
description = "Allow outbound database traffic from RDS Proxy to Aurora cluster"
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
data "aws_iam_policy" "backup" {
|
|
1482
|
+
count = var.enable_backup ? 1 : 0
|
|
1483
|
+
|
|
1484
|
+
arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup"
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
resource "aws_iam_role" "backup" {
|
|
1488
|
+
count = var.enable_backup ? 1 : 0
|
|
1489
|
+
|
|
1490
|
+
name_prefix = "\${var.name}-backup-"
|
|
1491
|
+
|
|
1492
|
+
assume_role_policy = jsonencode({
|
|
1493
|
+
Version = "2012-10-17"
|
|
1494
|
+
Statement = [
|
|
1495
|
+
{
|
|
1496
|
+
Effect = "Allow"
|
|
1497
|
+
Principal = { Service = "backup.amazonaws.com" }
|
|
1498
|
+
Action = "sts:AssumeRole"
|
|
1499
|
+
}
|
|
1500
|
+
]
|
|
1501
|
+
})
|
|
1502
|
+
|
|
1503
|
+
tags = var.tags
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
resource "aws_iam_role_policy_attachment" "backup" {
|
|
1507
|
+
count = var.enable_backup ? 1 : 0
|
|
1508
|
+
|
|
1509
|
+
role = aws_iam_role.backup[0].name
|
|
1510
|
+
policy_arn = data.aws_iam_policy.backup[0].arn
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
resource "aws_backup_vault" "database" {
|
|
1514
|
+
count = var.enable_backup ? 1 : 0
|
|
1515
|
+
|
|
1516
|
+
name = "\${var.name}-aurora-backup"
|
|
1517
|
+
kms_key_arn = aws_kms_key.database.arn
|
|
1518
|
+
tags = var.tags
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
resource "aws_backup_plan" "database" {
|
|
1522
|
+
count = var.enable_backup ? 1 : 0
|
|
1523
|
+
|
|
1524
|
+
name = "\${var.name}-aurora-backup"
|
|
1525
|
+
|
|
1526
|
+
rule {
|
|
1527
|
+
rule_name = "daily-backup"
|
|
1528
|
+
target_vault_name = aws_backup_vault.database[0].name
|
|
1529
|
+
schedule = "cron(0 5 ? * * *)"
|
|
1530
|
+
|
|
1531
|
+
lifecycle {
|
|
1532
|
+
delete_after = 35
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
tags = var.tags
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
resource "aws_backup_selection" "database" {
|
|
1540
|
+
count = var.enable_backup ? 1 : 0
|
|
1541
|
+
|
|
1542
|
+
name = "\${var.name}-aurora"
|
|
1543
|
+
plan_id = aws_backup_plan.database[0].id
|
|
1544
|
+
iam_role_arn = aws_iam_role.backup[0].arn
|
|
1545
|
+
|
|
1546
|
+
resources = [aws_rds_cluster.database.arn]
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
output "cluster_arn" {
|
|
1550
|
+
description = "ARN of the Aurora cluster."
|
|
1551
|
+
value = aws_rds_cluster.database.arn
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
output "cluster_resource_id" {
|
|
1555
|
+
description = "Resource ID of the Aurora cluster used for IAM database authentication."
|
|
1556
|
+
value = aws_rds_cluster.database.cluster_resource_id
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
output "proxy_resource_id" {
|
|
1560
|
+
description = "Resource identifier of the RDS Proxy (prx-XXXX), used for IAM rds-db:connect policies when connecting through the proxy. Null if RDS Proxy is disabled."
|
|
1561
|
+
value = var.enable_rds_proxy ? split(":", aws_db_proxy.aurora[0].arn)[6] : null
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
output "cluster_endpoint" {
|
|
1565
|
+
description = "Writer endpoint of the Aurora cluster or proxy."
|
|
1566
|
+
value = var.enable_rds_proxy ? aws_db_proxy.aurora[0].endpoint : aws_rds_cluster.database.endpoint
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
output "reader_endpoint" {
|
|
1570
|
+
description = "Reader endpoint of the Aurora cluster."
|
|
1571
|
+
value = aws_rds_cluster.database.reader_endpoint
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
output "cluster_port" {
|
|
1575
|
+
description = "Port exposed by the Aurora cluster."
|
|
1576
|
+
value = aws_rds_cluster.database.port
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
output "security_group_id" {
|
|
1580
|
+
description = "Security group protecting the Aurora proxy or cluster."
|
|
1581
|
+
value = var.enable_rds_proxy ? aws_security_group.proxy[0].id : aws_security_group.database.id
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
output "database_security_group_id" {
|
|
1585
|
+
description = "Security group protecting the Aurora cluster directly."
|
|
1586
|
+
value = aws_security_group.database.id
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
output "proxy_role_name" {
|
|
1590
|
+
description = "Name of the IAM role used by the RDS Proxy, or null if proxy is disabled."
|
|
1591
|
+
value = var.enable_rds_proxy ? aws_iam_role.proxy[0].name : null
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
output "secret_arn" {
|
|
1595
|
+
description = "ARN of the generated admin credentials secret."
|
|
1596
|
+
value = aws_secretsmanager_secret.credentials.arn
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
output "kms_key_arn" {
|
|
1600
|
+
description = "ARN of the KMS key protecting Aurora and the generated secret."
|
|
1601
|
+
value = aws_kms_key.database.arn
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
output "database_host" {
|
|
1605
|
+
description = "Hostname of the Aurora cluster writer endpoint or proxy endpoint."
|
|
1606
|
+
value = var.enable_rds_proxy ? aws_db_proxy.aurora[0].endpoint : aws_rds_cluster.database.endpoint
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
output "database_ready" {
|
|
1610
|
+
description = "Dependency marker that is resolved after the selected database connection path is ready."
|
|
1611
|
+
value = true
|
|
1612
|
+
|
|
1613
|
+
depends_on = [
|
|
1614
|
+
null_resource.proxy_target_ready,
|
|
1615
|
+
null_resource.cluster_ready
|
|
1616
|
+
]
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
output "database_name" {
|
|
1620
|
+
description = "Initial database created in the cluster."
|
|
1621
|
+
value = var.database_name
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
output "admin_user" {
|
|
1625
|
+
description = "Admin username stored in the generated secret."
|
|
1626
|
+
value = jsondecode(aws_secretsmanager_secret_version.credentials.secret_string).username
|
|
1627
|
+
}
|
|
1628
|
+
"
|
|
1629
|
+
`;
|
|
1630
|
+
|
|
1631
|
+
exports[`ts#rdb generator > should generate terraform modules when iacProvider is Terraform 2`] = `
|
|
1632
|
+
"terraform {
|
|
1633
|
+
required_version = ">= 1.0"
|
|
1634
|
+
|
|
1635
|
+
required_providers {
|
|
1636
|
+
archive = {
|
|
1637
|
+
source = "hashicorp/archive"
|
|
1638
|
+
version = "~> 2.5"
|
|
1639
|
+
}
|
|
1640
|
+
aws = {
|
|
1641
|
+
source = "hashicorp/aws"
|
|
1642
|
+
version = "~> 6.33"
|
|
1643
|
+
}
|
|
1644
|
+
null = {
|
|
1645
|
+
source = "hashicorp/null"
|
|
1646
|
+
version = ">= 3.0"
|
|
1647
|
+
}
|
|
1648
|
+
random = {
|
|
1649
|
+
source = "hashicorp/random"
|
|
1650
|
+
version = ">= 3.0"
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
variable "vpc_id" {
|
|
1656
|
+
description = "VPC where the Aurora cluster will be deployed."
|
|
1657
|
+
type = string
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
variable "database_subnet_ids" {
|
|
1661
|
+
description = "Subnet IDs for the Aurora cluster and RDS Proxy. Private isolated subnets are fine — no outbound egress required."
|
|
1662
|
+
type = list(string)
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
variable "lambda_subnet_ids" {
|
|
1666
|
+
description = "Subnet IDs for the create-db-user and migration Lambda functions. Must have outbound egress to AWS service endpoints (e.g. Secrets Manager) — use private subnets with a NAT gateway."
|
|
1667
|
+
type = list(string)
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
variable "engine_version" {
|
|
1671
|
+
description = "Aurora engine version."
|
|
1672
|
+
type = string
|
|
1673
|
+
default = null
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
variable "port" {
|
|
1677
|
+
description = "Database port for the selected Aurora engine."
|
|
1678
|
+
type = number
|
|
1679
|
+
default = null
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
variable "serverless_min_capacity" {
|
|
1683
|
+
description = "Minimum Aurora Serverless v2 ACUs."
|
|
1684
|
+
type = number
|
|
1685
|
+
default = 0.5
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
variable "serverless_max_capacity" {
|
|
1689
|
+
description = "Maximum Aurora Serverless v2 ACUs."
|
|
1690
|
+
type = number
|
|
1691
|
+
default = 4
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
variable "instance_count" {
|
|
1695
|
+
description = "Number of Aurora instances to create."
|
|
1696
|
+
type = number
|
|
1697
|
+
default = 1
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
variable "deletion_protection" {
|
|
1701
|
+
description = "Whether deletion protection is enabled."
|
|
1702
|
+
type = bool
|
|
1703
|
+
default = true
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
variable "skip_final_snapshot" {
|
|
1707
|
+
description = "Whether to skip the final snapshot on deletion."
|
|
1708
|
+
type = bool
|
|
1709
|
+
default = false
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
variable "enable_rds_proxy" {
|
|
1713
|
+
description = "Whether to provision an RDS Proxy in front of the Aurora cluster."
|
|
1714
|
+
type = bool
|
|
1715
|
+
default = true
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
variable "enable_credential_rotation" {
|
|
1719
|
+
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
1720
|
+
type = bool
|
|
1721
|
+
default = true
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
variable "enable_cloudwatch_logs" {
|
|
1725
|
+
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL also enables verbose statement logging."
|
|
1726
|
+
type = bool
|
|
1727
|
+
default = false
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
variable "enable_performance_insights" {
|
|
1731
|
+
description = "Whether to enable Performance Insights on Aurora cluster instances."
|
|
1732
|
+
type = bool
|
|
1733
|
+
default = true
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
variable "performance_insights_retention_period" {
|
|
1737
|
+
description = "Retention period, in days, for Performance Insights data when enabled."
|
|
1738
|
+
type = number
|
|
1739
|
+
default = 7
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
variable "enable_backup" {
|
|
1743
|
+
description = "Whether to provision an AWS Backup plan for the Aurora cluster."
|
|
1744
|
+
type = bool
|
|
1745
|
+
default = false
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
variable "tags" {
|
|
1749
|
+
description = "Tags to apply to all resources."
|
|
1750
|
+
type = map(string)
|
|
1751
|
+
default = {}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
variable "docker_image_tag" {
|
|
1755
|
+
description = "Docker image tag for the migration handler. Defaults to the tag built by the Nx docker target."
|
|
1756
|
+
type = string
|
|
1757
|
+
default = "proj-db-migration:latest"
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
variable "asset_bucket_name" {
|
|
1761
|
+
description = "Name of the shared asset S3 bucket used to stage the Lambda deployment zip. Instantiate the \`core/asset-bucket\` module once per deployment and pass its \`bucket_name\` output here."
|
|
1762
|
+
type = string
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
data "aws_region" "current" {}
|
|
1766
|
+
data "aws_caller_identity" "current" {}
|
|
1767
|
+
|
|
1768
|
+
resource "random_string" "suffix" {
|
|
1769
|
+
length = 8
|
|
1770
|
+
special = false
|
|
1771
|
+
upper = false
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
locals {
|
|
1775
|
+
create_db_user_bundle_path = "\${path.module}/../../../../../../../dist/packages/db/bundle/create-db-user"
|
|
1776
|
+
migration_function_name = "db-migration-\${random_string.suffix.result}"
|
|
1777
|
+
create_db_user_function_name = "db-create-db-user-\${random_string.suffix.result}"
|
|
1778
|
+
database_runtime_user = "db_\${random_string.suffix.result}"
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
module "aurora" {
|
|
1782
|
+
source = "../../../core/rdb/aurora"
|
|
1783
|
+
#checkov:skip=CKV_AWS_139:Deletion protection is enabled but checkov is unable to detect it correctly
|
|
1784
|
+
name = "db"
|
|
1785
|
+
vpc_id = var.vpc_id
|
|
1786
|
+
subnet_ids = var.database_subnet_ids
|
|
1787
|
+
lambda_subnet_ids = var.lambda_subnet_ids
|
|
1788
|
+
engine = "aurora-postgresql"
|
|
1789
|
+
engine_version = var.engine_version
|
|
1790
|
+
database_name = "database_name"
|
|
1791
|
+
admin_user = "databaseUser"
|
|
1792
|
+
port = var.port
|
|
1793
|
+
serverless_min_capacity = var.serverless_min_capacity
|
|
1794
|
+
serverless_max_capacity = var.serverless_max_capacity
|
|
1795
|
+
instance_count = var.instance_count
|
|
1796
|
+
deletion_protection = var.deletion_protection
|
|
1797
|
+
skip_final_snapshot = var.skip_final_snapshot
|
|
1798
|
+
enable_rds_proxy = var.enable_rds_proxy
|
|
1799
|
+
enable_credential_rotation = var.enable_credential_rotation
|
|
1800
|
+
enable_cloudwatch_logs = var.enable_cloudwatch_logs
|
|
1801
|
+
enable_performance_insights = var.enable_performance_insights
|
|
1802
|
+
performance_insights_retention_period = var.performance_insights_retention_period
|
|
1803
|
+
enable_backup = var.enable_backup
|
|
1804
|
+
tags = var.tags
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
resource "aws_iam_role_policy" "proxy_db_user_connect" {
|
|
1808
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
1809
|
+
|
|
1810
|
+
role = module.aurora.proxy_role_name
|
|
1811
|
+
|
|
1812
|
+
policy = jsonencode({
|
|
1813
|
+
Version = "2012-10-17"
|
|
1814
|
+
Statement = [
|
|
1815
|
+
{
|
|
1816
|
+
Effect = "Allow"
|
|
1817
|
+
Action = ["rds-db:connect"]
|
|
1818
|
+
Resource = [
|
|
1819
|
+
"arn:aws:rds-db:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:dbuser:\${module.aurora.cluster_resource_id}/\${local.database_runtime_user}"
|
|
1820
|
+
]
|
|
1821
|
+
}
|
|
1822
|
+
]
|
|
1823
|
+
})
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
data "archive_file" "create_db_user_zip" {
|
|
1827
|
+
type = "zip"
|
|
1828
|
+
source_dir = local.create_db_user_bundle_path
|
|
1829
|
+
output_path = "\${path.module}/../../../../../../../dist/packages/common/terraform/dbs/db/create-db-user.zip"
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
resource "aws_s3_object" "create_db_user_zip" {
|
|
1833
|
+
bucket = var.asset_bucket_name
|
|
1834
|
+
key = "dbs/db/\${data.archive_file.create_db_user_zip.output_sha256}.zip"
|
|
1835
|
+
source = data.archive_file.create_db_user_zip.output_path
|
|
1836
|
+
source_hash = data.archive_file.create_db_user_zip.output_base64sha256
|
|
1837
|
+
etag = data.archive_file.create_db_user_zip.output_md5
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
module "add_rdb_to_runtime_config" {
|
|
1841
|
+
source = "../../../core/runtime-config/entry"
|
|
1842
|
+
|
|
1843
|
+
namespace = "database"
|
|
1844
|
+
key = "Db"
|
|
1845
|
+
value = {
|
|
1846
|
+
hostname = module.aurora.cluster_endpoint
|
|
1847
|
+
port = module.aurora.cluster_port
|
|
1848
|
+
database = module.aurora.database_name
|
|
1849
|
+
adminUser = module.aurora.admin_user
|
|
1850
|
+
dbUser = local.database_runtime_user
|
|
1851
|
+
region = data.aws_region.current.region
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
resource "aws_ecr_repository" "migration_handler" {
|
|
1856
|
+
#checkov:skip=CKV_AWS_136:AES256 encryption is sufficient for ECR repositories
|
|
1857
|
+
#checkov:skip=CKV_AWS_51:Mutable tags are intentional; the migration handler is always pushed as latest and redeployment is gated on docker_digest changes in null_resource.docker_publish
|
|
1858
|
+
name = "db-migration-\${random_string.suffix.result}"
|
|
1859
|
+
image_tag_mutability = "MUTABLE"
|
|
1860
|
+
force_delete = true
|
|
1861
|
+
|
|
1862
|
+
image_scanning_configuration {
|
|
1863
|
+
scan_on_push = true
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
tags = var.tags
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
resource "aws_ecr_repository_policy" "migration_handler" {
|
|
1870
|
+
repository = aws_ecr_repository.migration_handler.name
|
|
1871
|
+
|
|
1872
|
+
policy = jsonencode({
|
|
1873
|
+
Version = "2012-10-17"
|
|
1874
|
+
Statement = [
|
|
1875
|
+
{
|
|
1876
|
+
Sid = "AllowPushPull"
|
|
1877
|
+
Effect = "Allow"
|
|
1878
|
+
Principal = {
|
|
1879
|
+
AWS = "arn:aws:iam::\${data.aws_caller_identity.current.account_id}:root"
|
|
1880
|
+
}
|
|
1881
|
+
Action = [
|
|
1882
|
+
"ecr:BatchCheckLayerAvailability",
|
|
1883
|
+
"ecr:CompleteLayerUpload",
|
|
1884
|
+
"ecr:GetDownloadUrlForLayer",
|
|
1885
|
+
"ecr:InitiateLayerUpload",
|
|
1886
|
+
"ecr:PutImage",
|
|
1887
|
+
"ecr:UploadLayerPart"
|
|
1888
|
+
]
|
|
1889
|
+
},
|
|
1890
|
+
{
|
|
1891
|
+
Sid = "AllowLambdaPull"
|
|
1892
|
+
Effect = "Allow"
|
|
1893
|
+
Principal = {
|
|
1894
|
+
Service = "lambda.amazonaws.com"
|
|
1895
|
+
}
|
|
1896
|
+
Action = [
|
|
1897
|
+
"ecr:BatchCheckLayerAvailability",
|
|
1898
|
+
"ecr:BatchGetImage",
|
|
1899
|
+
"ecr:GetDownloadUrlForLayer"
|
|
1900
|
+
]
|
|
1901
|
+
}
|
|
1902
|
+
]
|
|
1903
|
+
})
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
data "external" "docker_digest" {
|
|
1907
|
+
program = ["sh", "-c", "echo '{\\"digest\\":\\"'$(docker inspect \${var.docker_image_tag} --format '{{.Id}}')'\\"}' "]
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
resource "null_resource" "docker_publish" {
|
|
1911
|
+
triggers = {
|
|
1912
|
+
docker_digest = data.external.docker_digest.result.digest
|
|
1913
|
+
repository_url = aws_ecr_repository.migration_handler.repository_url
|
|
1914
|
+
docker_image_tag = var.docker_image_tag
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
provisioner "local-exec" {
|
|
1918
|
+
command = <<-EOT
|
|
1919
|
+
aws ecr get-login-password --region \${data.aws_region.current.region} | docker login --username AWS --password-stdin \${self.triggers.repository_url}
|
|
1920
|
+
docker tag \${self.triggers.docker_image_tag} \${self.triggers.repository_url}:latest
|
|
1921
|
+
docker push \${self.triggers.repository_url}:latest
|
|
1922
|
+
EOT
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
depends_on = [aws_ecr_repository_policy.migration_handler]
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
resource "aws_iam_role" "migration_handler" {
|
|
1929
|
+
name = "DbMigrationHandlerRole-\${random_string.suffix.result}"
|
|
1930
|
+
|
|
1931
|
+
assume_role_policy = jsonencode({
|
|
1932
|
+
Version = "2012-10-17"
|
|
1933
|
+
Statement = [
|
|
1934
|
+
{
|
|
1935
|
+
Effect = "Allow"
|
|
1936
|
+
Principal = {
|
|
1937
|
+
Service = "lambda.amazonaws.com"
|
|
1938
|
+
}
|
|
1939
|
+
Action = "sts:AssumeRole"
|
|
1940
|
+
}
|
|
1941
|
+
]
|
|
1942
|
+
})
|
|
1943
|
+
|
|
1944
|
+
tags = var.tags
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
resource "aws_iam_role_policy_attachment" "migration_handler_basic_execution" {
|
|
1948
|
+
role = aws_iam_role.migration_handler.name
|
|
1949
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
resource "aws_iam_role_policy_attachment" "migration_handler_vpc_access" {
|
|
1953
|
+
role = aws_iam_role.migration_handler.name
|
|
1954
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
resource "aws_iam_role_policy_attachment" "migration_handler_xray" {
|
|
1958
|
+
role = aws_iam_role.migration_handler.name
|
|
1959
|
+
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
resource "aws_iam_role_policy" "migration_handler_access" {
|
|
1963
|
+
role = aws_iam_role.migration_handler.name
|
|
1964
|
+
|
|
1965
|
+
policy = jsonencode({
|
|
1966
|
+
Version = "2012-10-17"
|
|
1967
|
+
Statement = [
|
|
1968
|
+
{
|
|
1969
|
+
Effect = "Allow"
|
|
1970
|
+
Action = [
|
|
1971
|
+
"rds-db:connect"
|
|
1972
|
+
]
|
|
1973
|
+
Resource = [
|
|
1974
|
+
"arn:aws:rds-db:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:dbuser:\${var.enable_rds_proxy ? module.aurora.proxy_resource_id : module.aurora.cluster_resource_id}/\${local.database_runtime_user}"
|
|
1975
|
+
]
|
|
1976
|
+
}
|
|
1977
|
+
]
|
|
1978
|
+
})
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
resource "aws_iam_role" "create_db_user" {
|
|
1982
|
+
name = "DbCreateDbUserRole-\${random_string.suffix.result}"
|
|
1983
|
+
|
|
1984
|
+
assume_role_policy = jsonencode({
|
|
1985
|
+
Version = "2012-10-17"
|
|
1986
|
+
Statement = [
|
|
1987
|
+
{
|
|
1988
|
+
Effect = "Allow"
|
|
1989
|
+
Principal = {
|
|
1990
|
+
Service = "lambda.amazonaws.com"
|
|
1991
|
+
}
|
|
1992
|
+
Action = "sts:AssumeRole"
|
|
1993
|
+
}
|
|
1994
|
+
]
|
|
1995
|
+
})
|
|
1996
|
+
|
|
1997
|
+
tags = var.tags
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
resource "aws_iam_role_policy_attachment" "create_db_user_basic_execution" {
|
|
2001
|
+
role = aws_iam_role.create_db_user.name
|
|
2002
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
resource "aws_iam_role_policy_attachment" "create_db_user_vpc_access" {
|
|
2006
|
+
role = aws_iam_role.create_db_user.name
|
|
2007
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
resource "aws_iam_role_policy_attachment" "create_db_user_xray" {
|
|
2011
|
+
role = aws_iam_role.create_db_user.name
|
|
2012
|
+
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
resource "aws_iam_role_policy" "create_db_user_secret_access" {
|
|
2016
|
+
role = aws_iam_role.create_db_user.name
|
|
2017
|
+
|
|
2018
|
+
policy = jsonencode({
|
|
2019
|
+
Version = "2012-10-17"
|
|
2020
|
+
Statement = [
|
|
2021
|
+
{
|
|
2022
|
+
Effect = "Allow"
|
|
2023
|
+
Action = [
|
|
2024
|
+
"secretsmanager:GetSecretValue"
|
|
2025
|
+
]
|
|
2026
|
+
Resource = [module.aurora.secret_arn]
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
Effect = "Allow"
|
|
2030
|
+
Action = [
|
|
2031
|
+
"kms:Decrypt"
|
|
2032
|
+
]
|
|
2033
|
+
Resource = [module.aurora.kms_key_arn]
|
|
2034
|
+
}
|
|
2035
|
+
]
|
|
2036
|
+
})
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
resource "aws_cloudwatch_log_group" "migration_handler" {
|
|
2040
|
+
#checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
|
|
2041
|
+
#checkov:skip=CKV_AWS_338:Log retention set to forever
|
|
2042
|
+
#checkov:skip=CKV_AWS_66:Log retention set to forever
|
|
2043
|
+
name = "/aws/lambda/\${local.migration_function_name}"
|
|
2044
|
+
tags = var.tags
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
resource "aws_cloudwatch_log_group" "create_db_user" {
|
|
2048
|
+
#checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
|
|
2049
|
+
#checkov:skip=CKV_AWS_338:Log retention set to forever
|
|
2050
|
+
#checkov:skip=CKV_AWS_66:Log retention set to forever
|
|
2051
|
+
name = "/aws/lambda/\${local.create_db_user_function_name}"
|
|
2052
|
+
tags = var.tags
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
resource "aws_security_group" "migration_handler" {
|
|
2056
|
+
name_prefix = "db-migration-"
|
|
2057
|
+
description = "Security group for the migration Lambda function"
|
|
2058
|
+
vpc_id = var.vpc_id
|
|
2059
|
+
tags = var.tags
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
resource "aws_vpc_security_group_egress_rule" "migration_handler_to_database" {
|
|
2063
|
+
security_group_id = aws_security_group.migration_handler.id
|
|
2064
|
+
referenced_security_group_id = module.aurora.security_group_id
|
|
2065
|
+
from_port = module.aurora.cluster_port
|
|
2066
|
+
to_port = module.aurora.cluster_port
|
|
2067
|
+
ip_protocol = "tcp"
|
|
2068
|
+
description = "Allow outbound traffic to Aurora on database port"
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
resource "aws_vpc_security_group_egress_rule" "migration_handler_https" {
|
|
2072
|
+
security_group_id = aws_security_group.migration_handler.id
|
|
2073
|
+
cidr_ipv4 = "0.0.0.0/0"
|
|
2074
|
+
from_port = 443
|
|
2075
|
+
to_port = 443
|
|
2076
|
+
ip_protocol = "tcp"
|
|
2077
|
+
description = "Allow outbound HTTPS to AWS service endpoints"
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
resource "aws_security_group" "create_db_user" {
|
|
2081
|
+
name_prefix = "db-create-db-user-"
|
|
2082
|
+
description = "Security group for the create-db-user Lambda function"
|
|
2083
|
+
vpc_id = var.vpc_id
|
|
2084
|
+
tags = var.tags
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
resource "aws_vpc_security_group_egress_rule" "create_db_user_to_database" {
|
|
2088
|
+
security_group_id = aws_security_group.create_db_user.id
|
|
2089
|
+
referenced_security_group_id = module.aurora.database_security_group_id
|
|
2090
|
+
from_port = module.aurora.cluster_port
|
|
2091
|
+
to_port = module.aurora.cluster_port
|
|
2092
|
+
ip_protocol = "tcp"
|
|
2093
|
+
description = "Allow outbound traffic to Aurora on database port"
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
resource "aws_vpc_security_group_egress_rule" "create_db_user_https" {
|
|
2097
|
+
security_group_id = aws_security_group.create_db_user.id
|
|
2098
|
+
cidr_ipv4 = "0.0.0.0/0"
|
|
2099
|
+
from_port = 443
|
|
2100
|
+
to_port = 443
|
|
2101
|
+
ip_protocol = "tcp"
|
|
2102
|
+
description = "Allow outbound HTTPS to AWS service endpoints"
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
resource "aws_vpc_security_group_ingress_rule" "migration_handler_to_database" {
|
|
2106
|
+
security_group_id = module.aurora.security_group_id
|
|
2107
|
+
referenced_security_group_id = aws_security_group.migration_handler.id
|
|
2108
|
+
from_port = module.aurora.cluster_port
|
|
2109
|
+
to_port = module.aurora.cluster_port
|
|
2110
|
+
ip_protocol = "tcp"
|
|
2111
|
+
description = "Allow inbound traffic from migration Lambda on database port"
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
resource "aws_vpc_security_group_ingress_rule" "create_db_user_to_database" {
|
|
2115
|
+
security_group_id = module.aurora.database_security_group_id
|
|
2116
|
+
referenced_security_group_id = aws_security_group.create_db_user.id
|
|
2117
|
+
from_port = module.aurora.cluster_port
|
|
2118
|
+
to_port = module.aurora.cluster_port
|
|
2119
|
+
ip_protocol = "tcp"
|
|
2120
|
+
description = "Allow inbound traffic from create-db-user Lambda on database port"
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
resource "aws_lambda_function" "create_db_user" {
|
|
2124
|
+
#checkov:skip=CKV_AWS_116:Dead Letter Queue not required for the create-db-user handler
|
|
2125
|
+
#checkov:skip=CKV_AWS_115:Concurrent execution limit not required for the create-db-user handler
|
|
2126
|
+
#checkov:skip=CKV_AWS_173:Lambda environment variables encrypted by managed key
|
|
2127
|
+
#checkov:skip=CKV_AWS_272:Code signing not configured as deployment packages are built and deployed within the same pipeline
|
|
2128
|
+
s3_bucket = aws_s3_object.create_db_user_zip.bucket
|
|
2129
|
+
s3_key = aws_s3_object.create_db_user_zip.key
|
|
2130
|
+
s3_object_version = aws_s3_object.create_db_user_zip.version_id
|
|
2131
|
+
function_name = local.create_db_user_function_name
|
|
2132
|
+
role = aws_iam_role.create_db_user.arn
|
|
2133
|
+
handler = "index.handler"
|
|
2134
|
+
source_code_hash = data.archive_file.create_db_user_zip.output_base64sha256
|
|
2135
|
+
runtime = "nodejs24.x"
|
|
2136
|
+
timeout = 300
|
|
2137
|
+
architectures = ["arm64"]
|
|
2138
|
+
|
|
2139
|
+
tracing_config {
|
|
2140
|
+
mode = "Active"
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
vpc_config {
|
|
2144
|
+
subnet_ids = var.lambda_subnet_ids
|
|
2145
|
+
security_group_ids = [aws_security_group.create_db_user.id]
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
environment {
|
|
2149
|
+
variables = {
|
|
2150
|
+
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
2151
|
+
NODE_EXTRA_CA_CERTS = "/var/runtime/ca-cert.pem"
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
tags = var.tags
|
|
2156
|
+
|
|
2157
|
+
depends_on = [
|
|
2158
|
+
aws_s3_object.create_db_user_zip,
|
|
2159
|
+
aws_iam_role_policy_attachment.create_db_user_basic_execution,
|
|
2160
|
+
aws_iam_role_policy_attachment.create_db_user_vpc_access,
|
|
2161
|
+
aws_iam_role_policy_attachment.create_db_user_xray,
|
|
2162
|
+
aws_iam_role_policy.create_db_user_secret_access,
|
|
2163
|
+
aws_cloudwatch_log_group.create_db_user,
|
|
2164
|
+
aws_vpc_security_group_ingress_rule.create_db_user_to_database
|
|
2165
|
+
]
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
resource "null_resource" "create_db_user_trigger" {
|
|
2169
|
+
triggers = {
|
|
2170
|
+
cluster_arn = module.aurora.cluster_arn
|
|
2171
|
+
function_name = aws_lambda_function.create_db_user.function_name
|
|
2172
|
+
db_user = local.database_runtime_user
|
|
2173
|
+
bundle_hash = data.archive_file.create_db_user_zip.output_base64sha256
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
provisioner "local-exec" {
|
|
2177
|
+
command = <<-EOT
|
|
2178
|
+
output_file=$(mktemp)
|
|
2179
|
+
response=$(aws lambda invoke \\
|
|
2180
|
+
--region \${data.aws_region.current.region} \\
|
|
2181
|
+
--function-name \${self.triggers.function_name} \\
|
|
2182
|
+
--cli-binary-format raw-in-base64-out \\
|
|
2183
|
+
--payload '{"RequestType":"Create","PhysicalResourceId":"db-user:\${self.triggers.db_user}"}' \\
|
|
2184
|
+
"$output_file")
|
|
2185
|
+
cat "$output_file"
|
|
2186
|
+
echo "$response"
|
|
2187
|
+
if echo "$response" | grep -q '"FunctionError"'; then
|
|
2188
|
+
rm -f "$output_file"
|
|
2189
|
+
exit 1
|
|
2190
|
+
fi
|
|
2191
|
+
rm -f "$output_file"
|
|
2192
|
+
EOT
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
depends_on = [aws_lambda_function.create_db_user, module.aurora]
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
resource "aws_lambda_function" "migration_handler" {
|
|
2199
|
+
#checkov:skip=CKV_AWS_116:Dead Letter Queue not required for the migration handler
|
|
2200
|
+
#checkov:skip=CKV_AWS_115:Concurrent execution limit not required for the migration handler
|
|
2201
|
+
#checkov:skip=CKV_AWS_173:Lambda environment variables encrypted by managed key
|
|
2202
|
+
#checkov:skip=CKV_AWS_272:Code signing does not apply to container image Lambda functions
|
|
2203
|
+
package_type = "Image"
|
|
2204
|
+
function_name = local.migration_function_name
|
|
2205
|
+
role = aws_iam_role.migration_handler.arn
|
|
2206
|
+
image_uri = "\${aws_ecr_repository.migration_handler.repository_url}:latest"
|
|
2207
|
+
memory_size = 1024
|
|
2208
|
+
timeout = 300
|
|
2209
|
+
architectures = ["arm64"]
|
|
2210
|
+
|
|
2211
|
+
tracing_config {
|
|
2212
|
+
mode = "Active"
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
vpc_config {
|
|
2216
|
+
subnet_ids = var.lambda_subnet_ids
|
|
2217
|
+
security_group_ids = [aws_security_group.migration_handler.id]
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
environment {
|
|
2221
|
+
variables = {
|
|
2222
|
+
HOSTNAME = module.aurora.cluster_endpoint
|
|
2223
|
+
DATABASE = module.aurora.database_name
|
|
2224
|
+
PORT = tostring(module.aurora.cluster_port)
|
|
2225
|
+
DBUSER = local.database_runtime_user
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
tags = var.tags
|
|
2230
|
+
|
|
2231
|
+
depends_on = [
|
|
2232
|
+
null_resource.docker_publish,
|
|
2233
|
+
aws_iam_role_policy_attachment.migration_handler_basic_execution,
|
|
2234
|
+
aws_iam_role_policy_attachment.migration_handler_vpc_access,
|
|
2235
|
+
aws_iam_role_policy_attachment.migration_handler_xray,
|
|
2236
|
+
aws_iam_role_policy.migration_handler_access,
|
|
2237
|
+
aws_cloudwatch_log_group.migration_handler,
|
|
2238
|
+
aws_vpc_security_group_ingress_rule.migration_handler_to_database
|
|
2239
|
+
]
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
resource "null_resource" "migration_trigger" {
|
|
2243
|
+
triggers = {
|
|
2244
|
+
docker_digest = data.external.docker_digest.result.digest
|
|
2245
|
+
cluster_arn = module.aurora.cluster_arn
|
|
2246
|
+
database_ready = module.aurora.database_ready
|
|
2247
|
+
function_name = aws_lambda_function.migration_handler.function_name
|
|
2248
|
+
db_user = local.database_runtime_user
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
provisioner "local-exec" {
|
|
2252
|
+
command = <<-EOT
|
|
2253
|
+
output_file=$(mktemp)
|
|
2254
|
+
response=$(aws lambda invoke \\
|
|
2255
|
+
--region \${data.aws_region.current.region} \\
|
|
2256
|
+
--function-name \${self.triggers.function_name} \\
|
|
2257
|
+
--cli-binary-format raw-in-base64-out \\
|
|
2258
|
+
"$output_file")
|
|
2259
|
+
cat "$output_file"
|
|
2260
|
+
echo "$response"
|
|
2261
|
+
if echo "$response" | grep -q '"FunctionError"'; then
|
|
2262
|
+
rm -f "$output_file"
|
|
2263
|
+
exit 1
|
|
2264
|
+
fi
|
|
2265
|
+
rm -f "$output_file"
|
|
2266
|
+
EOT
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
depends_on = [
|
|
2270
|
+
aws_lambda_function.migration_handler,
|
|
2271
|
+
module.aurora,
|
|
2272
|
+
null_resource.create_db_user_trigger,
|
|
2273
|
+
aws_iam_role_policy.proxy_db_user_connect
|
|
2274
|
+
]
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
output "security_group_id" {
|
|
2278
|
+
description = "Security group ID to allow inbound connections to the database (proxy SG if RDS Proxy is enabled, otherwise DB SG)."
|
|
2279
|
+
value = module.aurora.security_group_id
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
output "database_security_group_id" {
|
|
2283
|
+
description = "Security group ID of the Aurora cluster directly, used for direct DB access rules (e.g. when RDS Proxy is disabled)."
|
|
2284
|
+
value = module.aurora.database_security_group_id
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
output "cluster_resource_id" {
|
|
2288
|
+
description = "Resource ID of the Aurora cluster, used for IAM rds-db:connect policies."
|
|
2289
|
+
value = module.aurora.cluster_resource_id
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
output "proxy_resource_id" {
|
|
2293
|
+
description = "Resource identifier of the RDS Proxy (prx-XXXX), used for IAM rds-db:connect policies when connecting through the proxy. Null if RDS Proxy is disabled."
|
|
2294
|
+
value = module.aurora.proxy_resource_id
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
output "kms_key_arn" {
|
|
2298
|
+
description = "ARN of the KMS key protecting Aurora and the admin credentials secret, used to grant decrypt access alongside secret_arn."
|
|
2299
|
+
value = module.aurora.kms_key_arn
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
output "cluster_arn" {
|
|
2303
|
+
description = "ARN of the Aurora cluster."
|
|
2304
|
+
value = module.aurora.cluster_arn
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
output "cluster_endpoint" {
|
|
2308
|
+
description = "Writer endpoint of the Aurora cluster."
|
|
2309
|
+
value = module.aurora.cluster_endpoint
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
output "reader_endpoint" {
|
|
2313
|
+
description = "Reader endpoint of the Aurora cluster."
|
|
2314
|
+
value = module.aurora.reader_endpoint
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
output "cluster_port" {
|
|
2318
|
+
description = "Port exposed by the Aurora cluster."
|
|
2319
|
+
value = module.aurora.cluster_port
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
output "secret_arn" {
|
|
2323
|
+
description = "ARN of the generated admin credentials secret."
|
|
2324
|
+
value = module.aurora.secret_arn
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
output "database_runtime_user" {
|
|
2328
|
+
description = "Application database user created for IAM-authenticated access."
|
|
2329
|
+
value = local.database_runtime_user
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
"
|
|
2333
|
+
`;
|
|
2334
|
+
|
|
2335
|
+
exports[`ts#rdb generator > should generate terraform modules with MySQL engine 1`] = `
|
|
2336
|
+
"# Core Aurora module
|
|
2337
|
+
# This module creates an Aurora cluster and a generated admin secret.
|
|
2338
|
+
|
|
2339
|
+
terraform {
|
|
2340
|
+
required_version = ">= 1.0"
|
|
2341
|
+
|
|
2342
|
+
required_providers {
|
|
2343
|
+
aws = {
|
|
2344
|
+
source = "hashicorp/aws"
|
|
2345
|
+
version = "~> 6.33"
|
|
2346
|
+
}
|
|
2347
|
+
null = {
|
|
2348
|
+
source = "hashicorp/null"
|
|
2349
|
+
version = ">= 3.0"
|
|
2350
|
+
}
|
|
2351
|
+
random = {
|
|
2352
|
+
source = "hashicorp/random"
|
|
2353
|
+
version = ">= 3.0"
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
data "aws_caller_identity" "current" {}
|
|
2359
|
+
data "aws_partition" "current" {}
|
|
2360
|
+
data "aws_region" "current" {}
|
|
2361
|
+
|
|
2362
|
+
data "aws_iam_policy" "enhanced_monitoring" {
|
|
2363
|
+
arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
variable "name" {
|
|
2367
|
+
description = "Base name applied to Aurora resources."
|
|
2368
|
+
type = string
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
variable "vpc_id" {
|
|
2372
|
+
description = "VPC where the Aurora cluster will be deployed."
|
|
2373
|
+
type = string
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
variable "subnet_ids" {
|
|
2377
|
+
description = "Subnet IDs for the Aurora DB subnet group and RDS Proxy. These subnets do not need outbound egress — private isolated subnets are fine."
|
|
2378
|
+
type = list(string)
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
variable "lambda_subnet_ids" {
|
|
2382
|
+
description = "Subnet IDs for the credential-rotation Lambda. Must have outbound egress to AWS service endpoints (e.g. Secrets Manager) — use private subnets with a NAT gateway."
|
|
2383
|
+
type = list(string)
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
variable "engine" {
|
|
2387
|
+
description = "Aurora engine to use."
|
|
2388
|
+
type = string
|
|
2389
|
+
default = "aurora-postgresql"
|
|
2390
|
+
|
|
2391
|
+
validation {
|
|
2392
|
+
condition = contains(["aurora-postgresql", "aurora-mysql"], var.engine)
|
|
2393
|
+
error_message = "engine must be aurora-postgresql or aurora-mysql."
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
variable "engine_version" {
|
|
2398
|
+
description = "Aurora engine version."
|
|
2399
|
+
type = string
|
|
2400
|
+
default = null
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
variable "database_name" {
|
|
2404
|
+
description = "Initial database created in the cluster."
|
|
2405
|
+
type = string
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
variable "admin_user" {
|
|
2409
|
+
description = "Admin username stored in the generated Secrets Manager secret."
|
|
2410
|
+
type = string
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
variable "port" {
|
|
2414
|
+
description = "Database port for the selected Aurora engine."
|
|
2415
|
+
type = number
|
|
2416
|
+
default = null
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
variable "serverless_min_capacity" {
|
|
2420
|
+
description = "Minimum Aurora Serverless v2 ACUs."
|
|
2421
|
+
type = number
|
|
2422
|
+
default = 0.5
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
variable "serverless_max_capacity" {
|
|
2426
|
+
description = "Maximum Aurora Serverless v2 ACUs."
|
|
2427
|
+
type = number
|
|
2428
|
+
default = 4
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
variable "instance_count" {
|
|
2432
|
+
description = "Number of Aurora instances to create."
|
|
2433
|
+
type = number
|
|
2434
|
+
default = 1
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
variable "deletion_protection" {
|
|
2438
|
+
description = "Whether deletion protection is enabled."
|
|
2439
|
+
type = bool
|
|
2440
|
+
default = true
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
variable "skip_final_snapshot" {
|
|
2444
|
+
description = "Whether to skip the final snapshot on deletion."
|
|
2445
|
+
type = bool
|
|
2446
|
+
default = false
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
variable "enable_rds_proxy" {
|
|
2450
|
+
description = "Whether to provision an RDS Proxy in front of the Aurora cluster."
|
|
2451
|
+
type = bool
|
|
2452
|
+
default = true
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
variable "enable_credential_rotation" {
|
|
2456
|
+
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
2457
|
+
type = bool
|
|
2458
|
+
default = true
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
variable "enable_cloudwatch_logs" {
|
|
2462
|
+
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL also enables verbose statement logging."
|
|
2463
|
+
type = bool
|
|
2464
|
+
default = false
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
variable "enable_performance_insights" {
|
|
2468
|
+
description = "Whether to enable Performance Insights on Aurora cluster instances."
|
|
2469
|
+
type = bool
|
|
2470
|
+
default = true
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
variable "performance_insights_retention_period" {
|
|
2474
|
+
description = "Retention period, in days, for Performance Insights data when enabled."
|
|
2475
|
+
type = number
|
|
2476
|
+
default = 7
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
variable "enable_backup" {
|
|
2480
|
+
description = "Whether to provision an AWS Backup plan for the Aurora cluster."
|
|
2481
|
+
type = bool
|
|
2482
|
+
default = false
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
variable "enable_key_rotation" {
|
|
2486
|
+
description = "Whether to enable automatic key rotation on the KMS key used to encrypt the Aurora cluster and its credentials secret."
|
|
2487
|
+
type = bool
|
|
2488
|
+
default = true
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
variable "tags" {
|
|
2492
|
+
description = "Tags to apply to all resources."
|
|
2493
|
+
type = map(string)
|
|
2494
|
+
default = {}
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
locals {
|
|
2498
|
+
default_port = var.engine == "aurora-mysql" ? 3306 : 5432
|
|
2499
|
+
default_engine_version = var.engine == "aurora-mysql" ? "8.0.mysql_aurora.3.12.0" : "17.7"
|
|
2500
|
+
parameter_group_family = var.engine == "aurora-mysql" ? "aurora-mysql8.0" : "aurora-postgresql\${split(".", coalesce(var.engine_version, local.default_engine_version))[0]}"
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
resource "aws_rds_cluster_parameter_group" "database" {
|
|
2504
|
+
count = var.enable_cloudwatch_logs ? 1 : 0
|
|
2505
|
+
|
|
2506
|
+
name_prefix = "\${var.name}-"
|
|
2507
|
+
family = local.parameter_group_family
|
|
2508
|
+
description = "Parameter group for \${var.name} Aurora cluster"
|
|
2509
|
+
|
|
2510
|
+
dynamic "parameter" {
|
|
2511
|
+
for_each = var.engine == "aurora-postgresql" ? [1] : []
|
|
2512
|
+
content {
|
|
2513
|
+
name = "log_statement"
|
|
2514
|
+
value = "all"
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
tags = var.tags
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
resource "aws_kms_key" "database" {
|
|
2522
|
+
description = "KMS key for Aurora cluster \${var.name}"
|
|
2523
|
+
enable_key_rotation = var.enable_key_rotation
|
|
2524
|
+
|
|
2525
|
+
policy = jsonencode({
|
|
2526
|
+
Version = "2012-10-17"
|
|
2527
|
+
Statement = [
|
|
2528
|
+
{
|
|
2529
|
+
Sid = "EnableRootAccess"
|
|
2530
|
+
Effect = "Allow"
|
|
2531
|
+
Principal = {
|
|
2532
|
+
AWS = "arn:\${data.aws_partition.current.partition}:iam::\${data.aws_caller_identity.current.account_id}:root"
|
|
2533
|
+
}
|
|
2534
|
+
Action = "kms:*"
|
|
2535
|
+
Resource = "*"
|
|
2536
|
+
},
|
|
2537
|
+
{
|
|
2538
|
+
Sid = "AllowRDSService"
|
|
2539
|
+
Effect = "Allow"
|
|
2540
|
+
Principal = {
|
|
2541
|
+
Service = "rds.amazonaws.com"
|
|
2542
|
+
}
|
|
2543
|
+
Action = [
|
|
2544
|
+
"kms:Encrypt",
|
|
2545
|
+
"kms:Decrypt",
|
|
2546
|
+
"kms:ReEncrypt*",
|
|
2547
|
+
"kms:GenerateDataKey*",
|
|
2548
|
+
"kms:DescribeKey",
|
|
2549
|
+
"kms:CreateGrant"
|
|
2550
|
+
]
|
|
2551
|
+
Resource = "*"
|
|
2552
|
+
}
|
|
2553
|
+
]
|
|
2554
|
+
})
|
|
2555
|
+
|
|
2556
|
+
tags = merge(var.tags, {
|
|
2557
|
+
Name = "\${var.name}-aurora"
|
|
2558
|
+
})
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
resource "aws_iam_role" "enhanced_monitoring" {
|
|
2562
|
+
name_prefix = "\${var.name}-aurora-monitoring-"
|
|
2563
|
+
|
|
2564
|
+
assume_role_policy = jsonencode({
|
|
2565
|
+
Version = "2012-10-17"
|
|
2566
|
+
Statement = [
|
|
2567
|
+
{
|
|
2568
|
+
Effect = "Allow"
|
|
2569
|
+
Principal = {
|
|
2570
|
+
Service = "monitoring.rds.amazonaws.com"
|
|
2571
|
+
}
|
|
2572
|
+
Action = "sts:AssumeRole"
|
|
2573
|
+
}
|
|
2574
|
+
]
|
|
2575
|
+
})
|
|
2576
|
+
|
|
2577
|
+
tags = merge(var.tags, {
|
|
2578
|
+
Name = "\${var.name}-aurora-monitoring"
|
|
2579
|
+
})
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
resource "aws_iam_role_policy_attachment" "enhanced_monitoring" {
|
|
2583
|
+
role = aws_iam_role.enhanced_monitoring.name
|
|
2584
|
+
policy_arn = data.aws_iam_policy.enhanced_monitoring.arn
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
resource "aws_security_group" "database" {
|
|
2588
|
+
name_prefix = "\${var.name}-aurora-"
|
|
2589
|
+
description = "Security group for Aurora cluster \${var.name}"
|
|
2590
|
+
vpc_id = var.vpc_id
|
|
2591
|
+
|
|
2592
|
+
tags = merge(var.tags, {
|
|
2593
|
+
Name = "\${var.name}-aurora"
|
|
2594
|
+
})
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
resource "aws_db_subnet_group" "database" {
|
|
2598
|
+
name = "\${var.name}-aurora"
|
|
2599
|
+
subnet_ids = var.subnet_ids
|
|
2600
|
+
|
|
2601
|
+
tags = merge(var.tags, {
|
|
2602
|
+
Name = "\${var.name}-aurora"
|
|
2603
|
+
})
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
resource "random_password" "master_password" {
|
|
2607
|
+
length = 32
|
|
2608
|
+
special = true
|
|
2609
|
+
override_special = "!#$%&*()-_=+[]{}<>:?"
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
resource "aws_secretsmanager_secret" "credentials" {
|
|
2613
|
+
#checkov:skip=CKV2_AWS_57:Rotation is configured via aws_cloudformation_stack.credentials_rotation using the AWS::SecretsManager-2024-09-16 transform; Checkov cannot resolve rotation schedules created by CloudFormation transforms
|
|
2614
|
+
name_prefix = "\${var.name}-aurora-credentials-"
|
|
2615
|
+
kms_key_id = aws_kms_key.database.arn
|
|
2616
|
+
|
|
2617
|
+
tags = merge(var.tags, {
|
|
2618
|
+
Name = "\${var.name}-aurora-credentials"
|
|
2619
|
+
})
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
resource "aws_secretsmanager_secret_version" "credentials" {
|
|
2623
|
+
secret_id = aws_secretsmanager_secret.credentials.id
|
|
2624
|
+
secret_string = jsonencode({
|
|
2625
|
+
engine = var.engine == "aurora-mysql" ? "mysql" : "postgres"
|
|
2626
|
+
username = var.admin_user
|
|
2627
|
+
password = random_password.master_password.result
|
|
2628
|
+
host = aws_rds_cluster.database.endpoint
|
|
2629
|
+
port = aws_rds_cluster.database.port
|
|
2630
|
+
dbname = var.database_name
|
|
2631
|
+
dbClusterIdentifier = aws_rds_cluster.database.cluster_identifier
|
|
2632
|
+
})
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
resource "aws_rds_cluster" "database" {
|
|
2636
|
+
#checkov:skip=CKV2_AWS_27:Query logging can be enabled with enable_cloudwatch_logs; this module defaults to CDK-equivalent behavior unless logging is requested
|
|
2637
|
+
#checkov:skip=CKV_AWS_139:Deletion protection is enabled but checkov is unable to detect it correctly
|
|
2638
|
+
cluster_identifier = "\${var.name}-aurora"
|
|
2639
|
+
engine = var.engine
|
|
2640
|
+
engine_version = coalesce(var.engine_version, local.default_engine_version)
|
|
2641
|
+
database_name = var.database_name
|
|
2642
|
+
master_username = var.admin_user
|
|
2643
|
+
master_password = random_password.master_password.result
|
|
2644
|
+
db_subnet_group_name = aws_db_subnet_group.database.name
|
|
2645
|
+
db_cluster_parameter_group_name = var.enable_cloudwatch_logs ? aws_rds_cluster_parameter_group.database[0].name : null
|
|
2646
|
+
vpc_security_group_ids = [aws_security_group.database.id]
|
|
2647
|
+
port = coalesce(var.port, local.default_port)
|
|
2648
|
+
storage_encrypted = true
|
|
2649
|
+
kms_key_id = aws_kms_key.database.arn
|
|
2650
|
+
deletion_protection = var.deletion_protection
|
|
2651
|
+
skip_final_snapshot = var.skip_final_snapshot
|
|
2652
|
+
final_snapshot_identifier = var.skip_final_snapshot ? null : "\${var.name}-aurora-final-snapshot"
|
|
2653
|
+
iam_database_authentication_enabled = true
|
|
2654
|
+
monitoring_interval = 5
|
|
2655
|
+
monitoring_role_arn = aws_iam_role.enhanced_monitoring.arn
|
|
2656
|
+
copy_tags_to_snapshot = true
|
|
2657
|
+
enabled_cloudwatch_logs_exports = var.enable_cloudwatch_logs ? (var.engine == "aurora-mysql" ? ["audit", "error", "general", "slowquery"] : ["postgresql"]) : null
|
|
2658
|
+
|
|
2659
|
+
serverlessv2_scaling_configuration {
|
|
2660
|
+
min_capacity = var.serverless_min_capacity
|
|
2661
|
+
max_capacity = var.serverless_max_capacity
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
tags = merge(var.tags, {
|
|
2665
|
+
Name = "\${var.name}-aurora"
|
|
2666
|
+
})
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
resource "aws_rds_cluster_instance" "database" {
|
|
2670
|
+
count = var.instance_count
|
|
2671
|
+
|
|
2672
|
+
identifier = "\${var.name}-aurora-\${count.index + 1}"
|
|
2673
|
+
cluster_identifier = aws_rds_cluster.database.id
|
|
2674
|
+
instance_class = "db.serverless"
|
|
2675
|
+
engine = aws_rds_cluster.database.engine
|
|
2676
|
+
engine_version = aws_rds_cluster.database.engine_version
|
|
2677
|
+
db_subnet_group_name = aws_db_subnet_group.database.name
|
|
2678
|
+
|
|
2679
|
+
auto_minor_version_upgrade = true
|
|
2680
|
+
monitoring_interval = 5
|
|
2681
|
+
monitoring_role_arn = aws_iam_role.enhanced_monitoring.arn
|
|
2682
|
+
performance_insights_enabled = var.enable_performance_insights
|
|
2683
|
+
performance_insights_kms_key_id = var.enable_performance_insights ? aws_kms_key.database.arn : null
|
|
2684
|
+
performance_insights_retention_period = var.enable_performance_insights ? var.performance_insights_retention_period : null
|
|
2685
|
+
|
|
2686
|
+
tags = merge(var.tags, {
|
|
2687
|
+
Name = "\${var.name}-aurora-\${count.index + 1}"
|
|
2688
|
+
})
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
resource "aws_security_group" "rotation" {
|
|
2692
|
+
#checkov:skip=CKV2_AWS_5:Security group is attached to the hosted rotation Lambda created by AWS::SecretsManager::RotationSchedule; Checkov cannot resolve resources created by the CloudFormation transform
|
|
2693
|
+
count = var.enable_credential_rotation ? 1 : 0
|
|
2694
|
+
name_prefix = "\${var.name}-aurora-rotation-"
|
|
2695
|
+
description = "Security group for Aurora secret rotation \${var.name}"
|
|
2696
|
+
vpc_id = var.vpc_id
|
|
2697
|
+
|
|
2698
|
+
tags = merge(var.tags, {
|
|
2699
|
+
Name = "\${var.name}-aurora-rotation"
|
|
2700
|
+
})
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
resource "aws_vpc_security_group_egress_rule" "rotation_to_database" {
|
|
2704
|
+
count = var.enable_credential_rotation ? 1 : 0
|
|
2705
|
+
security_group_id = aws_security_group.rotation[0].id
|
|
2706
|
+
referenced_security_group_id = aws_security_group.database.id
|
|
2707
|
+
from_port = coalesce(var.port, local.default_port)
|
|
2708
|
+
to_port = coalesce(var.port, local.default_port)
|
|
2709
|
+
ip_protocol = "tcp"
|
|
2710
|
+
description = "Allow outbound database traffic from secret rotation Lambda"
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
resource "aws_vpc_security_group_ingress_rule" "rotation_to_database" {
|
|
2714
|
+
count = var.enable_credential_rotation ? 1 : 0
|
|
2715
|
+
security_group_id = aws_security_group.database.id
|
|
2716
|
+
referenced_security_group_id = aws_security_group.rotation[0].id
|
|
2717
|
+
from_port = coalesce(var.port, local.default_port)
|
|
2718
|
+
to_port = coalesce(var.port, local.default_port)
|
|
2719
|
+
ip_protocol = "tcp"
|
|
2720
|
+
description = "Allow inbound database traffic from secret rotation Lambda"
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
resource "aws_cloudformation_stack" "credentials_rotation" {
|
|
2724
|
+
#checkov:skip=CKV_AWS_124:SNS event notifications are not required for the credentials rotation stack
|
|
2725
|
+
count = var.enable_credential_rotation ? 1 : 0
|
|
2726
|
+
name = "\${var.name}-aurora-credentials-rotation"
|
|
2727
|
+
|
|
2728
|
+
capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"]
|
|
2729
|
+
|
|
2730
|
+
template_body = jsonencode({
|
|
2731
|
+
AWSTemplateFormatVersion = "2010-09-09"
|
|
2732
|
+
Transform = "AWS::SecretsManager-2024-09-16"
|
|
2733
|
+
Resources = {
|
|
2734
|
+
CredentialsRotationSchedule = {
|
|
2735
|
+
Type = "AWS::SecretsManager::RotationSchedule"
|
|
2736
|
+
Properties = {
|
|
2737
|
+
SecretId = aws_secretsmanager_secret.credentials.arn
|
|
2738
|
+
HostedRotationLambda = {
|
|
2739
|
+
RotationType = var.engine == "aurora-mysql" ? "MySQLSingleUser" : "PostgreSQLSingleUser"
|
|
2740
|
+
RotationLambdaName = "\${var.name}-aurora-credentials-rotation"
|
|
2741
|
+
VpcSecurityGroupIds = aws_security_group.rotation[0].id
|
|
2742
|
+
VpcSubnetIds = join(",", var.lambda_subnet_ids)
|
|
2743
|
+
}
|
|
2744
|
+
RotationRules = {
|
|
2745
|
+
AutomaticallyAfterDays = 30
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
})
|
|
2751
|
+
|
|
2752
|
+
depends_on = [
|
|
2753
|
+
aws_secretsmanager_secret_version.credentials,
|
|
2754
|
+
aws_rds_cluster_instance.database,
|
|
2755
|
+
aws_vpc_security_group_egress_rule.rotation_to_database,
|
|
2756
|
+
aws_vpc_security_group_ingress_rule.rotation_to_database,
|
|
2757
|
+
]
|
|
2758
|
+
}
|
|
2759
|
+
|
|
2760
|
+
resource "aws_iam_role" "proxy" {
|
|
2761
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
2762
|
+
|
|
2763
|
+
name_prefix = "\${var.name}-aurora-proxy-"
|
|
2764
|
+
|
|
2765
|
+
assume_role_policy = jsonencode({
|
|
2766
|
+
Version = "2012-10-17"
|
|
2767
|
+
Statement = [
|
|
2768
|
+
{
|
|
2769
|
+
Effect = "Allow"
|
|
2770
|
+
Principal = {
|
|
2771
|
+
Service = "rds.amazonaws.com"
|
|
2772
|
+
}
|
|
2773
|
+
Action = "sts:AssumeRole"
|
|
2774
|
+
}
|
|
2775
|
+
]
|
|
2776
|
+
})
|
|
2777
|
+
|
|
2778
|
+
tags = merge(var.tags, {
|
|
2779
|
+
Name = "\${var.name}-aurora-proxy"
|
|
2780
|
+
})
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
resource "aws_iam_role_policy" "proxy_secret_access" {
|
|
2784
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
2785
|
+
role = aws_iam_role.proxy[0].name
|
|
2786
|
+
|
|
2787
|
+
policy = jsonencode({
|
|
2788
|
+
Version = "2012-10-17"
|
|
2789
|
+
Statement = [
|
|
2790
|
+
{
|
|
2791
|
+
Effect = "Allow"
|
|
2792
|
+
Action = ["secretsmanager:GetSecretValue"]
|
|
2793
|
+
Resource = [aws_secretsmanager_secret.credentials.arn]
|
|
2794
|
+
},
|
|
2795
|
+
{
|
|
2796
|
+
Effect = "Allow"
|
|
2797
|
+
Action = ["kms:Decrypt"]
|
|
2798
|
+
Resource = [aws_kms_key.database.arn]
|
|
2799
|
+
}
|
|
2800
|
+
]
|
|
2801
|
+
})
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
resource "aws_security_group" "proxy" {
|
|
2805
|
+
#checkov:skip=CKV2_AWS_5:Security group is attached to aws_db_proxy.aurora; Checkov cannot resolve count-conditional cross-resource references
|
|
2806
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
2807
|
+
|
|
2808
|
+
name_prefix = "\${var.name}-aurora-proxy-"
|
|
2809
|
+
description = "Security group for Aurora proxy \${var.name}"
|
|
2810
|
+
vpc_id = var.vpc_id
|
|
2811
|
+
|
|
2812
|
+
tags = merge(var.tags, {
|
|
2813
|
+
Name = "\${var.name}-aurora-proxy"
|
|
2814
|
+
})
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
resource "aws_db_proxy" "aurora" {
|
|
2818
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
2819
|
+
|
|
2820
|
+
name = "\${var.name}-proxy"
|
|
2821
|
+
default_auth_scheme = "IAM_AUTH"
|
|
2822
|
+
engine_family = var.engine == "aurora-mysql" ? "MYSQL" : "POSTGRESQL"
|
|
2823
|
+
role_arn = aws_iam_role.proxy[0].arn
|
|
2824
|
+
vpc_subnet_ids = var.subnet_ids
|
|
2825
|
+
vpc_security_group_ids = [aws_security_group.proxy[0].id]
|
|
2826
|
+
require_tls = true
|
|
2827
|
+
|
|
2828
|
+
auth {
|
|
2829
|
+
auth_scheme = "SECRETS"
|
|
2830
|
+
iam_auth = "REQUIRED"
|
|
2831
|
+
secret_arn = aws_secretsmanager_secret.credentials.arn
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
depends_on = [
|
|
2835
|
+
aws_iam_role_policy.proxy_secret_access,
|
|
2836
|
+
aws_secretsmanager_secret_version.credentials,
|
|
2837
|
+
]
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
resource "aws_db_proxy_default_target_group" "default" {
|
|
2841
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
2842
|
+
|
|
2843
|
+
db_proxy_name = aws_db_proxy.aurora[0].name
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
resource "aws_db_proxy_target" "default" {
|
|
2847
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
2848
|
+
|
|
2849
|
+
db_proxy_name = aws_db_proxy.aurora[0].name
|
|
2850
|
+
target_group_name = aws_db_proxy_default_target_group.default[0].name
|
|
2851
|
+
db_cluster_identifier = aws_rds_cluster.database.cluster_identifier
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
resource "null_resource" "proxy_target_ready" {
|
|
2855
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
2856
|
+
|
|
2857
|
+
triggers = {
|
|
2858
|
+
db_proxy_name = aws_db_proxy.aurora[0].name
|
|
2859
|
+
target_group_name = aws_db_proxy_default_target_group.default[0].name
|
|
2860
|
+
db_cluster_identifier = aws_rds_cluster.database.cluster_identifier
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
provisioner "local-exec" {
|
|
2864
|
+
command = <<-EOT
|
|
2865
|
+
start_time=$(date +%s)
|
|
2866
|
+
deadline=$((start_time + 600))
|
|
2867
|
+
while true; do
|
|
2868
|
+
target_state=$(aws rds describe-db-proxy-targets \\
|
|
2869
|
+
--region \${data.aws_region.current.region} \\
|
|
2870
|
+
--db-proxy-name \${self.triggers.db_proxy_name} \\
|
|
2871
|
+
--target-group-name \${self.triggers.target_group_name} \\
|
|
2872
|
+
--query "Targets[?Type=='RDS_INSTANCE'] | [0].TargetHealth.State" \\
|
|
2873
|
+
--output text)
|
|
2874
|
+
|
|
2875
|
+
if [ "$target_state" = "AVAILABLE" ]; then
|
|
2876
|
+
exit 0
|
|
2877
|
+
fi
|
|
2878
|
+
|
|
2879
|
+
current_time=$(date +%s)
|
|
2880
|
+
if [ "$current_time" -ge "$deadline" ]; then
|
|
2881
|
+
echo "RDS Proxy target \${self.triggers.db_cluster_identifier} did not become AVAILABLE within 10 minutes. Last state: $target_state"
|
|
2882
|
+
exit 1
|
|
2883
|
+
fi
|
|
2884
|
+
|
|
2885
|
+
sleep 10
|
|
2886
|
+
done
|
|
2887
|
+
EOT
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
depends_on = [
|
|
2891
|
+
aws_db_proxy_target.default,
|
|
2892
|
+
aws_rds_cluster_instance.database,
|
|
2893
|
+
aws_vpc_security_group_ingress_rule.proxy_to_database,
|
|
2894
|
+
aws_vpc_security_group_egress_rule.proxy_to_database
|
|
2895
|
+
]
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
resource "null_resource" "cluster_ready" {
|
|
2899
|
+
count = var.enable_rds_proxy ? 0 : 1
|
|
2900
|
+
|
|
2901
|
+
triggers = {
|
|
2902
|
+
cluster_identifier = aws_rds_cluster.database.cluster_identifier
|
|
2903
|
+
instance_ids = join(",", aws_rds_cluster_instance.database[*].id)
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
depends_on = [aws_rds_cluster_instance.database]
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
resource "aws_vpc_security_group_ingress_rule" "proxy_to_database" {
|
|
2910
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
2911
|
+
|
|
2912
|
+
security_group_id = aws_security_group.database.id
|
|
2913
|
+
referenced_security_group_id = aws_security_group.proxy[0].id
|
|
2914
|
+
from_port = coalesce(var.port, local.default_port)
|
|
2915
|
+
to_port = coalesce(var.port, local.default_port)
|
|
2916
|
+
ip_protocol = "tcp"
|
|
2917
|
+
description = "Allow inbound database traffic from RDS Proxy"
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
resource "aws_vpc_security_group_egress_rule" "proxy_to_database" {
|
|
2921
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
2922
|
+
|
|
2923
|
+
security_group_id = aws_security_group.proxy[0].id
|
|
2924
|
+
referenced_security_group_id = aws_security_group.database.id
|
|
2925
|
+
from_port = coalesce(var.port, local.default_port)
|
|
2926
|
+
to_port = coalesce(var.port, local.default_port)
|
|
2927
|
+
ip_protocol = "tcp"
|
|
2928
|
+
description = "Allow outbound database traffic from RDS Proxy to Aurora cluster"
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
data "aws_iam_policy" "backup" {
|
|
2932
|
+
count = var.enable_backup ? 1 : 0
|
|
2933
|
+
|
|
2934
|
+
arn = "arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup"
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
resource "aws_iam_role" "backup" {
|
|
2938
|
+
count = var.enable_backup ? 1 : 0
|
|
2939
|
+
|
|
2940
|
+
name_prefix = "\${var.name}-backup-"
|
|
2941
|
+
|
|
2942
|
+
assume_role_policy = jsonencode({
|
|
2943
|
+
Version = "2012-10-17"
|
|
2944
|
+
Statement = [
|
|
2945
|
+
{
|
|
2946
|
+
Effect = "Allow"
|
|
2947
|
+
Principal = { Service = "backup.amazonaws.com" }
|
|
2948
|
+
Action = "sts:AssumeRole"
|
|
2949
|
+
}
|
|
2950
|
+
]
|
|
2951
|
+
})
|
|
2952
|
+
|
|
2953
|
+
tags = var.tags
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
resource "aws_iam_role_policy_attachment" "backup" {
|
|
2957
|
+
count = var.enable_backup ? 1 : 0
|
|
2958
|
+
|
|
2959
|
+
role = aws_iam_role.backup[0].name
|
|
2960
|
+
policy_arn = data.aws_iam_policy.backup[0].arn
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
resource "aws_backup_vault" "database" {
|
|
2964
|
+
count = var.enable_backup ? 1 : 0
|
|
2965
|
+
|
|
2966
|
+
name = "\${var.name}-aurora-backup"
|
|
2967
|
+
kms_key_arn = aws_kms_key.database.arn
|
|
2968
|
+
tags = var.tags
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
resource "aws_backup_plan" "database" {
|
|
2972
|
+
count = var.enable_backup ? 1 : 0
|
|
2973
|
+
|
|
2974
|
+
name = "\${var.name}-aurora-backup"
|
|
2975
|
+
|
|
2976
|
+
rule {
|
|
2977
|
+
rule_name = "daily-backup"
|
|
2978
|
+
target_vault_name = aws_backup_vault.database[0].name
|
|
2979
|
+
schedule = "cron(0 5 ? * * *)"
|
|
2980
|
+
|
|
2981
|
+
lifecycle {
|
|
2982
|
+
delete_after = 35
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
tags = var.tags
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
resource "aws_backup_selection" "database" {
|
|
2990
|
+
count = var.enable_backup ? 1 : 0
|
|
2991
|
+
|
|
2992
|
+
name = "\${var.name}-aurora"
|
|
2993
|
+
plan_id = aws_backup_plan.database[0].id
|
|
2994
|
+
iam_role_arn = aws_iam_role.backup[0].arn
|
|
2995
|
+
|
|
2996
|
+
resources = [aws_rds_cluster.database.arn]
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
output "cluster_arn" {
|
|
3000
|
+
description = "ARN of the Aurora cluster."
|
|
3001
|
+
value = aws_rds_cluster.database.arn
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
output "cluster_resource_id" {
|
|
3005
|
+
description = "Resource ID of the Aurora cluster used for IAM database authentication."
|
|
3006
|
+
value = aws_rds_cluster.database.cluster_resource_id
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
output "proxy_resource_id" {
|
|
3010
|
+
description = "Resource identifier of the RDS Proxy (prx-XXXX), used for IAM rds-db:connect policies when connecting through the proxy. Null if RDS Proxy is disabled."
|
|
3011
|
+
value = var.enable_rds_proxy ? split(":", aws_db_proxy.aurora[0].arn)[6] : null
|
|
3012
|
+
}
|
|
3013
|
+
|
|
3014
|
+
output "cluster_endpoint" {
|
|
3015
|
+
description = "Writer endpoint of the Aurora cluster or proxy."
|
|
3016
|
+
value = var.enable_rds_proxy ? aws_db_proxy.aurora[0].endpoint : aws_rds_cluster.database.endpoint
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
output "reader_endpoint" {
|
|
3020
|
+
description = "Reader endpoint of the Aurora cluster."
|
|
3021
|
+
value = aws_rds_cluster.database.reader_endpoint
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
output "cluster_port" {
|
|
3025
|
+
description = "Port exposed by the Aurora cluster."
|
|
3026
|
+
value = aws_rds_cluster.database.port
|
|
3027
|
+
}
|
|
3028
|
+
|
|
3029
|
+
output "security_group_id" {
|
|
3030
|
+
description = "Security group protecting the Aurora proxy or cluster."
|
|
3031
|
+
value = var.enable_rds_proxy ? aws_security_group.proxy[0].id : aws_security_group.database.id
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
output "database_security_group_id" {
|
|
3035
|
+
description = "Security group protecting the Aurora cluster directly."
|
|
3036
|
+
value = aws_security_group.database.id
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
output "proxy_role_name" {
|
|
3040
|
+
description = "Name of the IAM role used by the RDS Proxy, or null if proxy is disabled."
|
|
3041
|
+
value = var.enable_rds_proxy ? aws_iam_role.proxy[0].name : null
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
output "secret_arn" {
|
|
3045
|
+
description = "ARN of the generated admin credentials secret."
|
|
3046
|
+
value = aws_secretsmanager_secret.credentials.arn
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
output "kms_key_arn" {
|
|
3050
|
+
description = "ARN of the KMS key protecting Aurora and the generated secret."
|
|
3051
|
+
value = aws_kms_key.database.arn
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
output "database_host" {
|
|
3055
|
+
description = "Hostname of the Aurora cluster writer endpoint or proxy endpoint."
|
|
3056
|
+
value = var.enable_rds_proxy ? aws_db_proxy.aurora[0].endpoint : aws_rds_cluster.database.endpoint
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
output "database_ready" {
|
|
3060
|
+
description = "Dependency marker that is resolved after the selected database connection path is ready."
|
|
3061
|
+
value = true
|
|
3062
|
+
|
|
3063
|
+
depends_on = [
|
|
3064
|
+
null_resource.proxy_target_ready,
|
|
3065
|
+
null_resource.cluster_ready
|
|
3066
|
+
]
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
output "database_name" {
|
|
3070
|
+
description = "Initial database created in the cluster."
|
|
3071
|
+
value = var.database_name
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
output "admin_user" {
|
|
3075
|
+
description = "Admin username stored in the generated secret."
|
|
3076
|
+
value = jsondecode(aws_secretsmanager_secret_version.credentials.secret_string).username
|
|
3077
|
+
}
|
|
3078
|
+
"
|
|
3079
|
+
`;
|
|
3080
|
+
|
|
3081
|
+
exports[`ts#rdb generator > should generate terraform modules with MySQL engine 2`] = `
|
|
3082
|
+
"terraform {
|
|
3083
|
+
required_version = ">= 1.0"
|
|
3084
|
+
|
|
3085
|
+
required_providers {
|
|
3086
|
+
archive = {
|
|
3087
|
+
source = "hashicorp/archive"
|
|
3088
|
+
version = "~> 2.5"
|
|
3089
|
+
}
|
|
3090
|
+
aws = {
|
|
3091
|
+
source = "hashicorp/aws"
|
|
3092
|
+
version = "~> 6.33"
|
|
3093
|
+
}
|
|
3094
|
+
null = {
|
|
3095
|
+
source = "hashicorp/null"
|
|
3096
|
+
version = ">= 3.0"
|
|
3097
|
+
}
|
|
3098
|
+
random = {
|
|
3099
|
+
source = "hashicorp/random"
|
|
3100
|
+
version = ">= 3.0"
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
variable "vpc_id" {
|
|
3106
|
+
description = "VPC where the Aurora cluster will be deployed."
|
|
3107
|
+
type = string
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
variable "database_subnet_ids" {
|
|
3111
|
+
description = "Subnet IDs for the Aurora cluster and RDS Proxy. Private isolated subnets are fine — no outbound egress required."
|
|
3112
|
+
type = list(string)
|
|
3113
|
+
}
|
|
3114
|
+
|
|
3115
|
+
variable "lambda_subnet_ids" {
|
|
3116
|
+
description = "Subnet IDs for the create-db-user and migration Lambda functions. Must have outbound egress to AWS service endpoints (e.g. Secrets Manager) — use private subnets with a NAT gateway."
|
|
3117
|
+
type = list(string)
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
variable "engine_version" {
|
|
3121
|
+
description = "Aurora engine version."
|
|
3122
|
+
type = string
|
|
3123
|
+
default = null
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
variable "port" {
|
|
3127
|
+
description = "Database port for the selected Aurora engine."
|
|
3128
|
+
type = number
|
|
3129
|
+
default = null
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
variable "serverless_min_capacity" {
|
|
3133
|
+
description = "Minimum Aurora Serverless v2 ACUs."
|
|
3134
|
+
type = number
|
|
3135
|
+
default = 0.5
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
variable "serverless_max_capacity" {
|
|
3139
|
+
description = "Maximum Aurora Serverless v2 ACUs."
|
|
3140
|
+
type = number
|
|
3141
|
+
default = 4
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
variable "instance_count" {
|
|
3145
|
+
description = "Number of Aurora instances to create."
|
|
3146
|
+
type = number
|
|
3147
|
+
default = 1
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
variable "deletion_protection" {
|
|
3151
|
+
description = "Whether deletion protection is enabled."
|
|
3152
|
+
type = bool
|
|
3153
|
+
default = true
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
variable "skip_final_snapshot" {
|
|
3157
|
+
description = "Whether to skip the final snapshot on deletion."
|
|
3158
|
+
type = bool
|
|
3159
|
+
default = false
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
variable "enable_rds_proxy" {
|
|
3163
|
+
description = "Whether to provision an RDS Proxy in front of the Aurora cluster."
|
|
3164
|
+
type = bool
|
|
3165
|
+
default = true
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
variable "enable_credential_rotation" {
|
|
3169
|
+
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
3170
|
+
type = bool
|
|
3171
|
+
default = true
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
variable "enable_cloudwatch_logs" {
|
|
3175
|
+
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL also enables verbose statement logging."
|
|
3176
|
+
type = bool
|
|
3177
|
+
default = false
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
variable "enable_performance_insights" {
|
|
3181
|
+
description = "Whether to enable Performance Insights on Aurora cluster instances."
|
|
3182
|
+
type = bool
|
|
3183
|
+
default = true
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
variable "performance_insights_retention_period" {
|
|
3187
|
+
description = "Retention period, in days, for Performance Insights data when enabled."
|
|
3188
|
+
type = number
|
|
3189
|
+
default = 7
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
variable "enable_backup" {
|
|
3193
|
+
description = "Whether to provision an AWS Backup plan for the Aurora cluster."
|
|
3194
|
+
type = bool
|
|
3195
|
+
default = false
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
variable "tags" {
|
|
3199
|
+
description = "Tags to apply to all resources."
|
|
3200
|
+
type = map(string)
|
|
3201
|
+
default = {}
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
variable "docker_image_tag" {
|
|
3205
|
+
description = "Docker image tag for the migration handler. Defaults to the tag built by the Nx docker target."
|
|
3206
|
+
type = string
|
|
3207
|
+
default = "proj-db-migration:latest"
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
variable "asset_bucket_name" {
|
|
3211
|
+
description = "Name of the shared asset S3 bucket used to stage the Lambda deployment zip. Instantiate the \`core/asset-bucket\` module once per deployment and pass its \`bucket_name\` output here."
|
|
3212
|
+
type = string
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
data "aws_region" "current" {}
|
|
3216
|
+
data "aws_caller_identity" "current" {}
|
|
3217
|
+
|
|
3218
|
+
resource "random_string" "suffix" {
|
|
3219
|
+
length = 8
|
|
3220
|
+
special = false
|
|
3221
|
+
upper = false
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
locals {
|
|
3225
|
+
create_db_user_bundle_path = "\${path.module}/../../../../../../../dist/packages/db/bundle/create-db-user"
|
|
3226
|
+
migration_function_name = "db-migration-\${random_string.suffix.result}"
|
|
3227
|
+
create_db_user_function_name = "db-create-db-user-\${random_string.suffix.result}"
|
|
3228
|
+
database_runtime_user = "db_\${random_string.suffix.result}"
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
module "aurora" {
|
|
3232
|
+
source = "../../../core/rdb/aurora"
|
|
3233
|
+
#checkov:skip=CKV_AWS_139:Deletion protection is enabled but checkov is unable to detect it correctly
|
|
3234
|
+
name = "db"
|
|
3235
|
+
vpc_id = var.vpc_id
|
|
3236
|
+
subnet_ids = var.database_subnet_ids
|
|
3237
|
+
lambda_subnet_ids = var.lambda_subnet_ids
|
|
3238
|
+
engine = "aurora-mysql"
|
|
3239
|
+
engine_version = var.engine_version
|
|
3240
|
+
database_name = "database_name"
|
|
3241
|
+
admin_user = "databaseUser"
|
|
3242
|
+
port = var.port
|
|
3243
|
+
serverless_min_capacity = var.serverless_min_capacity
|
|
3244
|
+
serverless_max_capacity = var.serverless_max_capacity
|
|
3245
|
+
instance_count = var.instance_count
|
|
3246
|
+
deletion_protection = var.deletion_protection
|
|
3247
|
+
skip_final_snapshot = var.skip_final_snapshot
|
|
3248
|
+
enable_rds_proxy = var.enable_rds_proxy
|
|
3249
|
+
enable_credential_rotation = var.enable_credential_rotation
|
|
3250
|
+
enable_cloudwatch_logs = var.enable_cloudwatch_logs
|
|
3251
|
+
enable_performance_insights = var.enable_performance_insights
|
|
3252
|
+
performance_insights_retention_period = var.performance_insights_retention_period
|
|
3253
|
+
enable_backup = var.enable_backup
|
|
3254
|
+
tags = var.tags
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
resource "aws_iam_role_policy" "proxy_db_user_connect" {
|
|
3258
|
+
count = var.enable_rds_proxy ? 1 : 0
|
|
3259
|
+
|
|
3260
|
+
role = module.aurora.proxy_role_name
|
|
3261
|
+
|
|
3262
|
+
policy = jsonencode({
|
|
3263
|
+
Version = "2012-10-17"
|
|
3264
|
+
Statement = [
|
|
3265
|
+
{
|
|
3266
|
+
Effect = "Allow"
|
|
3267
|
+
Action = ["rds-db:connect"]
|
|
3268
|
+
Resource = [
|
|
3269
|
+
"arn:aws:rds-db:\${data.aws_region.current.region}:\${data.aws_caller_identity.current.account_id}:dbuser:\${module.aurora.cluster_resource_id}/\${local.database_runtime_user}"
|
|
3270
|
+
]
|
|
3271
|
+
}
|
|
3272
|
+
]
|
|
3273
|
+
})
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
data "archive_file" "create_db_user_zip" {
|
|
3277
|
+
type = "zip"
|
|
3278
|
+
source_dir = local.create_db_user_bundle_path
|
|
3279
|
+
output_path = "\${path.module}/../../../../../../../dist/packages/common/terraform/dbs/db/create-db-user.zip"
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
resource "aws_s3_object" "create_db_user_zip" {
|
|
3283
|
+
bucket = var.asset_bucket_name
|
|
3284
|
+
key = "dbs/db/\${data.archive_file.create_db_user_zip.output_sha256}.zip"
|
|
3285
|
+
source = data.archive_file.create_db_user_zip.output_path
|
|
3286
|
+
source_hash = data.archive_file.create_db_user_zip.output_base64sha256
|
|
3287
|
+
etag = data.archive_file.create_db_user_zip.output_md5
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
module "add_rdb_to_runtime_config" {
|
|
3291
|
+
source = "../../../core/runtime-config/entry"
|
|
3292
|
+
|
|
3293
|
+
namespace = "database"
|
|
3294
|
+
key = "Db"
|
|
3295
|
+
value = {
|
|
3296
|
+
hostname = module.aurora.cluster_endpoint
|
|
3297
|
+
port = module.aurora.cluster_port
|
|
3298
|
+
database = module.aurora.database_name
|
|
3299
|
+
adminUser = module.aurora.admin_user
|
|
3300
|
+
dbUser = local.database_runtime_user
|
|
3301
|
+
region = data.aws_region.current.region
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
resource "aws_ecr_repository" "migration_handler" {
|
|
3306
|
+
#checkov:skip=CKV_AWS_136:AES256 encryption is sufficient for ECR repositories
|
|
3307
|
+
#checkov:skip=CKV_AWS_51:Mutable tags are intentional; the migration handler is always pushed as latest and redeployment is gated on docker_digest changes in null_resource.docker_publish
|
|
3308
|
+
name = "db-migration-\${random_string.suffix.result}"
|
|
3309
|
+
image_tag_mutability = "MUTABLE"
|
|
3310
|
+
force_delete = true
|
|
3311
|
+
|
|
3312
|
+
image_scanning_configuration {
|
|
3313
|
+
scan_on_push = true
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
tags = var.tags
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
resource "aws_ecr_repository_policy" "migration_handler" {
|
|
3320
|
+
repository = aws_ecr_repository.migration_handler.name
|
|
3321
|
+
|
|
3322
|
+
policy = jsonencode({
|
|
3323
|
+
Version = "2012-10-17"
|
|
3324
|
+
Statement = [
|
|
3325
|
+
{
|
|
3326
|
+
Sid = "AllowPushPull"
|
|
3327
|
+
Effect = "Allow"
|
|
3328
|
+
Principal = {
|
|
3329
|
+
AWS = "arn:aws:iam::\${data.aws_caller_identity.current.account_id}:root"
|
|
3330
|
+
}
|
|
3331
|
+
Action = [
|
|
3332
|
+
"ecr:BatchCheckLayerAvailability",
|
|
3333
|
+
"ecr:CompleteLayerUpload",
|
|
3334
|
+
"ecr:GetDownloadUrlForLayer",
|
|
3335
|
+
"ecr:InitiateLayerUpload",
|
|
3336
|
+
"ecr:PutImage",
|
|
3337
|
+
"ecr:UploadLayerPart"
|
|
3338
|
+
]
|
|
3339
|
+
},
|
|
3340
|
+
{
|
|
3341
|
+
Sid = "AllowLambdaPull"
|
|
3342
|
+
Effect = "Allow"
|
|
3343
|
+
Principal = {
|
|
3344
|
+
Service = "lambda.amazonaws.com"
|
|
3345
|
+
}
|
|
3346
|
+
Action = [
|
|
3347
|
+
"ecr:BatchCheckLayerAvailability",
|
|
3348
|
+
"ecr:BatchGetImage",
|
|
3349
|
+
"ecr:GetDownloadUrlForLayer"
|
|
3350
|
+
]
|
|
3351
|
+
}
|
|
3352
|
+
]
|
|
3353
|
+
})
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
data "external" "docker_digest" {
|
|
3357
|
+
program = ["sh", "-c", "echo '{\\"digest\\":\\"'$(docker inspect \${var.docker_image_tag} --format '{{.Id}}')'\\"}' "]
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
resource "null_resource" "docker_publish" {
|
|
3361
|
+
triggers = {
|
|
3362
|
+
docker_digest = data.external.docker_digest.result.digest
|
|
3363
|
+
repository_url = aws_ecr_repository.migration_handler.repository_url
|
|
3364
|
+
docker_image_tag = var.docker_image_tag
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
provisioner "local-exec" {
|
|
3368
|
+
command = <<-EOT
|
|
3369
|
+
aws ecr get-login-password --region \${data.aws_region.current.region} | docker login --username AWS --password-stdin \${self.triggers.repository_url}
|
|
3370
|
+
docker tag \${self.triggers.docker_image_tag} \${self.triggers.repository_url}:latest
|
|
3371
|
+
docker push \${self.triggers.repository_url}:latest
|
|
3372
|
+
EOT
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
depends_on = [aws_ecr_repository_policy.migration_handler]
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
resource "aws_iam_role" "migration_handler" {
|
|
3379
|
+
name = "DbMigrationHandlerRole-\${random_string.suffix.result}"
|
|
3380
|
+
|
|
3381
|
+
assume_role_policy = jsonencode({
|
|
3382
|
+
Version = "2012-10-17"
|
|
3383
|
+
Statement = [
|
|
3384
|
+
{
|
|
3385
|
+
Effect = "Allow"
|
|
3386
|
+
Principal = {
|
|
3387
|
+
Service = "lambda.amazonaws.com"
|
|
3388
|
+
}
|
|
3389
|
+
Action = "sts:AssumeRole"
|
|
3390
|
+
}
|
|
3391
|
+
]
|
|
3392
|
+
})
|
|
3393
|
+
|
|
3394
|
+
tags = var.tags
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
resource "aws_iam_role_policy_attachment" "migration_handler_basic_execution" {
|
|
3398
|
+
role = aws_iam_role.migration_handler.name
|
|
3399
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
resource "aws_iam_role_policy_attachment" "migration_handler_vpc_access" {
|
|
3403
|
+
role = aws_iam_role.migration_handler.name
|
|
3404
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
resource "aws_iam_role_policy_attachment" "migration_handler_xray" {
|
|
3408
|
+
role = aws_iam_role.migration_handler.name
|
|
3409
|
+
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
resource "aws_iam_role_policy" "migration_handler_access" {
|
|
3413
|
+
role = aws_iam_role.migration_handler.name
|
|
3414
|
+
|
|
3415
|
+
policy = jsonencode({
|
|
3416
|
+
Version = "2012-10-17"
|
|
3417
|
+
Statement = [
|
|
3418
|
+
{
|
|
3419
|
+
Effect = "Allow"
|
|
3420
|
+
Action = [
|
|
3421
|
+
"secretsmanager:GetSecretValue"
|
|
3422
|
+
]
|
|
3423
|
+
Resource = [module.aurora.secret_arn]
|
|
3424
|
+
},
|
|
3425
|
+
{
|
|
3426
|
+
Effect = "Allow"
|
|
3427
|
+
Action = [
|
|
3428
|
+
"kms:Decrypt"
|
|
3429
|
+
]
|
|
3430
|
+
Resource = [module.aurora.kms_key_arn]
|
|
3431
|
+
}
|
|
3432
|
+
]
|
|
3433
|
+
})
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
resource "aws_iam_role" "create_db_user" {
|
|
3437
|
+
name = "DbCreateDbUserRole-\${random_string.suffix.result}"
|
|
3438
|
+
|
|
3439
|
+
assume_role_policy = jsonencode({
|
|
3440
|
+
Version = "2012-10-17"
|
|
3441
|
+
Statement = [
|
|
3442
|
+
{
|
|
3443
|
+
Effect = "Allow"
|
|
3444
|
+
Principal = {
|
|
3445
|
+
Service = "lambda.amazonaws.com"
|
|
3446
|
+
}
|
|
3447
|
+
Action = "sts:AssumeRole"
|
|
3448
|
+
}
|
|
3449
|
+
]
|
|
3450
|
+
})
|
|
3451
|
+
|
|
3452
|
+
tags = var.tags
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
resource "aws_iam_role_policy_attachment" "create_db_user_basic_execution" {
|
|
3456
|
+
role = aws_iam_role.create_db_user.name
|
|
3457
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
|
|
3458
|
+
}
|
|
3459
|
+
|
|
3460
|
+
resource "aws_iam_role_policy_attachment" "create_db_user_vpc_access" {
|
|
3461
|
+
role = aws_iam_role.create_db_user.name
|
|
3462
|
+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
resource "aws_iam_role_policy_attachment" "create_db_user_xray" {
|
|
3466
|
+
role = aws_iam_role.create_db_user.name
|
|
3467
|
+
policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess"
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
resource "aws_iam_role_policy" "create_db_user_secret_access" {
|
|
3471
|
+
role = aws_iam_role.create_db_user.name
|
|
3472
|
+
|
|
3473
|
+
policy = jsonencode({
|
|
3474
|
+
Version = "2012-10-17"
|
|
3475
|
+
Statement = [
|
|
3476
|
+
{
|
|
3477
|
+
Effect = "Allow"
|
|
3478
|
+
Action = [
|
|
3479
|
+
"secretsmanager:GetSecretValue"
|
|
3480
|
+
]
|
|
3481
|
+
Resource = [module.aurora.secret_arn]
|
|
3482
|
+
},
|
|
3483
|
+
{
|
|
3484
|
+
Effect = "Allow"
|
|
3485
|
+
Action = [
|
|
3486
|
+
"kms:Decrypt"
|
|
3487
|
+
]
|
|
3488
|
+
Resource = [module.aurora.kms_key_arn]
|
|
3489
|
+
}
|
|
3490
|
+
]
|
|
3491
|
+
})
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
resource "aws_cloudwatch_log_group" "migration_handler" {
|
|
3495
|
+
#checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
|
|
3496
|
+
#checkov:skip=CKV_AWS_338:Log retention set to forever
|
|
3497
|
+
#checkov:skip=CKV_AWS_66:Log retention set to forever
|
|
3498
|
+
name = "/aws/lambda/\${local.migration_function_name}"
|
|
3499
|
+
tags = var.tags
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
resource "aws_cloudwatch_log_group" "create_db_user" {
|
|
3503
|
+
#checkov:skip=CKV_AWS_158:Using default CloudWatch log encryption
|
|
3504
|
+
#checkov:skip=CKV_AWS_338:Log retention set to forever
|
|
3505
|
+
#checkov:skip=CKV_AWS_66:Log retention set to forever
|
|
3506
|
+
name = "/aws/lambda/\${local.create_db_user_function_name}"
|
|
3507
|
+
tags = var.tags
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
resource "aws_security_group" "migration_handler" {
|
|
3511
|
+
name_prefix = "db-migration-"
|
|
3512
|
+
description = "Security group for the migration Lambda function"
|
|
3513
|
+
vpc_id = var.vpc_id
|
|
3514
|
+
tags = var.tags
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
resource "aws_vpc_security_group_egress_rule" "migration_handler_to_database" {
|
|
3518
|
+
security_group_id = aws_security_group.migration_handler.id
|
|
3519
|
+
referenced_security_group_id = module.aurora.database_security_group_id
|
|
3520
|
+
from_port = module.aurora.cluster_port
|
|
3521
|
+
to_port = module.aurora.cluster_port
|
|
3522
|
+
ip_protocol = "tcp"
|
|
3523
|
+
description = "Allow outbound traffic to Aurora on database port"
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
resource "aws_vpc_security_group_egress_rule" "migration_handler_https" {
|
|
3527
|
+
security_group_id = aws_security_group.migration_handler.id
|
|
3528
|
+
cidr_ipv4 = "0.0.0.0/0"
|
|
3529
|
+
from_port = 443
|
|
3530
|
+
to_port = 443
|
|
3531
|
+
ip_protocol = "tcp"
|
|
3532
|
+
description = "Allow outbound HTTPS to AWS service endpoints"
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
resource "aws_security_group" "create_db_user" {
|
|
3536
|
+
name_prefix = "db-create-db-user-"
|
|
3537
|
+
description = "Security group for the create-db-user Lambda function"
|
|
3538
|
+
vpc_id = var.vpc_id
|
|
3539
|
+
tags = var.tags
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
resource "aws_vpc_security_group_egress_rule" "create_db_user_to_database" {
|
|
3543
|
+
security_group_id = aws_security_group.create_db_user.id
|
|
3544
|
+
referenced_security_group_id = module.aurora.database_security_group_id
|
|
3545
|
+
from_port = module.aurora.cluster_port
|
|
3546
|
+
to_port = module.aurora.cluster_port
|
|
3547
|
+
ip_protocol = "tcp"
|
|
3548
|
+
description = "Allow outbound traffic to Aurora on database port"
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
resource "aws_vpc_security_group_egress_rule" "create_db_user_https" {
|
|
3552
|
+
security_group_id = aws_security_group.create_db_user.id
|
|
3553
|
+
cidr_ipv4 = "0.0.0.0/0"
|
|
3554
|
+
from_port = 443
|
|
3555
|
+
to_port = 443
|
|
3556
|
+
ip_protocol = "tcp"
|
|
3557
|
+
description = "Allow outbound HTTPS to AWS service endpoints"
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
resource "aws_vpc_security_group_ingress_rule" "migration_handler_to_database" {
|
|
3561
|
+
security_group_id = module.aurora.database_security_group_id
|
|
3562
|
+
referenced_security_group_id = aws_security_group.migration_handler.id
|
|
3563
|
+
from_port = module.aurora.cluster_port
|
|
3564
|
+
to_port = module.aurora.cluster_port
|
|
3565
|
+
ip_protocol = "tcp"
|
|
3566
|
+
description = "Allow inbound traffic from migration Lambda on database port"
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
resource "aws_vpc_security_group_ingress_rule" "create_db_user_to_database" {
|
|
3570
|
+
security_group_id = module.aurora.database_security_group_id
|
|
3571
|
+
referenced_security_group_id = aws_security_group.create_db_user.id
|
|
3572
|
+
from_port = module.aurora.cluster_port
|
|
3573
|
+
to_port = module.aurora.cluster_port
|
|
3574
|
+
ip_protocol = "tcp"
|
|
3575
|
+
description = "Allow inbound traffic from create-db-user Lambda on database port"
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
resource "aws_lambda_function" "create_db_user" {
|
|
3579
|
+
#checkov:skip=CKV_AWS_116:Dead Letter Queue not required for the create-db-user handler
|
|
3580
|
+
#checkov:skip=CKV_AWS_115:Concurrent execution limit not required for the create-db-user handler
|
|
3581
|
+
#checkov:skip=CKV_AWS_173:Lambda environment variables encrypted by managed key
|
|
3582
|
+
#checkov:skip=CKV_AWS_272:Code signing not configured as deployment packages are built and deployed within the same pipeline
|
|
3583
|
+
s3_bucket = aws_s3_object.create_db_user_zip.bucket
|
|
3584
|
+
s3_key = aws_s3_object.create_db_user_zip.key
|
|
3585
|
+
s3_object_version = aws_s3_object.create_db_user_zip.version_id
|
|
3586
|
+
function_name = local.create_db_user_function_name
|
|
3587
|
+
role = aws_iam_role.create_db_user.arn
|
|
3588
|
+
handler = "index.handler"
|
|
3589
|
+
source_code_hash = data.archive_file.create_db_user_zip.output_base64sha256
|
|
3590
|
+
runtime = "nodejs24.x"
|
|
3591
|
+
timeout = 300
|
|
3592
|
+
architectures = ["arm64"]
|
|
3593
|
+
|
|
3594
|
+
tracing_config {
|
|
3595
|
+
mode = "Active"
|
|
3596
|
+
}
|
|
3597
|
+
|
|
3598
|
+
vpc_config {
|
|
3599
|
+
subnet_ids = var.lambda_subnet_ids
|
|
3600
|
+
security_group_ids = [aws_security_group.create_db_user.id]
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
environment {
|
|
3604
|
+
variables = {
|
|
3605
|
+
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
3606
|
+
NODE_EXTRA_CA_CERTS = "/var/runtime/ca-cert.pem"
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
|
|
3610
|
+
tags = var.tags
|
|
3611
|
+
|
|
3612
|
+
depends_on = [
|
|
3613
|
+
aws_s3_object.create_db_user_zip,
|
|
3614
|
+
aws_iam_role_policy_attachment.create_db_user_basic_execution,
|
|
3615
|
+
aws_iam_role_policy_attachment.create_db_user_vpc_access,
|
|
3616
|
+
aws_iam_role_policy_attachment.create_db_user_xray,
|
|
3617
|
+
aws_iam_role_policy.create_db_user_secret_access,
|
|
3618
|
+
aws_cloudwatch_log_group.create_db_user,
|
|
3619
|
+
aws_vpc_security_group_ingress_rule.create_db_user_to_database
|
|
3620
|
+
]
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
resource "null_resource" "create_db_user_trigger" {
|
|
3624
|
+
triggers = {
|
|
3625
|
+
cluster_arn = module.aurora.cluster_arn
|
|
3626
|
+
function_name = aws_lambda_function.create_db_user.function_name
|
|
3627
|
+
db_user = local.database_runtime_user
|
|
3628
|
+
bundle_hash = data.archive_file.create_db_user_zip.output_base64sha256
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
provisioner "local-exec" {
|
|
3632
|
+
command = <<-EOT
|
|
3633
|
+
output_file=$(mktemp)
|
|
3634
|
+
response=$(aws lambda invoke \\
|
|
3635
|
+
--region \${data.aws_region.current.region} \\
|
|
3636
|
+
--function-name \${self.triggers.function_name} \\
|
|
3637
|
+
--cli-binary-format raw-in-base64-out \\
|
|
3638
|
+
--payload '{"RequestType":"Create","PhysicalResourceId":"db-user:\${self.triggers.db_user}"}' \\
|
|
3639
|
+
"$output_file")
|
|
3640
|
+
cat "$output_file"
|
|
3641
|
+
echo "$response"
|
|
3642
|
+
if echo "$response" | grep -q '"FunctionError"'; then
|
|
3643
|
+
rm -f "$output_file"
|
|
3644
|
+
exit 1
|
|
3645
|
+
fi
|
|
3646
|
+
rm -f "$output_file"
|
|
3647
|
+
EOT
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
depends_on = [aws_lambda_function.create_db_user, module.aurora]
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
resource "aws_lambda_function" "migration_handler" {
|
|
3654
|
+
#checkov:skip=CKV_AWS_116:Dead Letter Queue not required for the migration handler
|
|
3655
|
+
#checkov:skip=CKV_AWS_115:Concurrent execution limit not required for the migration handler
|
|
3656
|
+
#checkov:skip=CKV_AWS_173:Lambda environment variables encrypted by managed key
|
|
3657
|
+
#checkov:skip=CKV_AWS_272:Code signing does not apply to container image Lambda functions
|
|
3658
|
+
package_type = "Image"
|
|
3659
|
+
function_name = local.migration_function_name
|
|
3660
|
+
role = aws_iam_role.migration_handler.arn
|
|
3661
|
+
image_uri = "\${aws_ecr_repository.migration_handler.repository_url}:latest"
|
|
3662
|
+
memory_size = 1024
|
|
3663
|
+
timeout = 300
|
|
3664
|
+
architectures = ["arm64"]
|
|
3665
|
+
|
|
3666
|
+
tracing_config {
|
|
3667
|
+
mode = "Active"
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
vpc_config {
|
|
3671
|
+
subnet_ids = var.lambda_subnet_ids
|
|
3672
|
+
security_group_ids = [aws_security_group.migration_handler.id]
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
environment {
|
|
3676
|
+
variables = {
|
|
3677
|
+
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
tags = var.tags
|
|
3682
|
+
|
|
3683
|
+
depends_on = [
|
|
3684
|
+
null_resource.docker_publish,
|
|
3685
|
+
aws_iam_role_policy_attachment.migration_handler_basic_execution,
|
|
3686
|
+
aws_iam_role_policy_attachment.migration_handler_vpc_access,
|
|
3687
|
+
aws_iam_role_policy_attachment.migration_handler_xray,
|
|
3688
|
+
aws_iam_role_policy.migration_handler_access,
|
|
3689
|
+
aws_cloudwatch_log_group.migration_handler,
|
|
3690
|
+
aws_vpc_security_group_ingress_rule.migration_handler_to_database
|
|
3691
|
+
]
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
resource "null_resource" "migration_trigger" {
|
|
3695
|
+
triggers = {
|
|
3696
|
+
docker_digest = data.external.docker_digest.result.digest
|
|
3697
|
+
cluster_arn = module.aurora.cluster_arn
|
|
3698
|
+
database_ready = module.aurora.database_ready
|
|
3699
|
+
function_name = aws_lambda_function.migration_handler.function_name
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
provisioner "local-exec" {
|
|
3703
|
+
command = <<-EOT
|
|
3704
|
+
output_file=$(mktemp)
|
|
3705
|
+
response=$(aws lambda invoke \\
|
|
3706
|
+
--region \${data.aws_region.current.region} \\
|
|
3707
|
+
--function-name \${self.triggers.function_name} \\
|
|
3708
|
+
--cli-binary-format raw-in-base64-out \\
|
|
3709
|
+
"$output_file")
|
|
3710
|
+
cat "$output_file"
|
|
3711
|
+
echo "$response"
|
|
3712
|
+
if echo "$response" | grep -q '"FunctionError"'; then
|
|
3713
|
+
rm -f "$output_file"
|
|
3714
|
+
exit 1
|
|
3715
|
+
fi
|
|
3716
|
+
rm -f "$output_file"
|
|
3717
|
+
EOT
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
depends_on = [
|
|
3721
|
+
aws_lambda_function.migration_handler,
|
|
3722
|
+
module.aurora,
|
|
3723
|
+
null_resource.create_db_user_trigger,
|
|
3724
|
+
aws_iam_role_policy.proxy_db_user_connect
|
|
3725
|
+
]
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
output "security_group_id" {
|
|
3729
|
+
description = "Security group ID to allow inbound connections to the database (proxy SG if RDS Proxy is enabled, otherwise DB SG)."
|
|
3730
|
+
value = module.aurora.security_group_id
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
output "database_security_group_id" {
|
|
3734
|
+
description = "Security group ID of the Aurora cluster directly, used for direct DB access rules (e.g. when RDS Proxy is disabled)."
|
|
3735
|
+
value = module.aurora.database_security_group_id
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
output "cluster_resource_id" {
|
|
3739
|
+
description = "Resource ID of the Aurora cluster, used for IAM rds-db:connect policies."
|
|
3740
|
+
value = module.aurora.cluster_resource_id
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
output "proxy_resource_id" {
|
|
3744
|
+
description = "Resource identifier of the RDS Proxy (prx-XXXX), used for IAM rds-db:connect policies when connecting through the proxy. Null if RDS Proxy is disabled."
|
|
3745
|
+
value = module.aurora.proxy_resource_id
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3748
|
+
output "kms_key_arn" {
|
|
3749
|
+
description = "ARN of the KMS key protecting Aurora and the admin credentials secret, used to grant decrypt access alongside secret_arn."
|
|
3750
|
+
value = module.aurora.kms_key_arn
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
output "cluster_arn" {
|
|
3754
|
+
description = "ARN of the Aurora cluster."
|
|
3755
|
+
value = module.aurora.cluster_arn
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3758
|
+
output "cluster_endpoint" {
|
|
3759
|
+
description = "Writer endpoint of the Aurora cluster."
|
|
3760
|
+
value = module.aurora.cluster_endpoint
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
output "reader_endpoint" {
|
|
3764
|
+
description = "Reader endpoint of the Aurora cluster."
|
|
3765
|
+
value = module.aurora.reader_endpoint
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
output "cluster_port" {
|
|
3769
|
+
description = "Port exposed by the Aurora cluster."
|
|
3770
|
+
value = module.aurora.cluster_port
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
output "secret_arn" {
|
|
3774
|
+
description = "ARN of the generated admin credentials secret."
|
|
3775
|
+
value = module.aurora.secret_arn
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
output "database_runtime_user" {
|
|
3779
|
+
description = "Application database user created for IAM-authenticated access."
|
|
3780
|
+
value = local.database_runtime_user
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3783
|
+
"
|
|
3784
|
+
`;
|
|
3785
|
+
|
|
3786
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/prisma/models/example.prisma 1`] = `
|
|
3787
|
+
"model ExampleTable {
|
|
3788
|
+
id Int @id @default(autoincrement())
|
|
3789
|
+
column1 String
|
|
3790
|
+
column2 String
|
|
3791
|
+
}
|
|
3792
|
+
"
|
|
3793
|
+
`;
|
|
3794
|
+
|
|
3795
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/prisma/schema.prisma 1`] = `
|
|
3796
|
+
"generator client {
|
|
3797
|
+
provider = "prisma-client"
|
|
3798
|
+
output = "../generated/prisma"
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
datasource db {
|
|
3802
|
+
provider = "postgresql"
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
"
|
|
3806
|
+
`;
|
|
3807
|
+
|
|
3808
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/scripts/docker-pull.ts 1`] = `
|
|
3809
|
+
"import Docker from 'dockerode';
|
|
3810
|
+
import { promisify } from 'util';
|
|
3811
|
+
|
|
3812
|
+
const docker = new Docker();
|
|
3813
|
+
const image = process.argv[2];
|
|
3814
|
+
|
|
3815
|
+
try {
|
|
3816
|
+
await docker.getImage(image).inspect();
|
|
3817
|
+
} catch (e) {
|
|
3818
|
+
if ((e as { statusCode?: number }).statusCode !== 404) throw e;
|
|
3819
|
+
const stream = (await promisify(docker.pull.bind(docker))(
|
|
3820
|
+
image,
|
|
3821
|
+
)) as NodeJS.ReadableStream;
|
|
3822
|
+
await promisify(docker.modem.followProgress.bind(docker.modem))(stream);
|
|
3823
|
+
}
|
|
3824
|
+
"
|
|
3825
|
+
`;
|
|
3826
|
+
|
|
3827
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/scripts/docker-start.ts 1`] = `
|
|
3828
|
+
"import Docker from 'dockerode';
|
|
3829
|
+
|
|
3830
|
+
const [containerName, image, hostPort, dbName, dbUser, dbPassword] =
|
|
3831
|
+
process.argv.slice(2);
|
|
3832
|
+
|
|
3833
|
+
const docker = new Docker();
|
|
3834
|
+
|
|
3835
|
+
let container: Docker.Container;
|
|
3836
|
+
|
|
3837
|
+
try {
|
|
3838
|
+
const existing = docker.getContainer(containerName);
|
|
3839
|
+
const info = await existing.inspect();
|
|
3840
|
+
container = existing;
|
|
3841
|
+
if (!info.State.Running) {
|
|
3842
|
+
await container.start();
|
|
3843
|
+
}
|
|
3844
|
+
} catch (e) {
|
|
3845
|
+
if ((e as { statusCode?: number }).statusCode !== 404) throw e;
|
|
3846
|
+
container = await docker.createContainer({
|
|
3847
|
+
name: containerName,
|
|
3848
|
+
Image: image,
|
|
3849
|
+
Env: [
|
|
3850
|
+
\`POSTGRES_DB=\${dbName}\`,
|
|
3851
|
+
\`POSTGRES_USER=\${dbUser}\`,
|
|
3852
|
+
\`POSTGRES_PASSWORD=\${dbPassword}\`,
|
|
3853
|
+
],
|
|
3854
|
+
ExposedPorts: { '5432/tcp': {} },
|
|
3855
|
+
HostConfig: {
|
|
3856
|
+
AutoRemove: true,
|
|
3857
|
+
PortBindings: { '5432/tcp': [{ HostPort: hostPort }] },
|
|
3858
|
+
Binds: [\`\${containerName}-data:/var/lib/postgresql\`],
|
|
3859
|
+
},
|
|
3860
|
+
});
|
|
3861
|
+
await container.start();
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
const stream = await container.attach({
|
|
3865
|
+
stream: true,
|
|
3866
|
+
stdout: true,
|
|
3867
|
+
stderr: true,
|
|
3868
|
+
});
|
|
3869
|
+
container.modem.demuxStream(stream, process.stdout, process.stderr);
|
|
3870
|
+
|
|
3871
|
+
let exiting = false;
|
|
3872
|
+
|
|
3873
|
+
async function cleanup() {
|
|
3874
|
+
if (exiting) return;
|
|
3875
|
+
exiting = true;
|
|
3876
|
+
try {
|
|
3877
|
+
await container.stop();
|
|
3878
|
+
} catch (e) {
|
|
3879
|
+
if ((e as { statusCode?: number }).statusCode !== 404) console.error(e);
|
|
3880
|
+
}
|
|
3881
|
+
process.exit(0);
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
process.on('SIGTERM', () => void cleanup());
|
|
3885
|
+
process.on('SIGINT', () => void cleanup());
|
|
3886
|
+
process.on('SIGHUP', () => void cleanup());
|
|
3887
|
+
|
|
3888
|
+
const { StatusCode } = await container.wait();
|
|
3889
|
+
if (!exiting) process.exit(StatusCode);
|
|
3890
|
+
"
|
|
3891
|
+
`;
|
|
3892
|
+
|
|
3893
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/scripts/wait-for-db.ts 1`] = `
|
|
3894
|
+
"import { Client } from 'pg';
|
|
3895
|
+
|
|
3896
|
+
const noop = () => undefined;
|
|
3897
|
+
|
|
3898
|
+
const [portArg, dbArg, userArg, passwordArg] = process.argv.slice(2);
|
|
3899
|
+
const timeoutAt = Date.now() + 60000;
|
|
3900
|
+
|
|
3901
|
+
while (Date.now() < timeoutAt) {
|
|
3902
|
+
const client = new Client({
|
|
3903
|
+
host: 'localhost',
|
|
3904
|
+
port: parseInt(portArg),
|
|
3905
|
+
user: userArg,
|
|
3906
|
+
password: passwordArg,
|
|
3907
|
+
database: dbArg,
|
|
3908
|
+
connectionTimeoutMillis: 500,
|
|
3909
|
+
});
|
|
3910
|
+
client.on('error', noop);
|
|
3911
|
+
try {
|
|
3912
|
+
await client.connect();
|
|
3913
|
+
await client.end();
|
|
3914
|
+
console.log('Database is ready.');
|
|
3915
|
+
process.exit(0);
|
|
3916
|
+
} catch {
|
|
3917
|
+
console.log('Database is not ready.');
|
|
3918
|
+
await client.end().catch(noop);
|
|
3919
|
+
}
|
|
3920
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
throw new Error(\`Timed out waiting for postgres on port \${portArg}\`);
|
|
3924
|
+
"
|
|
3925
|
+
`;
|
|
3926
|
+
|
|
3927
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/src/constants.ts 1`] = `
|
|
3928
|
+
"export const DB_PACKAGE_NAME = 'Db';
|
|
3929
|
+
|
|
3930
|
+
// Local development connection details (used when SERVE_LOCAL=true, see serve-local Nx target)
|
|
3931
|
+
export const LOCAL_DB_PORT = 5432;
|
|
3932
|
+
export const LOCAL_DB_HOST = 'localhost';
|
|
3933
|
+
export const LOCAL_DB_NAME = 'database_name';
|
|
3934
|
+
export const LOCAL_DB_USER = 'dbadmin';
|
|
3935
|
+
export const LOCAL_DB_PASSWORD = 'password';
|
|
3936
|
+
"
|
|
3937
|
+
`;
|
|
3938
|
+
|
|
3939
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/src/create-db-user-handler.ts 1`] = `
|
|
3940
|
+
"import type { CloudFormationCustomResourceEvent } from 'aws-lambda';
|
|
3941
|
+
import { randomBytes } from 'node:crypto';
|
|
3942
|
+
import { Client, escapeIdentifier } from 'pg';
|
|
3943
|
+
import { getDatabaseSecret, withConnectionRetry } from './utils.js';
|
|
3944
|
+
|
|
3945
|
+
type OnEventResult = {
|
|
3946
|
+
PhysicalResourceId: string;
|
|
3947
|
+
Data?: Record<string, unknown>;
|
|
3948
|
+
};
|
|
3949
|
+
|
|
3950
|
+
const physicalResourceIdPrefix = 'db-user:';
|
|
3951
|
+
|
|
3952
|
+
const resolveDbUser = (physicalResourceId?: string): string =>
|
|
3953
|
+
physicalResourceId?.startsWith(physicalResourceIdPrefix)
|
|
3954
|
+
? physicalResourceId.slice(physicalResourceIdPrefix.length)
|
|
3955
|
+
: \`db_\${randomBytes(8).toString('hex')}\`;
|
|
3956
|
+
|
|
3957
|
+
const ensureDatabaseUser = async (dbUser: string): Promise<void> => {
|
|
3958
|
+
const { dbname, username, password, host, port } = await getDatabaseSecret();
|
|
3959
|
+
const quotedDbUser = escapeIdentifier(dbUser);
|
|
3960
|
+
const quotedDbName = escapeIdentifier(dbname);
|
|
3961
|
+
const client = new Client({
|
|
3962
|
+
host,
|
|
3963
|
+
port,
|
|
3964
|
+
database: dbname,
|
|
3965
|
+
user: username,
|
|
3966
|
+
password,
|
|
3967
|
+
ssl: {
|
|
3968
|
+
rejectUnauthorized: true,
|
|
3969
|
+
},
|
|
3970
|
+
connectionTimeoutMillis: 10_000,
|
|
3971
|
+
});
|
|
3972
|
+
|
|
3973
|
+
await client.connect();
|
|
3974
|
+
|
|
3975
|
+
try {
|
|
3976
|
+
await client.query('BEGIN');
|
|
3977
|
+
|
|
3978
|
+
const roleExists = await client.query(
|
|
3979
|
+
'SELECT 1 FROM pg_roles WHERE rolname = $1',
|
|
3980
|
+
[dbUser],
|
|
3981
|
+
);
|
|
3982
|
+
if (roleExists.rowCount === 0) {
|
|
3983
|
+
await client.query(\`CREATE ROLE \${quotedDbUser} WITH LOGIN;\`);
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
await client.query(
|
|
3987
|
+
\`ALTER ROLE \${quotedDbUser} WITH LOGIN;
|
|
3988
|
+
GRANT ALL PRIVILEGES ON DATABASE \${quotedDbName} TO \${quotedDbUser};
|
|
3989
|
+
GRANT USAGE, CREATE ON SCHEMA public TO \${quotedDbUser};
|
|
3990
|
+
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \${quotedDbUser};
|
|
3991
|
+
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \${quotedDbUser};
|
|
3992
|
+
GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \${quotedDbUser};
|
|
3993
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO \${quotedDbUser};
|
|
3994
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON SEQUENCES TO \${quotedDbUser};
|
|
3995
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON FUNCTIONS TO \${quotedDbUser};
|
|
3996
|
+
GRANT rds_iam TO \${quotedDbUser};\`,
|
|
3997
|
+
);
|
|
3998
|
+
|
|
3999
|
+
await client.query('COMMIT');
|
|
4000
|
+
} catch (error) {
|
|
4001
|
+
await client.query('ROLLBACK');
|
|
4002
|
+
throw error;
|
|
4003
|
+
} finally {
|
|
4004
|
+
await client.end();
|
|
4005
|
+
}
|
|
4006
|
+
};
|
|
4007
|
+
|
|
4008
|
+
export const handler = async (
|
|
4009
|
+
event: CloudFormationCustomResourceEvent,
|
|
4010
|
+
): Promise<OnEventResult> => {
|
|
4011
|
+
const dbUser = resolveDbUser(
|
|
4012
|
+
'PhysicalResourceId' in event ? event.PhysicalResourceId : undefined,
|
|
4013
|
+
);
|
|
4014
|
+
|
|
4015
|
+
if (event.RequestType !== 'Delete') {
|
|
4016
|
+
await withConnectionRetry(() => ensureDatabaseUser(dbUser));
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
return {
|
|
4020
|
+
PhysicalResourceId: \`\${physicalResourceIdPrefix}\${dbUser}\`,
|
|
4021
|
+
Data: {
|
|
4022
|
+
dbUser,
|
|
4023
|
+
},
|
|
4024
|
+
};
|
|
4025
|
+
};
|
|
4026
|
+
"
|
|
4027
|
+
`;
|
|
4028
|
+
|
|
4029
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/src/index.ts 1`] = `
|
|
4030
|
+
"export { DB_PACKAGE_NAME } from './constants.js';
|
|
4031
|
+
export { getPrisma } from './prisma.js';
|
|
4032
|
+
"
|
|
4033
|
+
`;
|
|
4034
|
+
|
|
4035
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/src/migration-handler.ts 1`] = `
|
|
4036
|
+
"import { execFile } from 'node:child_process';
|
|
4037
|
+
import { promisify } from 'node:util';
|
|
4038
|
+
import { Signer } from '@aws-sdk/rds-signer';
|
|
4039
|
+
import { withConnectionRetry } from './utils.js';
|
|
4040
|
+
|
|
4041
|
+
const buildDatabaseUrl = async (): Promise<string> => {
|
|
4042
|
+
const hostname = process.env.HOSTNAME!;
|
|
4043
|
+
const port = process.env.PORT!;
|
|
4044
|
+
const database = process.env.DATABASE!;
|
|
4045
|
+
const dbUser = process.env.DBUSER!;
|
|
4046
|
+
const region = process.env.AWS_REGION!;
|
|
4047
|
+
|
|
4048
|
+
const iamAuthToken = await new Signer({
|
|
4049
|
+
hostname,
|
|
4050
|
+
port: Number(port),
|
|
4051
|
+
region,
|
|
4052
|
+
username: dbUser,
|
|
4053
|
+
}).getAuthToken();
|
|
4054
|
+
|
|
4055
|
+
return (
|
|
4056
|
+
\`postgresql://\${encodeURIComponent(dbUser)}\` +
|
|
4057
|
+
\`:\${encodeURIComponent(iamAuthToken)}\` +
|
|
4058
|
+
\`@\${hostname}:\${port}/\${database}\` +
|
|
4059
|
+
\`?sslaccept=strict\` // \`sslaccept\` is the correct parameter here for postgresql. The Prisma CLI Rust engine does not use the standard libpq \`sslmode\` option.
|
|
4060
|
+
);
|
|
4061
|
+
};
|
|
4062
|
+
|
|
4063
|
+
export const handler = async () => {
|
|
4064
|
+
await withConnectionRetry(async () => {
|
|
4065
|
+
const databaseUrl = await buildDatabaseUrl();
|
|
4066
|
+
await promisify(execFile)('npx', ['prisma', 'migrate', 'deploy'], {
|
|
4067
|
+
cwd: __dirname,
|
|
4068
|
+
env: {
|
|
4069
|
+
...process.env,
|
|
4070
|
+
DATABASE_URL: databaseUrl,
|
|
4071
|
+
},
|
|
4072
|
+
});
|
|
4073
|
+
});
|
|
4074
|
+
};
|
|
4075
|
+
"
|
|
4076
|
+
`;
|
|
4077
|
+
|
|
4078
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/src/prisma.ts 1`] = `
|
|
4079
|
+
"import { PrismaPg } from '@prisma/adapter-pg';
|
|
4080
|
+
import { Signer } from '@aws-sdk/rds-signer';
|
|
4081
|
+
import { Pool } from 'pg';
|
|
4082
|
+
import { PrismaClient } from '../generated/prisma/client.js';
|
|
4083
|
+
import {
|
|
4084
|
+
DB_PACKAGE_NAME,
|
|
4085
|
+
LOCAL_DB_HOST,
|
|
4086
|
+
LOCAL_DB_NAME,
|
|
4087
|
+
LOCAL_DB_PASSWORD,
|
|
4088
|
+
LOCAL_DB_PORT,
|
|
4089
|
+
LOCAL_DB_USER,
|
|
4090
|
+
} from './constants.js';
|
|
4091
|
+
import { getDatabaseConfig } from './utils.js';
|
|
4092
|
+
|
|
4093
|
+
let prismaPromise: Promise<PrismaClient> | undefined;
|
|
4094
|
+
|
|
4095
|
+
export const getPrisma = (): Promise<PrismaClient> => {
|
|
4096
|
+
prismaPromise ??= (async () => {
|
|
4097
|
+
if (process.env.SERVE_LOCAL === 'true') {
|
|
4098
|
+
const adapter = new PrismaPg(
|
|
4099
|
+
new Pool({
|
|
4100
|
+
host: LOCAL_DB_HOST,
|
|
4101
|
+
port: LOCAL_DB_PORT,
|
|
4102
|
+
database: LOCAL_DB_NAME,
|
|
4103
|
+
user: LOCAL_DB_USER,
|
|
4104
|
+
password: LOCAL_DB_PASSWORD,
|
|
4105
|
+
allowExitOnIdle: true,
|
|
4106
|
+
}),
|
|
4107
|
+
);
|
|
4108
|
+
return new PrismaClient({ adapter });
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
const { hostname, port, database, dbUser, region } =
|
|
4112
|
+
await getDatabaseConfig(DB_PACKAGE_NAME);
|
|
4113
|
+
const adapter = new PrismaPg(
|
|
4114
|
+
new Pool({
|
|
4115
|
+
host: hostname,
|
|
4116
|
+
port,
|
|
4117
|
+
database,
|
|
4118
|
+
user: dbUser,
|
|
4119
|
+
ssl: {
|
|
4120
|
+
rejectUnauthorized: true,
|
|
4121
|
+
},
|
|
4122
|
+
allowExitOnIdle: true,
|
|
4123
|
+
password: async () => {
|
|
4124
|
+
const token = await new Signer({
|
|
4125
|
+
hostname,
|
|
4126
|
+
port,
|
|
4127
|
+
region,
|
|
4128
|
+
username: dbUser,
|
|
4129
|
+
}).getAuthToken();
|
|
4130
|
+
return token;
|
|
4131
|
+
},
|
|
4132
|
+
}),
|
|
4133
|
+
);
|
|
4134
|
+
|
|
4135
|
+
return new PrismaClient({ adapter });
|
|
4136
|
+
})();
|
|
4137
|
+
|
|
4138
|
+
return prismaPromise;
|
|
4139
|
+
};
|
|
4140
|
+
"
|
|
4141
|
+
`;
|
|
4142
|
+
|
|
4143
|
+
exports[`ts#rdb generator > should generate the aurora shared construct > packages/db/src/utils.ts 1`] = `
|
|
4144
|
+
"import { getAppConfig } from '@aws-lambda-powertools/parameters/appconfig';
|
|
4145
|
+
import {
|
|
4146
|
+
GetSecretValueCommand,
|
|
4147
|
+
SecretsManagerClient,
|
|
4148
|
+
} from '@aws-sdk/client-secrets-manager';
|
|
4149
|
+
|
|
4150
|
+
export type DatabaseConfig = {
|
|
4151
|
+
hostname: string;
|
|
4152
|
+
port: number;
|
|
4153
|
+
database: string;
|
|
4154
|
+
adminUser: string;
|
|
4155
|
+
dbUser: string;
|
|
4156
|
+
region: string;
|
|
4157
|
+
};
|
|
4158
|
+
|
|
4159
|
+
export type DatabaseSecret = {
|
|
4160
|
+
dbname: string;
|
|
4161
|
+
username: string;
|
|
4162
|
+
password: string;
|
|
4163
|
+
host: string;
|
|
4164
|
+
port: number;
|
|
4165
|
+
};
|
|
4166
|
+
|
|
4167
|
+
const databaseConfigPromises: Record<string, Promise<DatabaseConfig>> = {};
|
|
4168
|
+
|
|
4169
|
+
const getSecretValue = async (secretArn: string): Promise<string> => {
|
|
4170
|
+
const client = new SecretsManagerClient();
|
|
4171
|
+
const data = await client.send(
|
|
4172
|
+
new GetSecretValueCommand({
|
|
4173
|
+
SecretId: secretArn,
|
|
4174
|
+
}),
|
|
4175
|
+
);
|
|
4176
|
+
|
|
4177
|
+
if (!data.SecretString) {
|
|
4178
|
+
throw new Error('Database secret does not contain SecretString.');
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
return data.SecretString;
|
|
4182
|
+
};
|
|
4183
|
+
|
|
4184
|
+
export const getDatabaseSecret = async (): Promise<DatabaseSecret> => {
|
|
4185
|
+
const secretArn = process.env.DATABASE_SECRET_ARN;
|
|
4186
|
+
|
|
4187
|
+
if (!secretArn) {
|
|
4188
|
+
throw new Error(
|
|
4189
|
+
'Missing required environment variable DATABASE_SECRET_ARN.',
|
|
4190
|
+
);
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
return JSON.parse(await getSecretValue(secretArn)) as DatabaseSecret;
|
|
4194
|
+
};
|
|
4195
|
+
|
|
4196
|
+
const loadDatabaseConfig = async (
|
|
4197
|
+
runtimeConfigKey: string,
|
|
4198
|
+
): Promise<DatabaseConfig> => {
|
|
4199
|
+
const appId = process.env.RUNTIME_CONFIG_APP_ID;
|
|
4200
|
+
|
|
4201
|
+
if (!appId) {
|
|
4202
|
+
throw new Error(
|
|
4203
|
+
'Missing required environment variable RUNTIME_CONFIG_APP_ID.',
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
const config = await getAppConfig<{
|
|
4208
|
+
[key: string]: DatabaseConfig | undefined;
|
|
4209
|
+
}>('database', {
|
|
4210
|
+
application: appId,
|
|
4211
|
+
environment: 'default',
|
|
4212
|
+
transform: 'json',
|
|
4213
|
+
});
|
|
4214
|
+
|
|
4215
|
+
const databaseConfig = config?.[runtimeConfigKey];
|
|
4216
|
+
|
|
4217
|
+
if (!databaseConfig) {
|
|
4218
|
+
throw new Error(\`RuntimeConfig is missing database.\${runtimeConfigKey}.\`);
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
return databaseConfig;
|
|
4222
|
+
};
|
|
4223
|
+
|
|
4224
|
+
export const getDatabaseConfig = (
|
|
4225
|
+
runtimeConfigKey: string,
|
|
4226
|
+
): Promise<DatabaseConfig> => {
|
|
4227
|
+
databaseConfigPromises[runtimeConfigKey] ??=
|
|
4228
|
+
loadDatabaseConfig(runtimeConfigKey);
|
|
4229
|
+
return databaseConfigPromises[runtimeConfigKey];
|
|
4230
|
+
};
|
|
4231
|
+
|
|
4232
|
+
// Aurora's writer endpoint is briefly unreachable from within the VPC right
|
|
4233
|
+
// after the cluster reports ready, and IAM policy attachments can take tens
|
|
4234
|
+
// of seconds to propagate before RDS accepts an IAM auth token. Both surface
|
|
4235
|
+
// as errors that resolve on retry.
|
|
4236
|
+
const transientErrorPatterns = [
|
|
4237
|
+
'ETIMEDOUT',
|
|
4238
|
+
'ECONNREFUSED',
|
|
4239
|
+
'ENOTFOUND',
|
|
4240
|
+
'P1000', // Prisma: authentication failed
|
|
4241
|
+
'ER_ACCESS_DENIED_ERROR', // MySQL: access denied
|
|
4242
|
+
];
|
|
4243
|
+
|
|
4244
|
+
const asString = (value: unknown): string => {
|
|
4245
|
+
if (value == null) return '';
|
|
4246
|
+
if (typeof value === 'string') return value;
|
|
4247
|
+
if (Buffer.isBuffer(value)) return value.toString('utf-8');
|
|
4248
|
+
return String(value);
|
|
4249
|
+
};
|
|
4250
|
+
|
|
4251
|
+
export const isTransientConnectionError = (error: unknown): boolean => {
|
|
4252
|
+
const err = error as {
|
|
4253
|
+
message?: unknown;
|
|
4254
|
+
code?: unknown;
|
|
4255
|
+
stderr?: unknown;
|
|
4256
|
+
stdout?: unknown;
|
|
4257
|
+
};
|
|
4258
|
+
const haystack = [
|
|
4259
|
+
error instanceof Error ? error.message : asString(err?.message),
|
|
4260
|
+
asString(err?.code),
|
|
4261
|
+
asString(err?.stderr),
|
|
4262
|
+
asString(err?.stdout),
|
|
4263
|
+
].join('\\n');
|
|
4264
|
+
return transientErrorPatterns.some((p) => haystack.includes(p));
|
|
4265
|
+
};
|
|
4266
|
+
|
|
4267
|
+
export const withConnectionRetry = async <T>(
|
|
4268
|
+
fn: () => Promise<T>,
|
|
4269
|
+
{
|
|
4270
|
+
maxAttempts = 6,
|
|
4271
|
+
delayMs = 10_000,
|
|
4272
|
+
}: { maxAttempts?: number; delayMs?: number } = {},
|
|
4273
|
+
): Promise<T> => {
|
|
4274
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
4275
|
+
try {
|
|
4276
|
+
return await fn();
|
|
4277
|
+
} catch (error) {
|
|
4278
|
+
if (attempt === maxAttempts || !isTransientConnectionError(error)) {
|
|
4279
|
+
throw error;
|
|
4280
|
+
}
|
|
4281
|
+
const wait = delayMs * attempt;
|
|
4282
|
+
console.log(
|
|
4283
|
+
\`Transient connection error (attempt \${attempt}/\${maxAttempts}), retrying in \${wait}ms: \${
|
|
4284
|
+
error instanceof Error ? error.message : String(error)
|
|
4285
|
+
}\`,
|
|
4286
|
+
);
|
|
4287
|
+
await new Promise((resolve) => setTimeout(resolve, wait));
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
throw new Error('unreachable');
|
|
4291
|
+
};
|
|
4292
|
+
"
|
|
4293
|
+
`;
|
|
4294
|
+
|
|
4295
|
+
exports[`ts#rdb generator > should generate the aurora shared construct 1`] = `
|
|
4296
|
+
"import { CfnOutput, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib';
|
|
4297
|
+
import { CustomResource } from 'aws-cdk-lib';
|
|
4298
|
+
import { IConnectable, IVpc, Port, SubnetType } from 'aws-cdk-lib/aws-ec2';
|
|
4299
|
+
import { IGrantable, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
|
|
4300
|
+
import { Key } from 'aws-cdk-lib/aws-kms';
|
|
4301
|
+
import { Platform } from 'aws-cdk-lib/aws-ecr-assets';
|
|
4302
|
+
import {
|
|
4303
|
+
Architecture,
|
|
4304
|
+
Code,
|
|
4305
|
+
DockerImageCode,
|
|
4306
|
+
DockerImageFunction,
|
|
4307
|
+
Function,
|
|
4308
|
+
Runtime,
|
|
4309
|
+
Tracing,
|
|
4310
|
+
} from 'aws-cdk-lib/aws-lambda';
|
|
4311
|
+
import {
|
|
4312
|
+
AuroraMysqlEngineVersion,
|
|
4313
|
+
AuroraPostgresEngineVersion,
|
|
4314
|
+
ClusterInstance,
|
|
4315
|
+
Credentials,
|
|
4316
|
+
DatabaseCluster,
|
|
4317
|
+
DatabaseClusterEngine,
|
|
4318
|
+
DatabaseClusterProps,
|
|
4319
|
+
DatabaseProxy,
|
|
4320
|
+
DefaultAuthScheme,
|
|
4321
|
+
IClusterEngine,
|
|
4322
|
+
} from 'aws-cdk-lib/aws-rds';
|
|
4323
|
+
import { Provider } from 'aws-cdk-lib/custom-resources';
|
|
4324
|
+
import { Trigger } from 'aws-cdk-lib/triggers';
|
|
4325
|
+
import { Construct } from 'constructs';
|
|
4326
|
+
import { RuntimeConfig } from '../runtime-config.js';
|
|
4327
|
+
|
|
4328
|
+
export type AuroraDatabaseEngineVersion =
|
|
4329
|
+
| AuroraMysqlEngineVersion
|
|
4330
|
+
| AuroraPostgresEngineVersion;
|
|
4331
|
+
|
|
4332
|
+
export interface AuroraDatabaseEngine {
|
|
4333
|
+
/**
|
|
4334
|
+
* Discriminant identifying the engine type.
|
|
4335
|
+
*/
|
|
4336
|
+
readonly type: 'mysql' | 'postgres';
|
|
4337
|
+
|
|
4338
|
+
/**
|
|
4339
|
+
* Default Aurora engine version used when one is not explicitly provided.
|
|
4340
|
+
*/
|
|
4341
|
+
readonly defaultVersion: AuroraDatabaseEngineVersion;
|
|
4342
|
+
|
|
4343
|
+
/**
|
|
4344
|
+
* Builds the CDK cluster engine for the provided engine version.
|
|
4345
|
+
*/
|
|
4346
|
+
clusterEngine(version: AuroraDatabaseEngineVersion): IClusterEngine;
|
|
4347
|
+
}
|
|
4348
|
+
|
|
4349
|
+
export class AuroraDatabaseEngines {
|
|
4350
|
+
public static mysql({
|
|
4351
|
+
defaultVersion = AuroraMysqlEngineVersion.VER_3_12_0,
|
|
4352
|
+
}: {
|
|
4353
|
+
defaultVersion?: AuroraMysqlEngineVersion;
|
|
4354
|
+
}): AuroraDatabaseEngine {
|
|
4355
|
+
return {
|
|
4356
|
+
type: 'mysql',
|
|
4357
|
+
defaultVersion,
|
|
4358
|
+
clusterEngine: (version) =>
|
|
4359
|
+
DatabaseClusterEngine.auroraMysql({
|
|
4360
|
+
version: version as AuroraMysqlEngineVersion,
|
|
4361
|
+
}),
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
public static postgres({
|
|
4366
|
+
defaultVersion = AuroraPostgresEngineVersion.VER_17_7,
|
|
4367
|
+
}: {
|
|
4368
|
+
defaultVersion?: AuroraPostgresEngineVersion;
|
|
4369
|
+
}): AuroraDatabaseEngine {
|
|
4370
|
+
return {
|
|
4371
|
+
type: 'postgres',
|
|
4372
|
+
defaultVersion,
|
|
4373
|
+
clusterEngine: (version) =>
|
|
4374
|
+
DatabaseClusterEngine.auroraPostgres({
|
|
4375
|
+
version: version as AuroraPostgresEngineVersion,
|
|
4376
|
+
}),
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
}
|
|
4380
|
+
|
|
4381
|
+
type _AuroraDatabaseProps = Omit<
|
|
4382
|
+
DatabaseClusterProps,
|
|
4383
|
+
| 'credentials'
|
|
4384
|
+
| 'defaultDatabaseName'
|
|
4385
|
+
| 'engine'
|
|
4386
|
+
| 'iamAuthentication'
|
|
4387
|
+
| 'instanceProps'
|
|
4388
|
+
| 'writer'
|
|
4389
|
+
>;
|
|
4390
|
+
|
|
4391
|
+
export interface AuroraDatabaseProps extends _AuroraDatabaseProps {
|
|
4392
|
+
/**
|
|
4393
|
+
* VPC where the Aurora cluster will be deployed.
|
|
4394
|
+
*/
|
|
4395
|
+
readonly vpc: IVpc;
|
|
4396
|
+
|
|
4397
|
+
/**
|
|
4398
|
+
* Aurora engine preset used to build the cluster.
|
|
4399
|
+
*/
|
|
4400
|
+
readonly engine: AuroraDatabaseEngine;
|
|
4401
|
+
|
|
4402
|
+
/**
|
|
4403
|
+
* The engine version to deploy.
|
|
4404
|
+
*
|
|
4405
|
+
* @default - engine.defaultVersion
|
|
4406
|
+
*/
|
|
4407
|
+
readonly engineVersion?: AuroraDatabaseEngineVersion;
|
|
4408
|
+
|
|
4409
|
+
/**
|
|
4410
|
+
* Admin username used when generating credentials.
|
|
4411
|
+
*/
|
|
4412
|
+
readonly adminUser: string;
|
|
4413
|
+
|
|
4414
|
+
/**
|
|
4415
|
+
* The initial database created in the cluster.
|
|
4416
|
+
*/
|
|
4417
|
+
readonly databaseName: string;
|
|
4418
|
+
|
|
4419
|
+
/**
|
|
4420
|
+
* RuntimeConfig key used under the \`database\` namespace.
|
|
4421
|
+
*/
|
|
4422
|
+
readonly runtimeConfigKey: string;
|
|
4423
|
+
|
|
4424
|
+
/**
|
|
4425
|
+
* Migration bundle used to create a migration handler Lambda.
|
|
4426
|
+
*/
|
|
4427
|
+
readonly migrationBundleDir: string;
|
|
4428
|
+
|
|
4429
|
+
/**
|
|
4430
|
+
* Bundle used to create or reconcile the application database user.
|
|
4431
|
+
*/
|
|
4432
|
+
readonly createDbUserBundleDir: string;
|
|
4433
|
+
|
|
4434
|
+
/**
|
|
4435
|
+
* Writer instance for the Aurora cluster.
|
|
4436
|
+
*/
|
|
4437
|
+
readonly writer?: DatabaseClusterProps['writer'];
|
|
4438
|
+
|
|
4439
|
+
/**
|
|
4440
|
+
* Whether to provision an RDS Proxy in front of the Aurora cluster.
|
|
4441
|
+
*
|
|
4442
|
+
* @default true
|
|
4443
|
+
*/
|
|
4444
|
+
readonly enableRdsProxy?: boolean;
|
|
4445
|
+
|
|
4446
|
+
/**
|
|
4447
|
+
* Whether to enable automatic credential rotation for the admin secret.
|
|
4448
|
+
*
|
|
4449
|
+
* @default true
|
|
4450
|
+
*/
|
|
4451
|
+
readonly enableCredentialRotation?: boolean;
|
|
4452
|
+
|
|
4453
|
+
/**
|
|
4454
|
+
* Whether to enable deletion protection on the Aurora cluster.
|
|
4455
|
+
*
|
|
4456
|
+
* @default true
|
|
4457
|
+
*/
|
|
4458
|
+
readonly deletionProtection?: boolean;
|
|
4459
|
+
|
|
4460
|
+
/**
|
|
4461
|
+
* Removal policy applied to the Aurora cluster.
|
|
4462
|
+
*
|
|
4463
|
+
* @default RemovalPolicy.RETAIN
|
|
4464
|
+
*/
|
|
4465
|
+
readonly removalPolicy?: RemovalPolicy;
|
|
4466
|
+
|
|
4467
|
+
/**
|
|
4468
|
+
* Whether to enable automatic key rotation on the KMS key used to encrypt the Aurora cluster and its credentials secret.
|
|
4469
|
+
*
|
|
4470
|
+
* @default true
|
|
4471
|
+
*/
|
|
4472
|
+
readonly enableKeyRotation?: boolean;
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
/**
|
|
4476
|
+
* Reusable Aurora database construct that supports different Aurora engines
|
|
4477
|
+
* through typed engine presets.
|
|
4478
|
+
*/
|
|
4479
|
+
export abstract class AuroraDatabase extends Construct {
|
|
4480
|
+
public readonly cluster: DatabaseCluster;
|
|
4481
|
+
public readonly proxy?: DatabaseProxy;
|
|
4482
|
+
private readonly adminUser: string;
|
|
4483
|
+
private readonly dbUser: string;
|
|
4484
|
+
|
|
4485
|
+
constructor(
|
|
4486
|
+
scope: Construct,
|
|
4487
|
+
id: string,
|
|
4488
|
+
{
|
|
4489
|
+
vpc,
|
|
4490
|
+
adminUser,
|
|
4491
|
+
databaseName,
|
|
4492
|
+
runtimeConfigKey,
|
|
4493
|
+
migrationBundleDir,
|
|
4494
|
+
createDbUserBundleDir,
|
|
4495
|
+
vpcSubnets,
|
|
4496
|
+
writer,
|
|
4497
|
+
enableRdsProxy = true,
|
|
4498
|
+
enableCredentialRotation = true,
|
|
4499
|
+
deletionProtection = true,
|
|
4500
|
+
removalPolicy = RemovalPolicy.RETAIN,
|
|
4501
|
+
enableKeyRotation = true,
|
|
4502
|
+
engine,
|
|
4503
|
+
engineVersion,
|
|
4504
|
+
...clusterProps
|
|
4505
|
+
}: AuroraDatabaseProps,
|
|
4506
|
+
) {
|
|
4507
|
+
super(scope, id);
|
|
4508
|
+
|
|
4509
|
+
this.adminUser = adminUser;
|
|
4510
|
+
|
|
4511
|
+
const key = new Key(this, 'EncryptionKey', { enableKeyRotation });
|
|
4512
|
+
this.cluster = new DatabaseCluster(this, 'DatabaseCluster', {
|
|
4513
|
+
...clusterProps,
|
|
4514
|
+
vpc,
|
|
4515
|
+
vpcSubnets,
|
|
4516
|
+
engine: engine.clusterEngine(engineVersion ?? engine.defaultVersion),
|
|
4517
|
+
writer: writer ?? ClusterInstance.serverlessV2('writer'),
|
|
4518
|
+
credentials: Credentials.fromGeneratedSecret(adminUser, {
|
|
4519
|
+
encryptionKey: key,
|
|
4520
|
+
}),
|
|
4521
|
+
iamAuthentication: true,
|
|
4522
|
+
monitoringInterval: Duration.seconds(5),
|
|
4523
|
+
defaultDatabaseName: databaseName,
|
|
4524
|
+
storageEncrypted: true,
|
|
4525
|
+
storageEncryptionKey: key,
|
|
4526
|
+
deletionProtection,
|
|
4527
|
+
removalPolicy,
|
|
4528
|
+
});
|
|
4529
|
+
|
|
4530
|
+
if (enableCredentialRotation) {
|
|
4531
|
+
this.cluster.addRotationSingleUser({
|
|
4532
|
+
automaticallyAfter: Duration.days(30),
|
|
4533
|
+
vpcSubnets: {
|
|
4534
|
+
subnetType: SubnetType.PRIVATE_WITH_EGRESS,
|
|
4535
|
+
},
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
const proxyRole = enableRdsProxy
|
|
4540
|
+
? new Role(this, 'DatabaseProxyRole', {
|
|
4541
|
+
assumedBy: new ServicePrincipal('rds.amazonaws.com'),
|
|
4542
|
+
})
|
|
4543
|
+
: undefined;
|
|
4544
|
+
|
|
4545
|
+
if (enableRdsProxy) {
|
|
4546
|
+
this.proxy = this.cluster.addProxy('DatabaseProxy', {
|
|
4547
|
+
vpc,
|
|
4548
|
+
vpcSubnets,
|
|
4549
|
+
defaultAuthScheme: DefaultAuthScheme.IAM_AUTH,
|
|
4550
|
+
role: proxyRole,
|
|
4551
|
+
});
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
const databaseHostname =
|
|
4555
|
+
this.proxy?.endpoint ?? this.cluster.clusterEndpoint.hostname;
|
|
4556
|
+
const databasePort = this.cluster.clusterEndpoint.port;
|
|
4557
|
+
|
|
4558
|
+
const createDbUserHandler = new Function(this, 'CreateDbUserHandler', {
|
|
4559
|
+
code: Code.fromAsset(createDbUserBundleDir),
|
|
4560
|
+
handler: 'index.handler',
|
|
4561
|
+
runtime: Runtime.NODEJS_LATEST,
|
|
4562
|
+
timeout: Duration.minutes(5),
|
|
4563
|
+
tracing: Tracing.ACTIVE,
|
|
4564
|
+
vpc,
|
|
4565
|
+
vpcSubnets: {
|
|
4566
|
+
subnetType: SubnetType.PRIVATE_WITH_EGRESS,
|
|
4567
|
+
},
|
|
4568
|
+
environment: {
|
|
4569
|
+
DATABASE_SECRET_ARN: this.cluster.secret!.secretArn,
|
|
4570
|
+
NODE_EXTRA_CA_CERTS: '/var/runtime/ca-cert.pem',
|
|
4571
|
+
},
|
|
4572
|
+
architecture: Architecture.ARM_64,
|
|
4573
|
+
});
|
|
4574
|
+
|
|
4575
|
+
this.cluster.connections.allowDefaultPortFrom(
|
|
4576
|
+
createDbUserHandler,
|
|
4577
|
+
'Allow the create-db-user handler to connect to the database',
|
|
4578
|
+
);
|
|
4579
|
+
this.grantSecretRead(createDbUserHandler);
|
|
4580
|
+
|
|
4581
|
+
const createDbUserProvider = new Provider(this, 'CreateDbUserProvider', {
|
|
4582
|
+
onEventHandler: createDbUserHandler,
|
|
4583
|
+
});
|
|
4584
|
+
|
|
4585
|
+
const createDbUserResource = new CustomResource(
|
|
4586
|
+
this,
|
|
4587
|
+
'CreateDbUserResource',
|
|
4588
|
+
{
|
|
4589
|
+
serviceToken: createDbUserProvider.serviceToken,
|
|
4590
|
+
properties: {
|
|
4591
|
+
clusterIdentifier: this.cluster.clusterIdentifier,
|
|
4592
|
+
},
|
|
4593
|
+
},
|
|
4594
|
+
);
|
|
4595
|
+
createDbUserResource.node.addDependency(this.cluster);
|
|
4596
|
+
this.dbUser = createDbUserResource.getAttString('dbUser');
|
|
4597
|
+
if (this.proxy && proxyRole) {
|
|
4598
|
+
this.cluster.grantConnect(proxyRole, this.dbUser);
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4601
|
+
const rc = RuntimeConfig.ensure(this);
|
|
4602
|
+
rc.set('database', runtimeConfigKey, {
|
|
4603
|
+
hostname: databaseHostname,
|
|
4604
|
+
port: databasePort,
|
|
4605
|
+
database: databaseName,
|
|
4606
|
+
adminUser: this.adminUser,
|
|
4607
|
+
dbUser: this.dbUser,
|
|
4608
|
+
region: Stack.of(this).region,
|
|
4609
|
+
});
|
|
4610
|
+
|
|
4611
|
+
const migrationHandler = new DockerImageFunction(this, 'MigrationHandler', {
|
|
4612
|
+
code: DockerImageCode.fromImageAsset(migrationBundleDir, {
|
|
4613
|
+
platform: Platform.LINUX_ARM64,
|
|
4614
|
+
}),
|
|
4615
|
+
memorySize: 1024,
|
|
4616
|
+
timeout: Duration.minutes(5),
|
|
4617
|
+
tracing: Tracing.ACTIVE,
|
|
4618
|
+
vpc,
|
|
4619
|
+
environment:
|
|
4620
|
+
engine.type === 'mysql'
|
|
4621
|
+
? {
|
|
4622
|
+
DATABASE_SECRET_ARN: this.cluster.secret!.secretArn,
|
|
4623
|
+
}
|
|
4624
|
+
: {
|
|
4625
|
+
HOSTNAME: databaseHostname,
|
|
4626
|
+
DATABASE: databaseName,
|
|
4627
|
+
PORT: databasePort.toString(),
|
|
4628
|
+
DBUSER: this.dbUser,
|
|
4629
|
+
},
|
|
4630
|
+
architecture: Architecture.ARM_64,
|
|
4631
|
+
});
|
|
4632
|
+
|
|
4633
|
+
if (engine.type === 'mysql') {
|
|
4634
|
+
this.cluster.connections.allowDefaultPortFrom(migrationHandler);
|
|
4635
|
+
this.grantSecretRead(migrationHandler);
|
|
4636
|
+
} else {
|
|
4637
|
+
this.allowDefaultPortFrom(migrationHandler);
|
|
4638
|
+
this.grantConnect(migrationHandler);
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
const trigger = new Trigger(this, 'MigrationTrigger', {
|
|
4642
|
+
handler: migrationHandler,
|
|
4643
|
+
});
|
|
4644
|
+
trigger.node.addDependency(createDbUserResource);
|
|
4645
|
+
|
|
4646
|
+
new CfnOutput(this, 'ClusterEndpoint', {
|
|
4647
|
+
value: this.cluster.clusterEndpoint.hostname,
|
|
4648
|
+
});
|
|
4649
|
+
|
|
4650
|
+
if (this.proxy?.endpoint) {
|
|
4651
|
+
new CfnOutput(this, 'ProxyEndpoint', {
|
|
4652
|
+
value: this.proxy?.endpoint,
|
|
4653
|
+
});
|
|
4654
|
+
}
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4657
|
+
public allowDefaultPortFrom(other: IConnectable, description?: string): void {
|
|
4658
|
+
if (this.proxy) {
|
|
4659
|
+
this.proxy.connections.allowFrom(
|
|
4660
|
+
other,
|
|
4661
|
+
Port.tcp(this.cluster.clusterEndpoint.port),
|
|
4662
|
+
description,
|
|
4663
|
+
);
|
|
4664
|
+
} else {
|
|
4665
|
+
this.cluster.connections.allowDefaultPortFrom(other, description);
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
|
|
4669
|
+
public grantSecretRead(grantee: IGrantable) {
|
|
4670
|
+
return this.cluster.secret!.grantRead(grantee);
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4673
|
+
public grantConnect(grantee: IGrantable) {
|
|
4674
|
+
return this.proxy
|
|
4675
|
+
? this.proxy.grantConnect(grantee, this.dbUser)
|
|
4676
|
+
: this.cluster.grantConnect(grantee, this.dbUser);
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
"
|
|
4680
|
+
`;
|
|
4681
|
+
|
|
4682
|
+
exports[`ts#rdb generator > should generate the aurora shared construct 2`] = `
|
|
4683
|
+
"import * as path from 'path';
|
|
4684
|
+
import * as url from 'url';
|
|
4685
|
+
import { Construct } from 'constructs';
|
|
4686
|
+
import { DB_PACKAGE_NAME } from ':proj/db';
|
|
4687
|
+
import {
|
|
4688
|
+
AuroraDatabase,
|
|
4689
|
+
AuroraDatabaseEngines,
|
|
4690
|
+
AuroraDatabaseProps,
|
|
4691
|
+
} from '../../core/rdb/aurora.js';
|
|
4692
|
+
import { findWorkspaceRoot } from '../../core/workspace.js';
|
|
4693
|
+
|
|
4694
|
+
export type DbProps = Omit<
|
|
4695
|
+
AuroraDatabaseProps,
|
|
4696
|
+
| 'databaseName'
|
|
4697
|
+
| 'adminUser'
|
|
4698
|
+
| 'createDbUserBundleDir'
|
|
4699
|
+
| 'engine'
|
|
4700
|
+
| 'runtimeConfigKey'
|
|
4701
|
+
| 'migrationBundleDir'
|
|
4702
|
+
>;
|
|
4703
|
+
|
|
4704
|
+
/**
|
|
4705
|
+
* CDK construct that provisions an Aurora Serverless v2 cluster.
|
|
4706
|
+
*/
|
|
4707
|
+
export class Db extends AuroraDatabase {
|
|
4708
|
+
constructor(scope: Construct, id: string, props: DbProps) {
|
|
4709
|
+
super(scope, id, {
|
|
4710
|
+
...props,
|
|
4711
|
+
databaseName: 'database_name',
|
|
4712
|
+
adminUser: 'databaseUser',
|
|
4713
|
+
runtimeConfigKey: DB_PACKAGE_NAME,
|
|
4714
|
+
migrationBundleDir: path.join(
|
|
4715
|
+
findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
|
|
4716
|
+
'dist/packages/db/bundle/migration',
|
|
4717
|
+
),
|
|
4718
|
+
createDbUserBundleDir: path.join(
|
|
4719
|
+
findWorkspaceRoot(url.fileURLToPath(new URL(import.meta.url))),
|
|
4720
|
+
'dist/packages/db/bundle/create-db-user',
|
|
4721
|
+
),
|
|
4722
|
+
engine: AuroraDatabaseEngines.postgres({}),
|
|
4723
|
+
});
|
|
4724
|
+
}
|
|
4725
|
+
}
|
|
4726
|
+
"
|
|
4727
|
+
`;
|
|
4728
|
+
|
|
4729
|
+
exports[`ts#rdb generator > should generate the aurora shared construct 3`] = `
|
|
4730
|
+
"import { defineConfig } from 'prisma/config';
|
|
4731
|
+
|
|
4732
|
+
const getDatabaseUrl = async () => {
|
|
4733
|
+
if (process.env.SERVE_LOCAL === 'true') {
|
|
4734
|
+
const {
|
|
4735
|
+
LOCAL_DB_HOST,
|
|
4736
|
+
LOCAL_DB_NAME,
|
|
4737
|
+
LOCAL_DB_PASSWORD,
|
|
4738
|
+
LOCAL_DB_PORT,
|
|
4739
|
+
LOCAL_DB_USER,
|
|
4740
|
+
} = await import('./src/constants.js');
|
|
4741
|
+
return \`postgresql://\${LOCAL_DB_USER}:\${LOCAL_DB_PASSWORD}@\${LOCAL_DB_HOST}:\${LOCAL_DB_PORT}/\${LOCAL_DB_NAME}\`;
|
|
4742
|
+
}
|
|
4743
|
+
return process.env.DATABASE_URL;
|
|
4744
|
+
};
|
|
4745
|
+
|
|
4746
|
+
export default defineConfig({
|
|
4747
|
+
schema: 'prisma/',
|
|
4748
|
+
migrations: {
|
|
4749
|
+
path: 'prisma/migrations',
|
|
4750
|
+
},
|
|
4751
|
+
datasource: {
|
|
4752
|
+
url: await getDatabaseUrl(),
|
|
4753
|
+
},
|
|
4754
|
+
});
|
|
4755
|
+
"
|
|
4756
|
+
`;
|
|
4757
|
+
|
|
4758
|
+
exports[`ts#rdb generator > should generate the aurora shared construct 4`] = `
|
|
4759
|
+
"FROM public.ecr.aws/lambda/nodejs:24
|
|
4760
|
+
|
|
4761
|
+
WORKDIR \${LAMBDA_TASK_ROOT}
|
|
4762
|
+
|
|
4763
|
+
RUN npm install prisma@7.8.0
|
|
4764
|
+
|
|
4765
|
+
COPY index.js ./index.js
|
|
4766
|
+
COPY prisma ./prisma
|
|
4767
|
+
COPY prisma.config.ts ./prisma.config.ts
|
|
4768
|
+
|
|
4769
|
+
RUN curl -fsSL "https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem" \\
|
|
4770
|
+
-o /etc/pki/ca-trust/source/anchors/rds-bundle.pem && \\
|
|
4771
|
+
update-ca-trust
|
|
4772
|
+
|
|
4773
|
+
CMD ["index.handler"]
|
|
4774
|
+
"
|
|
4775
|
+
`;
|
|
4776
|
+
|
|
4777
|
+
exports[`ts#rdb generator > should generate the aurora shared construct 5`] = `
|
|
4778
|
+
"import { defineConfig } from 'rolldown';
|
|
4779
|
+
|
|
4780
|
+
export default defineConfig([
|
|
4781
|
+
{
|
|
4782
|
+
tsconfig: 'tsconfig.lib.json',
|
|
4783
|
+
input: 'src/migration-handler.ts',
|
|
4784
|
+
output: {
|
|
4785
|
+
file: '../../dist/packages/db/bundle/migration/index.js',
|
|
4786
|
+
format: 'cjs',
|
|
4787
|
+
inlineDynamicImports: true,
|
|
4788
|
+
},
|
|
4789
|
+
platform: 'node',
|
|
4790
|
+
},
|
|
4791
|
+
{
|
|
4792
|
+
tsconfig: 'tsconfig.lib.json',
|
|
4793
|
+
input: 'src/create-db-user-handler.ts',
|
|
4794
|
+
output: {
|
|
4795
|
+
file: '../../dist/packages/db/bundle/create-db-user/index.js',
|
|
4796
|
+
format: 'cjs',
|
|
4797
|
+
inlineDynamicImports: true,
|
|
4798
|
+
},
|
|
4799
|
+
platform: 'node',
|
|
4800
|
+
},
|
|
4801
|
+
]);
|
|
4802
|
+
"
|
|
4803
|
+
`;
|
|
4804
|
+
|
|
4805
|
+
exports[`ts#rdb generator > should generate the aurora shared construct 6`] = `
|
|
4806
|
+
"export * from './rdb/aurora.js';
|
|
4807
|
+
export * from './app.js';
|
|
4808
|
+
export * from './checkov.js';
|
|
4809
|
+
export * from './runtime-config.js';
|
|
4810
|
+
export * from './workspace.js';
|
|
4811
|
+
"
|
|
4812
|
+
`;
|
|
4813
|
+
|
|
4814
|
+
exports[`ts#rdb generator > should generate the aurora shared construct 7`] = `
|
|
4815
|
+
"export * from './dbs/index.js';
|
|
4816
|
+
"
|
|
4817
|
+
`;
|
|
4818
|
+
|
|
4819
|
+
exports[`ts#rdb generator > should generate the aurora shared construct 8`] = `
|
|
4820
|
+
"export * from './db.js';
|
|
4821
|
+
"
|
|
4822
|
+
`;
|