@aws/nx-plugin 1.0.0-rc.85 → 1.0.0-rc.86
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/migrations.json +1 -1
- package/package.json +1 -1
- package/src/py/rdb/__snapshots__/generator.spec.ts.snap +21 -9
- package/src/py/rdb/files/__name__/utils.py.template +17 -1
- package/src/ts/rdb/__snapshots__/generator.spec.ts.snap +128 -250
- package/src/ts/rdb/files/src/utils.ts.template +28 -1
- package/src/utils/rdb-constructs/files/terraform/app/dbs/__nameKebabCase__/__nameKebabCase__.tf.template +6 -8
- package/src/utils/rdb-constructs/files/terraform/core/rdb/aurora/aurora.tf.template +28 -116
package/migrations.json
CHANGED
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
"implementation": "./src/migrations/v1.0.0-rc.75/0001-ts-agent-session-id-middleware-extraction/migration"
|
|
219
219
|
},
|
|
220
220
|
"sync-vended-versions": {
|
|
221
|
-
"version": "1.0.0-rc.
|
|
221
|
+
"version": "1.0.0-rc.86",
|
|
222
222
|
"description": "Sync vended dependency versions and the tracked plugin version to those vended by this release",
|
|
223
223
|
"implementation": "./src/utils/version-upgrade-migration/migration"
|
|
224
224
|
}
|
package/package.json
CHANGED
|
@@ -294,12 +294,6 @@ variable "enable_rds_proxy" {
|
|
|
294
294
|
default = true
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
variable "enable_credential_rotation" {
|
|
298
|
-
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
299
|
-
type = bool
|
|
300
|
-
default = true
|
|
301
|
-
}
|
|
302
|
-
|
|
303
297
|
variable "enable_cloudwatch_logs" {
|
|
304
298
|
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL logs DDL statements only (log_statement=ddl); MySQL enables Advanced Auditing scoped to connections and DDL (server_audit_events=CONNECT,QUERY_DDL). Neither logs statement parameter values, to avoid leaking PII into log data."
|
|
305
299
|
type = bool
|
|
@@ -372,7 +366,6 @@ module "aurora" {
|
|
|
372
366
|
name = "db"
|
|
373
367
|
vpc_id = var.vpc_id
|
|
374
368
|
subnet_ids = var.database_subnet_ids
|
|
375
|
-
lambda_subnet_ids = var.lambda_subnet_ids
|
|
376
369
|
engine = "aurora-postgresql"
|
|
377
370
|
engine_version = var.engine_version
|
|
378
371
|
database_name = "database_name"
|
|
@@ -384,7 +377,6 @@ module "aurora" {
|
|
|
384
377
|
deletion_protection = var.deletion_protection
|
|
385
378
|
skip_final_snapshot = var.skip_final_snapshot
|
|
386
379
|
enable_rds_proxy = var.enable_rds_proxy
|
|
387
|
-
enable_credential_rotation = var.enable_credential_rotation
|
|
388
380
|
enable_cloudwatch_logs = var.enable_cloudwatch_logs
|
|
389
381
|
enable_performance_insights = var.enable_performance_insights
|
|
390
382
|
performance_insights_retention_period = var.performance_insights_retention_period
|
|
@@ -754,6 +746,9 @@ resource "aws_lambda_function" "create_db_user" {
|
|
|
754
746
|
environment {
|
|
755
747
|
variables = {
|
|
756
748
|
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
749
|
+
DATABASE_HOST = module.aurora.writer_endpoint
|
|
750
|
+
DATABASE_PORT = module.aurora.cluster_port
|
|
751
|
+
DATABASE_NAME = module.aurora.database_name
|
|
757
752
|
}
|
|
758
753
|
}
|
|
759
754
|
|
|
@@ -830,6 +825,9 @@ resource "aws_lambda_function" "migration_handler" {
|
|
|
830
825
|
environment {
|
|
831
826
|
variables = {
|
|
832
827
|
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
828
|
+
DATABASE_HOST = module.aurora.writer_endpoint
|
|
829
|
+
DATABASE_PORT = module.aurora.cluster_port
|
|
830
|
+
DATABASE_NAME = module.aurora.database_name
|
|
833
831
|
}
|
|
834
832
|
}
|
|
835
833
|
|
|
@@ -1309,12 +1307,26 @@ def get_local_dev_config() -> dict:
|
|
|
1309
1307
|
|
|
1310
1308
|
|
|
1311
1309
|
def get_database_secret() -> DatabaseSecret:
|
|
1310
|
+
"""Read the admin credentials secret.
|
|
1311
|
+
|
|
1312
|
+
RDS-managed secrets hold only \`username\` and \`password\`, so the host, port
|
|
1313
|
+
and database name are taken from the environment when the secret omits them.
|
|
1314
|
+
"""
|
|
1312
1315
|
secret_arn = os.environ.get("DATABASE_SECRET_ARN")
|
|
1313
1316
|
if not secret_arn:
|
|
1314
1317
|
raise ValueError("Missing required environment variable DATABASE_SECRET_ARN.")
|
|
1315
1318
|
client = boto3.client("secretsmanager")
|
|
1316
1319
|
response = client.get_secret_value(SecretId=secret_arn)
|
|
1317
|
-
|
|
1320
|
+
secret = json.loads(response["SecretString"])
|
|
1321
|
+
|
|
1322
|
+
for key, env_var in (("host", "DATABASE_HOST"), ("port", "DATABASE_PORT"), ("dbname", "DATABASE_NAME")):
|
|
1323
|
+
value = os.environ.get(env_var)
|
|
1324
|
+
if value:
|
|
1325
|
+
secret[key] = value
|
|
1326
|
+
if secret.get(key) is None:
|
|
1327
|
+
raise ValueError(f"Database secret is missing {key} and no override was provided in {env_var}.")
|
|
1328
|
+
|
|
1329
|
+
return secret
|
|
1318
1330
|
|
|
1319
1331
|
|
|
1320
1332
|
_database_config: dict[str, DatabaseConfig] = {}
|
|
@@ -39,12 +39,28 @@ def get_local_dev_config() -> dict:
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
def get_database_secret() -> DatabaseSecret:
|
|
42
|
+
"""Read the admin credentials secret.
|
|
43
|
+
|
|
44
|
+
RDS-managed secrets hold only `username` and `password`, so the host, port
|
|
45
|
+
and database name are taken from the environment when the secret omits them.
|
|
46
|
+
"""
|
|
42
47
|
secret_arn = os.environ.get('DATABASE_SECRET_ARN')
|
|
43
48
|
if not secret_arn:
|
|
44
49
|
raise ValueError('Missing required environment variable DATABASE_SECRET_ARN.')
|
|
45
50
|
client = boto3.client('secretsmanager')
|
|
46
51
|
response = client.get_secret_value(SecretId=secret_arn)
|
|
47
|
-
|
|
52
|
+
secret = json.loads(response['SecretString'])
|
|
53
|
+
|
|
54
|
+
for key, env_var in (('host', 'DATABASE_HOST'), ('port', 'DATABASE_PORT'), ('dbname', 'DATABASE_NAME')):
|
|
55
|
+
value = os.environ.get(env_var)
|
|
56
|
+
if value:
|
|
57
|
+
secret[key] = value
|
|
58
|
+
if secret.get(key) is None:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f'Database secret is missing {key} and no override was provided in {env_var}.'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return secret
|
|
48
64
|
|
|
49
65
|
|
|
50
66
|
_database_config: dict[str, DatabaseConfig] = {}
|
|
@@ -225,6 +225,24 @@ export type DatabaseSecret = {
|
|
|
225
225
|
port: number;
|
|
226
226
|
};
|
|
227
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Connection details for the admin credentials secret. RDS-managed secrets hold
|
|
230
|
+
* only \`username\` and \`password\`, so the host, port and database name are taken
|
|
231
|
+
* from the environment when the secret omits them.
|
|
232
|
+
*/
|
|
233
|
+
type SecretConnectionOverrides = Partial<
|
|
234
|
+
Omit<DatabaseSecret, 'username' | 'password'>
|
|
235
|
+
>;
|
|
236
|
+
|
|
237
|
+
const connectionOverridesFromEnv = (): SecretConnectionOverrides => {
|
|
238
|
+
const { DATABASE_HOST, DATABASE_PORT, DATABASE_NAME } = process.env;
|
|
239
|
+
return {
|
|
240
|
+
...(DATABASE_HOST ? { host: DATABASE_HOST } : {}),
|
|
241
|
+
...(DATABASE_PORT ? { port: Number(DATABASE_PORT) } : {}),
|
|
242
|
+
...(DATABASE_NAME ? { dbname: DATABASE_NAME } : {}),
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
|
|
228
246
|
const databaseConfigPromises: Record<string, Promise<DatabaseConfig>> = {};
|
|
229
247
|
|
|
230
248
|
const getSecretValue = async (secretArn: string): Promise<string> => {
|
|
@@ -251,7 +269,18 @@ export const getDatabaseSecret = async (): Promise<DatabaseSecret> => {
|
|
|
251
269
|
);
|
|
252
270
|
}
|
|
253
271
|
|
|
254
|
-
|
|
272
|
+
const secret = JSON.parse(await getSecretValue(secretArn)) as DatabaseSecret;
|
|
273
|
+
const secretWithConnection = { ...secret, ...connectionOverridesFromEnv() };
|
|
274
|
+
|
|
275
|
+
for (const key of ['host', 'port', 'dbname'] as const) {
|
|
276
|
+
if (secretWithConnection[key] == null) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
\`Database secret is missing \${key} and no override was provided in the environment.\`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return secretWithConnection;
|
|
255
284
|
};
|
|
256
285
|
|
|
257
286
|
const loadDatabaseConfig = async (
|
|
@@ -810,7 +839,8 @@ export default defineConfig({
|
|
|
810
839
|
|
|
811
840
|
exports[`ts#rdb generator > should generate terraform modules when iac is Terraform 1`] = `
|
|
812
841
|
"# Core Aurora module
|
|
813
|
-
# This module creates an Aurora cluster
|
|
842
|
+
# This module creates an Aurora cluster whose admin credentials are generated
|
|
843
|
+
# and rotated by RDS in Secrets Manager.
|
|
814
844
|
|
|
815
845
|
terraform {
|
|
816
846
|
required_version = ">= 1.0"
|
|
@@ -854,11 +884,6 @@ variable "subnet_ids" {
|
|
|
854
884
|
type = list(string)
|
|
855
885
|
}
|
|
856
886
|
|
|
857
|
-
variable "lambda_subnet_ids" {
|
|
858
|
-
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."
|
|
859
|
-
type = list(string)
|
|
860
|
-
}
|
|
861
|
-
|
|
862
887
|
variable "engine" {
|
|
863
888
|
description = "Aurora engine to use."
|
|
864
889
|
type = string
|
|
@@ -928,12 +953,6 @@ variable "enable_rds_proxy" {
|
|
|
928
953
|
default = true
|
|
929
954
|
}
|
|
930
955
|
|
|
931
|
-
variable "enable_credential_rotation" {
|
|
932
|
-
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
933
|
-
type = bool
|
|
934
|
-
default = true
|
|
935
|
-
}
|
|
936
|
-
|
|
937
956
|
variable "enable_cloudwatch_logs" {
|
|
938
957
|
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL logs DDL statements only (log_statement=ddl); MySQL enables Advanced Auditing scoped to connections and DDL (server_audit_events=CONNECT,QUERY_DDL). Neither logs statement parameter values, to avoid leaking PII into log data."
|
|
939
958
|
type = bool
|
|
@@ -1055,6 +1074,20 @@ resource "aws_kms_key" "database" {
|
|
|
1055
1074
|
"kms:CreateGrant"
|
|
1056
1075
|
]
|
|
1057
1076
|
Resource = "*"
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
Sid = "AllowSecretsManagerService"
|
|
1080
|
+
Effect = "Allow"
|
|
1081
|
+
Principal = {
|
|
1082
|
+
Service = "secretsmanager.amazonaws.com"
|
|
1083
|
+
}
|
|
1084
|
+
Action = [
|
|
1085
|
+
"kms:Encrypt",
|
|
1086
|
+
"kms:Decrypt",
|
|
1087
|
+
"kms:GenerateDataKey*",
|
|
1088
|
+
"kms:DescribeKey"
|
|
1089
|
+
]
|
|
1090
|
+
Resource = "*"
|
|
1058
1091
|
}
|
|
1059
1092
|
]
|
|
1060
1093
|
})
|
|
@@ -1109,35 +1142,6 @@ resource "aws_db_subnet_group" "database" {
|
|
|
1109
1142
|
})
|
|
1110
1143
|
}
|
|
1111
1144
|
|
|
1112
|
-
resource "random_password" "master_password" {
|
|
1113
|
-
length = 32
|
|
1114
|
-
special = true
|
|
1115
|
-
override_special = "!#$%&*()-_=+[]{}<>:?"
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
resource "aws_secretsmanager_secret" "credentials" {
|
|
1119
|
-
#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
|
|
1120
|
-
name_prefix = "\${var.name}-aurora-credentials-"
|
|
1121
|
-
kms_key_id = aws_kms_key.database.arn
|
|
1122
|
-
|
|
1123
|
-
tags = merge(var.tags, {
|
|
1124
|
-
Name = "\${var.name}-aurora-credentials"
|
|
1125
|
-
})
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
resource "aws_secretsmanager_secret_version" "credentials" {
|
|
1129
|
-
secret_id = aws_secretsmanager_secret.credentials.id
|
|
1130
|
-
secret_string = jsonencode({
|
|
1131
|
-
engine = var.engine == "aurora-mysql" ? "mysql" : "postgres"
|
|
1132
|
-
username = var.admin_user
|
|
1133
|
-
password = random_password.master_password.result
|
|
1134
|
-
host = aws_rds_cluster.database.endpoint
|
|
1135
|
-
port = aws_rds_cluster.database.port
|
|
1136
|
-
dbname = var.database_name
|
|
1137
|
-
dbClusterIdentifier = aws_rds_cluster.database.cluster_identifier
|
|
1138
|
-
})
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
1145
|
resource "aws_rds_cluster" "database" {
|
|
1142
1146
|
#checkov:skip=CKV2_AWS_27:Query logging is enabled by default via enable_cloudwatch_logs; checkov cannot resolve the conditional count expression on aws_rds_cluster_parameter_group.database
|
|
1143
1147
|
#checkov:skip=CKV_AWS_139:Deletion protection is enabled but checkov is unable to detect it correctly
|
|
@@ -1146,7 +1150,8 @@ resource "aws_rds_cluster" "database" {
|
|
|
1146
1150
|
engine_version = coalesce(var.engine_version, local.default_engine_version)
|
|
1147
1151
|
database_name = var.database_name
|
|
1148
1152
|
master_username = var.admin_user
|
|
1149
|
-
|
|
1153
|
+
manage_master_user_password = true
|
|
1154
|
+
master_user_secret_kms_key_id = aws_kms_key.database.arn
|
|
1150
1155
|
db_subnet_group_name = aws_db_subnet_group.database.name
|
|
1151
1156
|
db_cluster_parameter_group_name = var.enable_cloudwatch_logs ? aws_rds_cluster_parameter_group.database[0].name : null
|
|
1152
1157
|
vpc_security_group_ids = [aws_security_group.database.id]
|
|
@@ -1198,75 +1203,6 @@ resource "aws_rds_cluster_instance" "database" {
|
|
|
1198
1203
|
})
|
|
1199
1204
|
}
|
|
1200
1205
|
|
|
1201
|
-
resource "aws_security_group" "rotation" {
|
|
1202
|
-
#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
|
|
1203
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
1204
|
-
name_prefix = "\${var.name}-aurora-rotation-"
|
|
1205
|
-
description = "Security group for Aurora secret rotation \${var.name}"
|
|
1206
|
-
vpc_id = var.vpc_id
|
|
1207
|
-
|
|
1208
|
-
tags = merge(var.tags, {
|
|
1209
|
-
Name = "\${var.name}-aurora-rotation"
|
|
1210
|
-
})
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
resource "aws_vpc_security_group_egress_rule" "rotation_to_database" {
|
|
1214
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
1215
|
-
security_group_id = aws_security_group.rotation[0].id
|
|
1216
|
-
referenced_security_group_id = aws_security_group.database.id
|
|
1217
|
-
from_port = coalesce(var.port, local.default_port)
|
|
1218
|
-
to_port = coalesce(var.port, local.default_port)
|
|
1219
|
-
ip_protocol = "tcp"
|
|
1220
|
-
description = "Allow outbound database traffic from secret rotation Lambda"
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
resource "aws_vpc_security_group_ingress_rule" "rotation_to_database" {
|
|
1224
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
1225
|
-
security_group_id = aws_security_group.database.id
|
|
1226
|
-
referenced_security_group_id = aws_security_group.rotation[0].id
|
|
1227
|
-
from_port = coalesce(var.port, local.default_port)
|
|
1228
|
-
to_port = coalesce(var.port, local.default_port)
|
|
1229
|
-
ip_protocol = "tcp"
|
|
1230
|
-
description = "Allow inbound database traffic from secret rotation Lambda"
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
resource "aws_cloudformation_stack" "credentials_rotation" {
|
|
1234
|
-
#checkov:skip=CKV_AWS_124:SNS event notifications are not required for the credentials rotation stack
|
|
1235
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
1236
|
-
name = "\${local.cluster_name}-credentials-rotation"
|
|
1237
|
-
|
|
1238
|
-
capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"]
|
|
1239
|
-
|
|
1240
|
-
template_body = jsonencode({
|
|
1241
|
-
AWSTemplateFormatVersion = "2010-09-09"
|
|
1242
|
-
Transform = "AWS::SecretsManager-2024-09-16"
|
|
1243
|
-
Resources = {
|
|
1244
|
-
CredentialsRotationSchedule = {
|
|
1245
|
-
Type = "AWS::SecretsManager::RotationSchedule"
|
|
1246
|
-
Properties = {
|
|
1247
|
-
SecretId = aws_secretsmanager_secret.credentials.arn
|
|
1248
|
-
HostedRotationLambda = {
|
|
1249
|
-
RotationType = var.engine == "aurora-mysql" ? "MySQLSingleUser" : "PostgreSQLSingleUser"
|
|
1250
|
-
RotationLambdaName = "\${local.cluster_name}-credentials-rotation"
|
|
1251
|
-
VpcSecurityGroupIds = aws_security_group.rotation[0].id
|
|
1252
|
-
VpcSubnetIds = join(",", var.lambda_subnet_ids)
|
|
1253
|
-
}
|
|
1254
|
-
RotationRules = {
|
|
1255
|
-
AutomaticallyAfterDays = 30
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
})
|
|
1261
|
-
|
|
1262
|
-
depends_on = [
|
|
1263
|
-
aws_secretsmanager_secret_version.credentials,
|
|
1264
|
-
aws_rds_cluster_instance.database,
|
|
1265
|
-
aws_vpc_security_group_egress_rule.rotation_to_database,
|
|
1266
|
-
aws_vpc_security_group_ingress_rule.rotation_to_database,
|
|
1267
|
-
]
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
1206
|
resource "aws_iam_role" "proxy" {
|
|
1271
1207
|
count = var.enable_rds_proxy ? 1 : 0
|
|
1272
1208
|
|
|
@@ -1505,6 +1441,11 @@ output "cluster_endpoint" {
|
|
|
1505
1441
|
value = var.enable_rds_proxy ? aws_db_proxy.aurora[0].endpoint : aws_rds_cluster.database.endpoint
|
|
1506
1442
|
}
|
|
1507
1443
|
|
|
1444
|
+
output "writer_endpoint" {
|
|
1445
|
+
description = "Writer endpoint of the Aurora cluster itself, bypassing the RDS Proxy. Used by the admin-credentialled handlers, which connect directly to the cluster."
|
|
1446
|
+
value = aws_rds_cluster.database.endpoint
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1508
1449
|
output "reader_endpoint" {
|
|
1509
1450
|
description = "Reader endpoint of the Aurora cluster."
|
|
1510
1451
|
value = aws_rds_cluster.database.reader_endpoint
|
|
@@ -1531,12 +1472,12 @@ output "proxy_role_name" {
|
|
|
1531
1472
|
}
|
|
1532
1473
|
|
|
1533
1474
|
output "secret_arn" {
|
|
1534
|
-
description = "ARN of the
|
|
1535
|
-
value =
|
|
1475
|
+
description = "ARN of the RDS-managed admin credentials secret. Its value is a JSON object with username and password only — connection details come from this module's other outputs."
|
|
1476
|
+
value = one(aws_rds_cluster.database.master_user_secret[*].secret_arn)
|
|
1536
1477
|
}
|
|
1537
1478
|
|
|
1538
1479
|
output "kms_key_arn" {
|
|
1539
|
-
description = "ARN of the KMS key protecting Aurora and
|
|
1480
|
+
description = "ARN of the KMS key protecting Aurora and its admin credentials secret."
|
|
1540
1481
|
value = aws_kms_key.database.arn
|
|
1541
1482
|
}
|
|
1542
1483
|
|
|
@@ -1561,8 +1502,8 @@ output "database_name" {
|
|
|
1561
1502
|
}
|
|
1562
1503
|
|
|
1563
1504
|
output "admin_user" {
|
|
1564
|
-
description = "Admin username stored in the
|
|
1565
|
-
value =
|
|
1505
|
+
description = "Admin username stored in the RDS-managed credentials secret."
|
|
1506
|
+
value = aws_rds_cluster.database.master_username
|
|
1566
1507
|
}
|
|
1567
1508
|
"
|
|
1568
1509
|
`;
|
|
@@ -1658,12 +1599,6 @@ variable "enable_rds_proxy" {
|
|
|
1658
1599
|
default = true
|
|
1659
1600
|
}
|
|
1660
1601
|
|
|
1661
|
-
variable "enable_credential_rotation" {
|
|
1662
|
-
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
1663
|
-
type = bool
|
|
1664
|
-
default = true
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
1602
|
variable "enable_cloudwatch_logs" {
|
|
1668
1603
|
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL logs DDL statements only (log_statement=ddl); MySQL enables Advanced Auditing scoped to connections and DDL (server_audit_events=CONNECT,QUERY_DDL). Neither logs statement parameter values, to avoid leaking PII into log data."
|
|
1669
1604
|
type = bool
|
|
@@ -1736,7 +1671,6 @@ module "aurora" {
|
|
|
1736
1671
|
name = "db"
|
|
1737
1672
|
vpc_id = var.vpc_id
|
|
1738
1673
|
subnet_ids = var.database_subnet_ids
|
|
1739
|
-
lambda_subnet_ids = var.lambda_subnet_ids
|
|
1740
1674
|
engine = "aurora-postgresql"
|
|
1741
1675
|
engine_version = var.engine_version
|
|
1742
1676
|
database_name = "database_name"
|
|
@@ -1748,7 +1682,6 @@ module "aurora" {
|
|
|
1748
1682
|
deletion_protection = var.deletion_protection
|
|
1749
1683
|
skip_final_snapshot = var.skip_final_snapshot
|
|
1750
1684
|
enable_rds_proxy = var.enable_rds_proxy
|
|
1751
|
-
enable_credential_rotation = var.enable_credential_rotation
|
|
1752
1685
|
enable_cloudwatch_logs = var.enable_cloudwatch_logs
|
|
1753
1686
|
enable_performance_insights = var.enable_performance_insights
|
|
1754
1687
|
performance_insights_retention_period = var.performance_insights_retention_period
|
|
@@ -2117,6 +2050,9 @@ resource "aws_lambda_function" "create_db_user" {
|
|
|
2117
2050
|
environment {
|
|
2118
2051
|
variables = {
|
|
2119
2052
|
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
2053
|
+
DATABASE_HOST = module.aurora.writer_endpoint
|
|
2054
|
+
DATABASE_PORT = module.aurora.cluster_port
|
|
2055
|
+
DATABASE_NAME = module.aurora.database_name
|
|
2120
2056
|
NODE_EXTRA_CA_CERTS = "/var/runtime/ca-cert.pem"
|
|
2121
2057
|
}
|
|
2122
2058
|
}
|
|
@@ -2194,6 +2130,9 @@ resource "aws_lambda_function" "migration_handler" {
|
|
|
2194
2130
|
environment {
|
|
2195
2131
|
variables = {
|
|
2196
2132
|
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
2133
|
+
DATABASE_HOST = module.aurora.writer_endpoint
|
|
2134
|
+
DATABASE_PORT = module.aurora.cluster_port
|
|
2135
|
+
DATABASE_NAME = module.aurora.database_name
|
|
2197
2136
|
}
|
|
2198
2137
|
}
|
|
2199
2138
|
|
|
@@ -2313,7 +2252,8 @@ output "database_runtime_user" {
|
|
|
2313
2252
|
|
|
2314
2253
|
exports[`ts#rdb generator > should generate terraform modules with MySQL engine 1`] = `
|
|
2315
2254
|
"# Core Aurora module
|
|
2316
|
-
# This module creates an Aurora cluster
|
|
2255
|
+
# This module creates an Aurora cluster whose admin credentials are generated
|
|
2256
|
+
# and rotated by RDS in Secrets Manager.
|
|
2317
2257
|
|
|
2318
2258
|
terraform {
|
|
2319
2259
|
required_version = ">= 1.0"
|
|
@@ -2357,11 +2297,6 @@ variable "subnet_ids" {
|
|
|
2357
2297
|
type = list(string)
|
|
2358
2298
|
}
|
|
2359
2299
|
|
|
2360
|
-
variable "lambda_subnet_ids" {
|
|
2361
|
-
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."
|
|
2362
|
-
type = list(string)
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
2300
|
variable "engine" {
|
|
2366
2301
|
description = "Aurora engine to use."
|
|
2367
2302
|
type = string
|
|
@@ -2431,12 +2366,6 @@ variable "enable_rds_proxy" {
|
|
|
2431
2366
|
default = true
|
|
2432
2367
|
}
|
|
2433
2368
|
|
|
2434
|
-
variable "enable_credential_rotation" {
|
|
2435
|
-
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
2436
|
-
type = bool
|
|
2437
|
-
default = true
|
|
2438
|
-
}
|
|
2439
|
-
|
|
2440
2369
|
variable "enable_cloudwatch_logs" {
|
|
2441
2370
|
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL logs DDL statements only (log_statement=ddl); MySQL enables Advanced Auditing scoped to connections and DDL (server_audit_events=CONNECT,QUERY_DDL). Neither logs statement parameter values, to avoid leaking PII into log data."
|
|
2442
2371
|
type = bool
|
|
@@ -2558,6 +2487,20 @@ resource "aws_kms_key" "database" {
|
|
|
2558
2487
|
"kms:CreateGrant"
|
|
2559
2488
|
]
|
|
2560
2489
|
Resource = "*"
|
|
2490
|
+
},
|
|
2491
|
+
{
|
|
2492
|
+
Sid = "AllowSecretsManagerService"
|
|
2493
|
+
Effect = "Allow"
|
|
2494
|
+
Principal = {
|
|
2495
|
+
Service = "secretsmanager.amazonaws.com"
|
|
2496
|
+
}
|
|
2497
|
+
Action = [
|
|
2498
|
+
"kms:Encrypt",
|
|
2499
|
+
"kms:Decrypt",
|
|
2500
|
+
"kms:GenerateDataKey*",
|
|
2501
|
+
"kms:DescribeKey"
|
|
2502
|
+
]
|
|
2503
|
+
Resource = "*"
|
|
2561
2504
|
}
|
|
2562
2505
|
]
|
|
2563
2506
|
})
|
|
@@ -2612,35 +2555,6 @@ resource "aws_db_subnet_group" "database" {
|
|
|
2612
2555
|
})
|
|
2613
2556
|
}
|
|
2614
2557
|
|
|
2615
|
-
resource "random_password" "master_password" {
|
|
2616
|
-
length = 32
|
|
2617
|
-
special = true
|
|
2618
|
-
override_special = "!#$%&*()-_=+[]{}<>:?"
|
|
2619
|
-
}
|
|
2620
|
-
|
|
2621
|
-
resource "aws_secretsmanager_secret" "credentials" {
|
|
2622
|
-
#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
|
|
2623
|
-
name_prefix = "\${var.name}-aurora-credentials-"
|
|
2624
|
-
kms_key_id = aws_kms_key.database.arn
|
|
2625
|
-
|
|
2626
|
-
tags = merge(var.tags, {
|
|
2627
|
-
Name = "\${var.name}-aurora-credentials"
|
|
2628
|
-
})
|
|
2629
|
-
}
|
|
2630
|
-
|
|
2631
|
-
resource "aws_secretsmanager_secret_version" "credentials" {
|
|
2632
|
-
secret_id = aws_secretsmanager_secret.credentials.id
|
|
2633
|
-
secret_string = jsonencode({
|
|
2634
|
-
engine = var.engine == "aurora-mysql" ? "mysql" : "postgres"
|
|
2635
|
-
username = var.admin_user
|
|
2636
|
-
password = random_password.master_password.result
|
|
2637
|
-
host = aws_rds_cluster.database.endpoint
|
|
2638
|
-
port = aws_rds_cluster.database.port
|
|
2639
|
-
dbname = var.database_name
|
|
2640
|
-
dbClusterIdentifier = aws_rds_cluster.database.cluster_identifier
|
|
2641
|
-
})
|
|
2642
|
-
}
|
|
2643
|
-
|
|
2644
2558
|
resource "aws_rds_cluster" "database" {
|
|
2645
2559
|
#checkov:skip=CKV2_AWS_27:Query logging is enabled by default via enable_cloudwatch_logs; checkov cannot resolve the conditional count expression on aws_rds_cluster_parameter_group.database
|
|
2646
2560
|
#checkov:skip=CKV_AWS_139:Deletion protection is enabled but checkov is unable to detect it correctly
|
|
@@ -2649,7 +2563,8 @@ resource "aws_rds_cluster" "database" {
|
|
|
2649
2563
|
engine_version = coalesce(var.engine_version, local.default_engine_version)
|
|
2650
2564
|
database_name = var.database_name
|
|
2651
2565
|
master_username = var.admin_user
|
|
2652
|
-
|
|
2566
|
+
manage_master_user_password = true
|
|
2567
|
+
master_user_secret_kms_key_id = aws_kms_key.database.arn
|
|
2653
2568
|
db_subnet_group_name = aws_db_subnet_group.database.name
|
|
2654
2569
|
db_cluster_parameter_group_name = var.enable_cloudwatch_logs ? aws_rds_cluster_parameter_group.database[0].name : null
|
|
2655
2570
|
vpc_security_group_ids = [aws_security_group.database.id]
|
|
@@ -2701,75 +2616,6 @@ resource "aws_rds_cluster_instance" "database" {
|
|
|
2701
2616
|
})
|
|
2702
2617
|
}
|
|
2703
2618
|
|
|
2704
|
-
resource "aws_security_group" "rotation" {
|
|
2705
|
-
#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
|
|
2706
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
2707
|
-
name_prefix = "\${var.name}-aurora-rotation-"
|
|
2708
|
-
description = "Security group for Aurora secret rotation \${var.name}"
|
|
2709
|
-
vpc_id = var.vpc_id
|
|
2710
|
-
|
|
2711
|
-
tags = merge(var.tags, {
|
|
2712
|
-
Name = "\${var.name}-aurora-rotation"
|
|
2713
|
-
})
|
|
2714
|
-
}
|
|
2715
|
-
|
|
2716
|
-
resource "aws_vpc_security_group_egress_rule" "rotation_to_database" {
|
|
2717
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
2718
|
-
security_group_id = aws_security_group.rotation[0].id
|
|
2719
|
-
referenced_security_group_id = aws_security_group.database.id
|
|
2720
|
-
from_port = coalesce(var.port, local.default_port)
|
|
2721
|
-
to_port = coalesce(var.port, local.default_port)
|
|
2722
|
-
ip_protocol = "tcp"
|
|
2723
|
-
description = "Allow outbound database traffic from secret rotation Lambda"
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
resource "aws_vpc_security_group_ingress_rule" "rotation_to_database" {
|
|
2727
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
2728
|
-
security_group_id = aws_security_group.database.id
|
|
2729
|
-
referenced_security_group_id = aws_security_group.rotation[0].id
|
|
2730
|
-
from_port = coalesce(var.port, local.default_port)
|
|
2731
|
-
to_port = coalesce(var.port, local.default_port)
|
|
2732
|
-
ip_protocol = "tcp"
|
|
2733
|
-
description = "Allow inbound database traffic from secret rotation Lambda"
|
|
2734
|
-
}
|
|
2735
|
-
|
|
2736
|
-
resource "aws_cloudformation_stack" "credentials_rotation" {
|
|
2737
|
-
#checkov:skip=CKV_AWS_124:SNS event notifications are not required for the credentials rotation stack
|
|
2738
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
2739
|
-
name = "\${local.cluster_name}-credentials-rotation"
|
|
2740
|
-
|
|
2741
|
-
capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"]
|
|
2742
|
-
|
|
2743
|
-
template_body = jsonencode({
|
|
2744
|
-
AWSTemplateFormatVersion = "2010-09-09"
|
|
2745
|
-
Transform = "AWS::SecretsManager-2024-09-16"
|
|
2746
|
-
Resources = {
|
|
2747
|
-
CredentialsRotationSchedule = {
|
|
2748
|
-
Type = "AWS::SecretsManager::RotationSchedule"
|
|
2749
|
-
Properties = {
|
|
2750
|
-
SecretId = aws_secretsmanager_secret.credentials.arn
|
|
2751
|
-
HostedRotationLambda = {
|
|
2752
|
-
RotationType = var.engine == "aurora-mysql" ? "MySQLSingleUser" : "PostgreSQLSingleUser"
|
|
2753
|
-
RotationLambdaName = "\${local.cluster_name}-credentials-rotation"
|
|
2754
|
-
VpcSecurityGroupIds = aws_security_group.rotation[0].id
|
|
2755
|
-
VpcSubnetIds = join(",", var.lambda_subnet_ids)
|
|
2756
|
-
}
|
|
2757
|
-
RotationRules = {
|
|
2758
|
-
AutomaticallyAfterDays = 30
|
|
2759
|
-
}
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
}
|
|
2763
|
-
})
|
|
2764
|
-
|
|
2765
|
-
depends_on = [
|
|
2766
|
-
aws_secretsmanager_secret_version.credentials,
|
|
2767
|
-
aws_rds_cluster_instance.database,
|
|
2768
|
-
aws_vpc_security_group_egress_rule.rotation_to_database,
|
|
2769
|
-
aws_vpc_security_group_ingress_rule.rotation_to_database,
|
|
2770
|
-
]
|
|
2771
|
-
}
|
|
2772
|
-
|
|
2773
2619
|
resource "aws_iam_role" "proxy" {
|
|
2774
2620
|
count = var.enable_rds_proxy ? 1 : 0
|
|
2775
2621
|
|
|
@@ -3008,6 +2854,11 @@ output "cluster_endpoint" {
|
|
|
3008
2854
|
value = var.enable_rds_proxy ? aws_db_proxy.aurora[0].endpoint : aws_rds_cluster.database.endpoint
|
|
3009
2855
|
}
|
|
3010
2856
|
|
|
2857
|
+
output "writer_endpoint" {
|
|
2858
|
+
description = "Writer endpoint of the Aurora cluster itself, bypassing the RDS Proxy. Used by the admin-credentialled handlers, which connect directly to the cluster."
|
|
2859
|
+
value = aws_rds_cluster.database.endpoint
|
|
2860
|
+
}
|
|
2861
|
+
|
|
3011
2862
|
output "reader_endpoint" {
|
|
3012
2863
|
description = "Reader endpoint of the Aurora cluster."
|
|
3013
2864
|
value = aws_rds_cluster.database.reader_endpoint
|
|
@@ -3034,12 +2885,12 @@ output "proxy_role_name" {
|
|
|
3034
2885
|
}
|
|
3035
2886
|
|
|
3036
2887
|
output "secret_arn" {
|
|
3037
|
-
description = "ARN of the
|
|
3038
|
-
value =
|
|
2888
|
+
description = "ARN of the RDS-managed admin credentials secret. Its value is a JSON object with username and password only — connection details come from this module's other outputs."
|
|
2889
|
+
value = one(aws_rds_cluster.database.master_user_secret[*].secret_arn)
|
|
3039
2890
|
}
|
|
3040
2891
|
|
|
3041
2892
|
output "kms_key_arn" {
|
|
3042
|
-
description = "ARN of the KMS key protecting Aurora and
|
|
2893
|
+
description = "ARN of the KMS key protecting Aurora and its admin credentials secret."
|
|
3043
2894
|
value = aws_kms_key.database.arn
|
|
3044
2895
|
}
|
|
3045
2896
|
|
|
@@ -3064,8 +2915,8 @@ output "database_name" {
|
|
|
3064
2915
|
}
|
|
3065
2916
|
|
|
3066
2917
|
output "admin_user" {
|
|
3067
|
-
description = "Admin username stored in the
|
|
3068
|
-
value =
|
|
2918
|
+
description = "Admin username stored in the RDS-managed credentials secret."
|
|
2919
|
+
value = aws_rds_cluster.database.master_username
|
|
3069
2920
|
}
|
|
3070
2921
|
"
|
|
3071
2922
|
`;
|
|
@@ -3161,12 +3012,6 @@ variable "enable_rds_proxy" {
|
|
|
3161
3012
|
default = true
|
|
3162
3013
|
}
|
|
3163
3014
|
|
|
3164
|
-
variable "enable_credential_rotation" {
|
|
3165
|
-
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
3166
|
-
type = bool
|
|
3167
|
-
default = true
|
|
3168
|
-
}
|
|
3169
|
-
|
|
3170
3015
|
variable "enable_cloudwatch_logs" {
|
|
3171
3016
|
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL logs DDL statements only (log_statement=ddl); MySQL enables Advanced Auditing scoped to connections and DDL (server_audit_events=CONNECT,QUERY_DDL). Neither logs statement parameter values, to avoid leaking PII into log data."
|
|
3172
3017
|
type = bool
|
|
@@ -3239,7 +3084,6 @@ module "aurora" {
|
|
|
3239
3084
|
name = "db"
|
|
3240
3085
|
vpc_id = var.vpc_id
|
|
3241
3086
|
subnet_ids = var.database_subnet_ids
|
|
3242
|
-
lambda_subnet_ids = var.lambda_subnet_ids
|
|
3243
3087
|
engine = "aurora-mysql"
|
|
3244
3088
|
engine_version = var.engine_version
|
|
3245
3089
|
database_name = "database_name"
|
|
@@ -3251,7 +3095,6 @@ module "aurora" {
|
|
|
3251
3095
|
deletion_protection = var.deletion_protection
|
|
3252
3096
|
skip_final_snapshot = var.skip_final_snapshot
|
|
3253
3097
|
enable_rds_proxy = var.enable_rds_proxy
|
|
3254
|
-
enable_credential_rotation = var.enable_credential_rotation
|
|
3255
3098
|
enable_cloudwatch_logs = var.enable_cloudwatch_logs
|
|
3256
3099
|
enable_performance_insights = var.enable_performance_insights
|
|
3257
3100
|
performance_insights_retention_period = var.performance_insights_retention_period
|
|
@@ -3627,6 +3470,9 @@ resource "aws_lambda_function" "create_db_user" {
|
|
|
3627
3470
|
environment {
|
|
3628
3471
|
variables = {
|
|
3629
3472
|
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
3473
|
+
DATABASE_HOST = module.aurora.writer_endpoint
|
|
3474
|
+
DATABASE_PORT = module.aurora.cluster_port
|
|
3475
|
+
DATABASE_NAME = module.aurora.database_name
|
|
3630
3476
|
NODE_EXTRA_CA_CERTS = "/var/runtime/ca-cert.pem"
|
|
3631
3477
|
}
|
|
3632
3478
|
}
|
|
@@ -3704,6 +3550,9 @@ resource "aws_lambda_function" "migration_handler" {
|
|
|
3704
3550
|
environment {
|
|
3705
3551
|
variables = {
|
|
3706
3552
|
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
3553
|
+
DATABASE_HOST = module.aurora.writer_endpoint
|
|
3554
|
+
DATABASE_PORT = module.aurora.cluster_port
|
|
3555
|
+
DATABASE_NAME = module.aurora.database_name
|
|
3707
3556
|
}
|
|
3708
3557
|
}
|
|
3709
3558
|
|
|
@@ -4070,6 +3919,24 @@ export type DatabaseSecret = {
|
|
|
4070
3919
|
port: number;
|
|
4071
3920
|
};
|
|
4072
3921
|
|
|
3922
|
+
/**
|
|
3923
|
+
* Connection details for the admin credentials secret. RDS-managed secrets hold
|
|
3924
|
+
* only \`username\` and \`password\`, so the host, port and database name are taken
|
|
3925
|
+
* from the environment when the secret omits them.
|
|
3926
|
+
*/
|
|
3927
|
+
type SecretConnectionOverrides = Partial<
|
|
3928
|
+
Omit<DatabaseSecret, 'username' | 'password'>
|
|
3929
|
+
>;
|
|
3930
|
+
|
|
3931
|
+
const connectionOverridesFromEnv = (): SecretConnectionOverrides => {
|
|
3932
|
+
const { DATABASE_HOST, DATABASE_PORT, DATABASE_NAME } = process.env;
|
|
3933
|
+
return {
|
|
3934
|
+
...(DATABASE_HOST ? { host: DATABASE_HOST } : {}),
|
|
3935
|
+
...(DATABASE_PORT ? { port: Number(DATABASE_PORT) } : {}),
|
|
3936
|
+
...(DATABASE_NAME ? { dbname: DATABASE_NAME } : {}),
|
|
3937
|
+
};
|
|
3938
|
+
};
|
|
3939
|
+
|
|
4073
3940
|
const databaseConfigPromises: Record<string, Promise<DatabaseConfig>> = {};
|
|
4074
3941
|
|
|
4075
3942
|
const getSecretValue = async (secretArn: string): Promise<string> => {
|
|
@@ -4096,7 +3963,18 @@ export const getDatabaseSecret = async (): Promise<DatabaseSecret> => {
|
|
|
4096
3963
|
);
|
|
4097
3964
|
}
|
|
4098
3965
|
|
|
4099
|
-
|
|
3966
|
+
const secret = JSON.parse(await getSecretValue(secretArn)) as DatabaseSecret;
|
|
3967
|
+
const secretWithConnection = { ...secret, ...connectionOverridesFromEnv() };
|
|
3968
|
+
|
|
3969
|
+
for (const key of ['host', 'port', 'dbname'] as const) {
|
|
3970
|
+
if (secretWithConnection[key] == null) {
|
|
3971
|
+
throw new Error(
|
|
3972
|
+
\`Database secret is missing \${key} and no override was provided in the environment.\`,
|
|
3973
|
+
);
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
return secretWithConnection;
|
|
4100
3978
|
};
|
|
4101
3979
|
|
|
4102
3980
|
const loadDatabaseConfig = async (
|
|
@@ -21,6 +21,22 @@ export type DatabaseSecret = {
|
|
|
21
21
|
port: number;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Connection details for the admin credentials secret. RDS-managed secrets hold
|
|
26
|
+
* only `username` and `password`, so the host, port and database name are taken
|
|
27
|
+
* from the environment when the secret omits them.
|
|
28
|
+
*/
|
|
29
|
+
type SecretConnectionOverrides = Partial<Omit<DatabaseSecret, 'username' | 'password'>>;
|
|
30
|
+
|
|
31
|
+
const connectionOverridesFromEnv = (): SecretConnectionOverrides => {
|
|
32
|
+
const { DATABASE_HOST, DATABASE_PORT, DATABASE_NAME } = process.env;
|
|
33
|
+
return {
|
|
34
|
+
...(DATABASE_HOST ? { host: DATABASE_HOST } : {}),
|
|
35
|
+
...(DATABASE_PORT ? { port: Number(DATABASE_PORT) } : {}),
|
|
36
|
+
...(DATABASE_NAME ? { dbname: DATABASE_NAME } : {}),
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
24
40
|
const databaseConfigPromises: Record<string, Promise<DatabaseConfig>> = {};
|
|
25
41
|
|
|
26
42
|
const getSecretValue = async (secretArn: string): Promise<string> => {
|
|
@@ -47,7 +63,18 @@ export const getDatabaseSecret = async (): Promise<DatabaseSecret> => {
|
|
|
47
63
|
);
|
|
48
64
|
}
|
|
49
65
|
|
|
50
|
-
|
|
66
|
+
const secret = JSON.parse(await getSecretValue(secretArn)) as DatabaseSecret;
|
|
67
|
+
const secretWithConnection = { ...secret, ...connectionOverridesFromEnv() };
|
|
68
|
+
|
|
69
|
+
for (const key of ['host', 'port', 'dbname'] as const) {
|
|
70
|
+
if (secretWithConnection[key] == null) {
|
|
71
|
+
throw new Error(
|
|
72
|
+
`Database secret is missing ${key} and no override was provided in the environment.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return secretWithConnection;
|
|
51
78
|
};
|
|
52
79
|
|
|
53
80
|
const loadDatabaseConfig = async (
|
|
@@ -88,12 +88,6 @@ variable "enable_rds_proxy" {
|
|
|
88
88
|
default = true
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
variable "enable_credential_rotation" {
|
|
92
|
-
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
93
|
-
type = bool
|
|
94
|
-
default = true
|
|
95
|
-
}
|
|
96
|
-
|
|
97
91
|
variable "enable_cloudwatch_logs" {
|
|
98
92
|
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL logs DDL statements only (log_statement=ddl); MySQL enables Advanced Auditing scoped to connections and DDL (server_audit_events=CONNECT,QUERY_DDL). Neither logs statement parameter values, to avoid leaking PII into log data."
|
|
99
93
|
type = bool
|
|
@@ -177,7 +171,6 @@ module "aurora" {
|
|
|
177
171
|
name = "<%= nameKebabCase %>"
|
|
178
172
|
vpc_id = var.vpc_id
|
|
179
173
|
subnet_ids = var.database_subnet_ids
|
|
180
|
-
lambda_subnet_ids = var.lambda_subnet_ids
|
|
181
174
|
engine = "<%= engine === 'mysql' ? 'aurora-mysql' : 'aurora-postgresql' %>"
|
|
182
175
|
engine_version = var.engine_version
|
|
183
176
|
database_name = "<%= databaseName %>"
|
|
@@ -189,7 +182,6 @@ module "aurora" {
|
|
|
189
182
|
deletion_protection = var.deletion_protection
|
|
190
183
|
skip_final_snapshot = var.skip_final_snapshot
|
|
191
184
|
enable_rds_proxy = var.enable_rds_proxy
|
|
192
|
-
enable_credential_rotation = var.enable_credential_rotation
|
|
193
185
|
enable_cloudwatch_logs = var.enable_cloudwatch_logs
|
|
194
186
|
enable_performance_insights = var.enable_performance_insights
|
|
195
187
|
performance_insights_retention_period = var.performance_insights_retention_period
|
|
@@ -612,6 +604,9 @@ resource "aws_lambda_function" "create_db_user" {
|
|
|
612
604
|
environment {
|
|
613
605
|
variables = {
|
|
614
606
|
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
607
|
+
DATABASE_HOST = module.aurora.writer_endpoint
|
|
608
|
+
DATABASE_PORT = module.aurora.cluster_port
|
|
609
|
+
DATABASE_NAME = module.aurora.database_name
|
|
615
610
|
<%_ if (framework === 'prisma') { _%>
|
|
616
611
|
NODE_EXTRA_CA_CERTS = "/var/runtime/ca-cert.pem"
|
|
617
612
|
<%_ } _%>
|
|
@@ -699,6 +694,9 @@ resource "aws_lambda_function" "migration_handler" {
|
|
|
699
694
|
environment {
|
|
700
695
|
variables = {
|
|
701
696
|
DATABASE_SECRET_ARN = module.aurora.secret_arn
|
|
697
|
+
DATABASE_HOST = module.aurora.writer_endpoint
|
|
698
|
+
DATABASE_PORT = module.aurora.cluster_port
|
|
699
|
+
DATABASE_NAME = module.aurora.database_name
|
|
702
700
|
}
|
|
703
701
|
}
|
|
704
702
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Core Aurora module
|
|
2
|
-
# This module creates an Aurora cluster
|
|
2
|
+
# This module creates an Aurora cluster whose admin credentials are generated
|
|
3
|
+
# and rotated by RDS in Secrets Manager.
|
|
3
4
|
|
|
4
5
|
terraform {
|
|
5
6
|
required_version = ">= 1.0"
|
|
@@ -43,11 +44,6 @@ variable "subnet_ids" {
|
|
|
43
44
|
type = list(string)
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
variable "lambda_subnet_ids" {
|
|
47
|
-
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."
|
|
48
|
-
type = list(string)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
variable "engine" {
|
|
52
48
|
description = "Aurora engine to use."
|
|
53
49
|
type = string
|
|
@@ -117,12 +113,6 @@ variable "enable_rds_proxy" {
|
|
|
117
113
|
default = true
|
|
118
114
|
}
|
|
119
115
|
|
|
120
|
-
variable "enable_credential_rotation" {
|
|
121
|
-
description = "Whether to enable automatic credential rotation for the admin secret."
|
|
122
|
-
type = bool
|
|
123
|
-
default = true
|
|
124
|
-
}
|
|
125
|
-
|
|
126
116
|
variable "enable_cloudwatch_logs" {
|
|
127
117
|
description = "Whether to export Aurora engine logs to CloudWatch. PostgreSQL logs DDL statements only (log_statement=ddl); MySQL enables Advanced Auditing scoped to connections and DDL (server_audit_events=CONNECT,QUERY_DDL). Neither logs statement parameter values, to avoid leaking PII into log data."
|
|
128
118
|
type = bool
|
|
@@ -244,6 +234,20 @@ resource "aws_kms_key" "database" {
|
|
|
244
234
|
"kms:CreateGrant"
|
|
245
235
|
]
|
|
246
236
|
Resource = "*"
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
Sid = "AllowSecretsManagerService"
|
|
240
|
+
Effect = "Allow"
|
|
241
|
+
Principal = {
|
|
242
|
+
Service = "secretsmanager.amazonaws.com"
|
|
243
|
+
}
|
|
244
|
+
Action = [
|
|
245
|
+
"kms:Encrypt",
|
|
246
|
+
"kms:Decrypt",
|
|
247
|
+
"kms:GenerateDataKey*",
|
|
248
|
+
"kms:DescribeKey"
|
|
249
|
+
]
|
|
250
|
+
Resource = "*"
|
|
247
251
|
}
|
|
248
252
|
]
|
|
249
253
|
})
|
|
@@ -298,35 +302,6 @@ resource "aws_db_subnet_group" "database" {
|
|
|
298
302
|
})
|
|
299
303
|
}
|
|
300
304
|
|
|
301
|
-
resource "random_password" "master_password" {
|
|
302
|
-
length = 32
|
|
303
|
-
special = true
|
|
304
|
-
override_special = "!#$%&*()-_=+[]{}<>:?"
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
resource "aws_secretsmanager_secret" "credentials" {
|
|
308
|
-
#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
|
|
309
|
-
name_prefix = "${var.name}-aurora-credentials-"
|
|
310
|
-
kms_key_id = aws_kms_key.database.arn
|
|
311
|
-
|
|
312
|
-
tags = merge(var.tags, {
|
|
313
|
-
Name = "${var.name}-aurora-credentials"
|
|
314
|
-
})
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
resource "aws_secretsmanager_secret_version" "credentials" {
|
|
318
|
-
secret_id = aws_secretsmanager_secret.credentials.id
|
|
319
|
-
secret_string = jsonencode({
|
|
320
|
-
engine = var.engine == "aurora-mysql" ? "mysql" : "postgres"
|
|
321
|
-
username = var.admin_user
|
|
322
|
-
password = random_password.master_password.result
|
|
323
|
-
host = aws_rds_cluster.database.endpoint
|
|
324
|
-
port = aws_rds_cluster.database.port
|
|
325
|
-
dbname = var.database_name
|
|
326
|
-
dbClusterIdentifier = aws_rds_cluster.database.cluster_identifier
|
|
327
|
-
})
|
|
328
|
-
}
|
|
329
|
-
|
|
330
305
|
resource "aws_rds_cluster" "database" {
|
|
331
306
|
#checkov:skip=CKV2_AWS_27:Query logging is enabled by default via enable_cloudwatch_logs; checkov cannot resolve the conditional count expression on aws_rds_cluster_parameter_group.database
|
|
332
307
|
#checkov:skip=CKV_AWS_139:Deletion protection is enabled but checkov is unable to detect it correctly
|
|
@@ -335,7 +310,8 @@ resource "aws_rds_cluster" "database" {
|
|
|
335
310
|
engine_version = coalesce(var.engine_version, local.default_engine_version)
|
|
336
311
|
database_name = var.database_name
|
|
337
312
|
master_username = var.admin_user
|
|
338
|
-
|
|
313
|
+
manage_master_user_password = true
|
|
314
|
+
master_user_secret_kms_key_id = aws_kms_key.database.arn
|
|
339
315
|
db_subnet_group_name = aws_db_subnet_group.database.name
|
|
340
316
|
db_cluster_parameter_group_name = var.enable_cloudwatch_logs ? aws_rds_cluster_parameter_group.database[0].name : null
|
|
341
317
|
vpc_security_group_ids = [aws_security_group.database.id]
|
|
@@ -387,75 +363,6 @@ resource "aws_rds_cluster_instance" "database" {
|
|
|
387
363
|
})
|
|
388
364
|
}
|
|
389
365
|
|
|
390
|
-
resource "aws_security_group" "rotation" {
|
|
391
|
-
#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
|
|
392
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
393
|
-
name_prefix = "${var.name}-aurora-rotation-"
|
|
394
|
-
description = "Security group for Aurora secret rotation ${var.name}"
|
|
395
|
-
vpc_id = var.vpc_id
|
|
396
|
-
|
|
397
|
-
tags = merge(var.tags, {
|
|
398
|
-
Name = "${var.name}-aurora-rotation"
|
|
399
|
-
})
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
resource "aws_vpc_security_group_egress_rule" "rotation_to_database" {
|
|
403
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
404
|
-
security_group_id = aws_security_group.rotation[0].id
|
|
405
|
-
referenced_security_group_id = aws_security_group.database.id
|
|
406
|
-
from_port = coalesce(var.port, local.default_port)
|
|
407
|
-
to_port = coalesce(var.port, local.default_port)
|
|
408
|
-
ip_protocol = "tcp"
|
|
409
|
-
description = "Allow outbound database traffic from secret rotation Lambda"
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
resource "aws_vpc_security_group_ingress_rule" "rotation_to_database" {
|
|
413
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
414
|
-
security_group_id = aws_security_group.database.id
|
|
415
|
-
referenced_security_group_id = aws_security_group.rotation[0].id
|
|
416
|
-
from_port = coalesce(var.port, local.default_port)
|
|
417
|
-
to_port = coalesce(var.port, local.default_port)
|
|
418
|
-
ip_protocol = "tcp"
|
|
419
|
-
description = "Allow inbound database traffic from secret rotation Lambda"
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
resource "aws_cloudformation_stack" "credentials_rotation" {
|
|
423
|
-
#checkov:skip=CKV_AWS_124:SNS event notifications are not required for the credentials rotation stack
|
|
424
|
-
count = var.enable_credential_rotation ? 1 : 0
|
|
425
|
-
name = "${local.cluster_name}-credentials-rotation"
|
|
426
|
-
|
|
427
|
-
capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"]
|
|
428
|
-
|
|
429
|
-
template_body = jsonencode({
|
|
430
|
-
AWSTemplateFormatVersion = "2010-09-09"
|
|
431
|
-
Transform = "AWS::SecretsManager-2024-09-16"
|
|
432
|
-
Resources = {
|
|
433
|
-
CredentialsRotationSchedule = {
|
|
434
|
-
Type = "AWS::SecretsManager::RotationSchedule"
|
|
435
|
-
Properties = {
|
|
436
|
-
SecretId = aws_secretsmanager_secret.credentials.arn
|
|
437
|
-
HostedRotationLambda = {
|
|
438
|
-
RotationType = var.engine == "aurora-mysql" ? "MySQLSingleUser" : "PostgreSQLSingleUser"
|
|
439
|
-
RotationLambdaName = "${local.cluster_name}-credentials-rotation"
|
|
440
|
-
VpcSecurityGroupIds = aws_security_group.rotation[0].id
|
|
441
|
-
VpcSubnetIds = join(",", var.lambda_subnet_ids)
|
|
442
|
-
}
|
|
443
|
-
RotationRules = {
|
|
444
|
-
AutomaticallyAfterDays = 30
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
})
|
|
450
|
-
|
|
451
|
-
depends_on = [
|
|
452
|
-
aws_secretsmanager_secret_version.credentials,
|
|
453
|
-
aws_rds_cluster_instance.database,
|
|
454
|
-
aws_vpc_security_group_egress_rule.rotation_to_database,
|
|
455
|
-
aws_vpc_security_group_ingress_rule.rotation_to_database,
|
|
456
|
-
]
|
|
457
|
-
}
|
|
458
|
-
|
|
459
366
|
resource "aws_iam_role" "proxy" {
|
|
460
367
|
count = var.enable_rds_proxy ? 1 : 0
|
|
461
368
|
|
|
@@ -694,6 +601,11 @@ output "cluster_endpoint" {
|
|
|
694
601
|
value = var.enable_rds_proxy ? aws_db_proxy.aurora[0].endpoint : aws_rds_cluster.database.endpoint
|
|
695
602
|
}
|
|
696
603
|
|
|
604
|
+
output "writer_endpoint" {
|
|
605
|
+
description = "Writer endpoint of the Aurora cluster itself, bypassing the RDS Proxy. Used by the admin-credentialled handlers, which connect directly to the cluster."
|
|
606
|
+
value = aws_rds_cluster.database.endpoint
|
|
607
|
+
}
|
|
608
|
+
|
|
697
609
|
output "reader_endpoint" {
|
|
698
610
|
description = "Reader endpoint of the Aurora cluster."
|
|
699
611
|
value = aws_rds_cluster.database.reader_endpoint
|
|
@@ -720,12 +632,12 @@ output "proxy_role_name" {
|
|
|
720
632
|
}
|
|
721
633
|
|
|
722
634
|
output "secret_arn" {
|
|
723
|
-
description = "ARN of the
|
|
724
|
-
value =
|
|
635
|
+
description = "ARN of the RDS-managed admin credentials secret. Its value is a JSON object with username and password only — connection details come from this module's other outputs."
|
|
636
|
+
value = one(aws_rds_cluster.database.master_user_secret[*].secret_arn)
|
|
725
637
|
}
|
|
726
638
|
|
|
727
639
|
output "kms_key_arn" {
|
|
728
|
-
description = "ARN of the KMS key protecting Aurora and
|
|
640
|
+
description = "ARN of the KMS key protecting Aurora and its admin credentials secret."
|
|
729
641
|
value = aws_kms_key.database.arn
|
|
730
642
|
}
|
|
731
643
|
|
|
@@ -750,6 +662,6 @@ output "database_name" {
|
|
|
750
662
|
}
|
|
751
663
|
|
|
752
664
|
output "admin_user" {
|
|
753
|
-
description = "Admin username stored in the
|
|
754
|
-
value =
|
|
665
|
+
description = "Admin username stored in the RDS-managed credentials secret."
|
|
666
|
+
value = aws_rds_cluster.database.master_username
|
|
755
667
|
}
|