@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 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.85",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws/nx-plugin",
3
- "version": "1.0.0-rc.85",
3
+ "version": "1.0.0-rc.86",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/awslabs/nx-plugin-for-aws.git",
@@ -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
- return json.loads(response["SecretString"])
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
- return json.loads(response['SecretString'])
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
- return JSON.parse(await getSecretValue(secretArn)) as DatabaseSecret;
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 and a generated admin secret.
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
- master_password = random_password.master_password.result
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 generated admin credentials secret."
1535
- value = aws_secretsmanager_secret.credentials.arn
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 the generated secret."
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 generated secret."
1565
- value = jsondecode(aws_secretsmanager_secret_version.credentials.secret_string).username
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 and a generated admin secret.
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
- master_password = random_password.master_password.result
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 generated admin credentials secret."
3038
- value = aws_secretsmanager_secret.credentials.arn
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 the generated secret."
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 generated secret."
3068
- value = jsondecode(aws_secretsmanager_secret_version.credentials.secret_string).username
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
- return JSON.parse(await getSecretValue(secretArn)) as DatabaseSecret;
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
- return JSON.parse(await getSecretValue(secretArn)) as DatabaseSecret;
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 and a generated admin secret.
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
- master_password = random_password.master_password.result
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 generated admin credentials secret."
724
- value = aws_secretsmanager_secret.credentials.arn
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 the generated secret."
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 generated secret."
754
- value = jsondecode(aws_secretsmanager_secret_version.credentials.secret_string).username
665
+ description = "Admin username stored in the RDS-managed credentials secret."
666
+ value = aws_rds_cluster.database.master_username
755
667
  }