1import type * as Duration from "effect/Duration";
2import * as Effect from "effect/Effect";
3import * as Redacted from "effect/Redacted";
4import type { Input } from "../../Input.ts";
5import { toWireSeconds } from "../../Util/Duration.ts";
6import * as Namespace from "../../Namespace.ts";
7import type { SecurityGroupId } from "../EC2/SecurityGroup.ts";
8import type { SubnetId } from "../EC2/Subnet.ts";
9import * as IAM from "../IAM/index.ts";
10import {
11 Secret,
12 type GenerateSecretStringProps,
13 type SecretProps,
14 type Secret as SecretResource,
15} from "../SecretsManager/Secret.ts";
16import {
17 DBCluster,
18 type DBClusterProps,
19 type DBCluster as DBClusterResource,
20} from "./DBCluster.ts";
21import {
22 DBClusterParameterGroup,
23 type DBClusterParameterGroupProps,
24 type DBClusterParameterGroup as DBClusterParameterGroupResource,
25} from "./DBClusterParameterGroup.ts";
26import {
27 DBInstance,
28 type DBInstanceProps,
29 type DBInstance as DBInstanceResource,
30} from "./DBInstance.ts";
31import {
32 DBParameterGroup,
33 type DBParameterGroupProps,
34 type DBParameterGroup as DBParameterGroupResource,
35} from "./DBParameterGroup.ts";
36import {
37 DBProxy,
38 type DBProxyProps,
39 type DBProxy as DBProxyResource,
40} from "./DBProxy.ts";
41import {
42 DBProxyEndpoint,
43 type DBProxyEndpointProps,
44 type DBProxyEndpoint as DBProxyEndpointResource,
45} from "./DBProxyEndpoint.ts";
46import {
47 DBProxyTargetGroup,
48 type DBProxyTargetGroupProps,
49 type DBProxyTargetGroup as DBProxyTargetGroupResource,
50} from "./DBProxyTargetGroup.ts";
51import {
52 DBSubnetGroup,
53 type DBSubnetGroupProps,
54 type DBSubnetGroup as DBSubnetGroupResource,
55} from "./DBSubnetGroup.ts";
56
57export interface AuroraSecretProps extends Omit<
58 SecretProps,
59 "secretString" | "secretBinary" | "generateSecretString"
60> {
61 /**
62 * Existing secret to reuse instead of creating one.
63 */
64 resource?: SecretResource;
65 /**
66 * Master username written into a generated secret payload.
67 * @default "app"
68 */
69 username?: string;
70 /**
71 * Optional explicit JSON/string secret payload.
72 */
73 secretString?: Redacted.Redacted<string>;
74 /**
75 * Optional explicit binary secret payload.
76 */
77 secretBinary?: Redacted.Redacted<Uint8Array<ArrayBufferLike>>;
78 /**
79 * Password generation settings for the created secret.
80 */
81 generateSecretString?: GenerateSecretStringProps;
82}
83
84export interface AuroraProxyProps extends Omit<
85 DBProxyProps,
86 "engineFamily" | "auth" | "roleArn" | "vpcSubnetIds"
87> {
88 /**
89 * Override the default proxy auth config.
90 */
91 auth?: DBProxyProps["auth"];
92 /**
93 * Additional target-group configuration for the default proxy target group.
94 */
95 targetGroup?: Omit<
96 DBProxyTargetGroupProps,
97 "dbProxyName" | "dbClusterIdentifiers"
98 >;
99 /**
100 * Optional extra endpoint to create for the proxy.
101 * Use `true` for sensible defaults.
102 */
103 endpoint?: true | Omit<DBProxyEndpointProps, "dbProxyName" | "vpcSubnetIds">;
104}
105
106export interface AuroraProps {
107 /**
108 * Database name created in the cluster.
109 * @default "app"
110 */
111 databaseName?: string;
112 /**
113 * Aurora engine.
114 * @default "aurora-postgresql"
115 */
116 engine?: string;
117 /**
118 * Optional engine version.
119 */
120 engineVersion?: string;
121 /**
122 * Subnets for the cluster and optional proxy.
123 */
124 subnetIds: Input<SubnetId[]>;
125 /**
126 * Security groups attached to the cluster and instance.
127 */
128 securityGroupIds: Input<SecurityGroupId[]>;
129 /**
130 * Number of read replicas to create alongside the writer.
131 * @default 0
132 */
133 readers?: number;
134 /**
135 * Cluster-wide tags applied to all created resources by default.
136 */
137 tags?: Record<string, Input<string>>;
138 /**
139 * Tune the generated or reused admin secret.
140 */
141 secret?: AuroraSecretProps;
142 /**
143 * Override subnet group creation.
144 */
145 subnetGroup?: Omit<DBSubnetGroupProps, "subnetIds">;
146 /**
147 * Optional cluster parameter group.
148 */
149 clusterParameterGroup?: Omit<DBClusterParameterGroupProps, "family"> & {
150 family: string;
151 };
152 /**
153 * Optional instance parameter group.
154 */
155 parameterGroup?: Omit<DBParameterGroupProps, "family"> & {
156 family: string;
157 };
158 /**
159 * Tune the Aurora cluster resource.
160 */
161 cluster?: Omit<
162 DBClusterProps,
163 | "engine"
164 | "engineVersion"
165 | "databaseName"
166 | "dbSubnetGroupName"
167 | "dbClusterParameterGroupName"
168 | "vpcSecurityGroupIds"
169 | "masterUserSecretArn"
170 | "masterUsername"
171 | "masterUserPassword"
172 | "tags"
173 > & {
174 tags?: Record<string, Input<string>>;
175 };
176 /**
177 * Tune the writer/readers.
178 */
179 instance?: Omit<
180 DBInstanceProps,
181 | "dbClusterIdentifier"
182 | "engine"
183 | "engineVersion"
184 | "dbSubnetGroupName"
185 | "dbParameterGroupName"
186 | "vpcSecurityGroupIds"
187 | "tags"
188 > & {
189 tags?: Record<string, Input<string>>;
190 };
191 /**
192 * Whether to enable the Aurora Data API.
193 * @default true
194 */
195 dataApi?: boolean;
196 /**
197 * Backup retention period (e.g. `"7 days"`), forwarded to the cluster.
198 */
199 backupRetentionPeriod?: Duration.Input;
200 /**
201 * Daily backup window (`hh:mm-hh:mm` UTC), forwarded to the cluster.
202 */
203 preferredBackupWindow?: string;
204 /**
205 * Weekly maintenance window, forwarded to the cluster.
206 */
207 preferredMaintenanceWindow?: string;
208 /**
209 * Encrypt cluster storage. Forwarded to the cluster.
210 */
211 storageEncrypted?: boolean;
212 /**
213 * KMS key for storage encryption. Forwarded to the cluster.
214 */
215 kmsKeyId?: string;
216 /**
217 * Enable IAM database authentication. Forwarded to the cluster.
218 */
219 enableIAMDatabaseAuthentication?: boolean;
220 /**
221 * Log types to export to CloudWatch Logs. Forwarded to the cluster.
222 */
223 enableCloudwatchLogsExports?: string[];
224 /**
225 * Block accidental deletion. Forwarded to the cluster and instances.
226 * @default false
227 */
228 deletionProtection?: boolean;
229 /**
230 * CA certificate identifier. Forwarded to the cluster.
231 */
232 caCertificateIdentifier?: string;
233 /**
234 * Listener port. Forwarded to the cluster.
235 */
236 port?: number;
237 /**
238 * Aurora MySQL backtrack window (e.g. `"1 hour"`). Forwarded to the
239 * cluster.
240 */
241 backtrackWindow?: Duration.Input;
242 /**
243 * Enhanced-monitoring + Performance Insights settings. Forwarded to the
244 * cluster (and the enhanced-monitoring role to the instances).
245 */
246 monitoring?: {
247 /**
248 * Enhanced-monitoring granularity (e.g. `"60 seconds"`). Sent to the
249 * API in whole seconds (valid: 0, 1, 5, 10, 15, 30, 60).
250 */
251 interval?: Duration.Input;
252 /**
253 * Existing IAM role ARN for enhanced monitoring. When omitted and
254 * `interval > 0`, Aurora creates one automatically.
255 */
256 roleArn?: Input<string>;
257 /**
258 * Enable Performance Insights on the cluster.
259 */
260 performanceInsights?: boolean;
261 };
262 /**
263 * Serverless v2 min/max ACUs. Shorthand for
264 * `serverlessV2ScalingConfiguration`.
265 */
266 scaling?: {
267 minCapacity?: number;
268 maxCapacity?: number;
269 };
270 /**
271 * Provisioned (non-serverless) instance class for the writer/readers, e.g.
272 * `db.r6g.large`. Defaults to `db.serverless`.
273 */
274 instanceClass?: string;
275 /**
276 * Opt in to an auto-wired RDS Proxy.
277 */
278 proxy?: boolean | AuroraProxyProps;
279}
280
281export interface AuroraDatabase {
282 secret: SecretResource;
283 subnetGroup: DBSubnetGroupResource;
284 clusterParameterGroup?: DBClusterParameterGroupResource;
285 parameterGroup?: DBParameterGroupResource;
286 cluster: DBClusterResource;
287 writer: DBInstanceResource;
288 readers: DBInstanceResource[];
289 instances: [DBInstanceResource, ...DBInstanceResource[]];
290 proxy?: {
291 role: IAM.Role;
292 proxy: DBProxyResource;
293 targetGroup: DBProxyTargetGroupResource;
294 endpoint?: DBProxyEndpointResource;
295 };
296}
297
298const mergeTags = (
299 base: AuroraProps["tags"] | undefined,
300 extra: Record<string, Input<string>> | undefined,
301) => ({
302 ...base,
303 ...extra,
304});
305
306const inferProxyEngineFamily = (engine: string) => {
307 if (engine.includes("postgres")) {
308 return "POSTGRESQL" as const;
309 }
310 if (engine.includes("mysql")) {
311 return "MYSQL" as const;
312 }
313 return "POSTGRESQL" as const;
314};
315
316/**
317 * Opinionated Aurora bring-up helper.
318 *
319 * `Aurora` is the fast-start L2 for getting a working database online with one
320 * call. It creates a generated admin secret, DB subnet group, Aurora cluster,
321 * and a single writer instance by default. Optional readers, parameter groups,
322 * and an auto-wired RDS Proxy can be enabled as needs grow.
323 *
324 * The return value intentionally exposes the underlying `DB*` resources so
325 * users can expand into the lower-level surface without rewriting the stack.
326 * ### Creating a Database
327 * **Example:** Start a Small Aurora Cluster
328 * ```typescript
329 * const db = yield* AWS.RDS.Aurora("AppDb", {
330 * subnetIds: [privateSubnetA.subnetId, privateSubnetB.subnetId],
331 * securityGroupIds: [databaseSecurityGroup.groupId],
332 * });
333 * ```
334 *
335 * ### Scaling Out
336 * **Example:** Add Readers and a Proxy
337 * ```typescript
338 * const db = yield* AWS.RDS.Aurora("AppDb", {
339 * subnetIds: [privateSubnetA.subnetId, privateSubnetB.subnetId],
340 * securityGroupIds: [databaseSecurityGroup.groupId],
341 * readers: 2,
342 * proxy: true,
343 * });
344 * ```
345 *
346 * ### Querying from a Function
347 * **Example:** Query over the Data API
348 * ```typescript
349 * // the Data API is enabled by default (dataApi: true) — bind
350 * // AWS.RDSData.ExecuteStatement to query without a VPC socket
351 * const executeStatement = yield* AWS.RDSData.ExecuteStatement(db.cluster, {
352 * secret: db.secret,
353 * database: "app",
354 * });
355 * const result = yield* executeStatement({ sql: "SELECT 1" });
356 * ```
357 *
358 * @resource
359 */
360export const Aurora = (id: string, props: AuroraProps) =>
361 Namespace.push(
362 id,
363 Effect.gen(function* () {
364 const engine = props.engine ?? "aurora-postgresql";
365 const engineVersion = props.engineVersion;
366 const databaseName = props.databaseName ?? "app";
367 const username = props.secret?.username ?? "app";
368 const subnetIds = props.subnetIds;
369 const securityGroupIds = props.securityGroupIds;
370 const commonTags = props.tags;
371 const proxyConfig: AuroraProxyProps | undefined =
372 props.proxy === true ? {} : props.proxy || undefined;
373
374 const secret =
375 props.secret?.resource ??
376 (yield* Secret("Secret", {
377 name: props.secret?.name,
378 description:
379 props.secret?.description ??
380 `Credentials for Aurora database ${id}`,
381 kmsKeyId: props.secret?.kmsKeyId,
382 secretString: props.secret?.secretString,
383 secretBinary: props.secret?.secretBinary,
384 generateSecretString:
385 props.secret?.secretString || props.secret?.secretBinary
386 ? undefined
387 : {
388 secretStringTemplate: JSON.stringify({ username }),
389 generateStringKey: "password",
390 PasswordLength: 32,
391 ExcludeCharacters: "\"'@/\\",
392 ...props.secret?.generateSecretString,
393 },
394 tags: mergeTags(commonTags, props.secret?.tags),
395 }));
396
397 const subnetGroup = yield* DBSubnetGroup("SubnetGroup", {
398 dbSubnetGroupName: props.subnetGroup?.dbSubnetGroupName,
399 description: props.subnetGroup?.description,
400 subnetIds,
401 tags: mergeTags(commonTags, props.subnetGroup?.tags),
402 });
403
404 const clusterParameterGroup = props.clusterParameterGroup
405 ? yield* DBClusterParameterGroup("ClusterParameterGroup", {
406 dbClusterParameterGroupName:
407 props.clusterParameterGroup.dbClusterParameterGroupName,
408 family: props.clusterParameterGroup.family,
409 description: props.clusterParameterGroup.description,
410 tags: mergeTags(commonTags, props.clusterParameterGroup.tags),
411 })
412 : undefined;
413
414 const parameterGroup = props.parameterGroup
415 ? yield* DBParameterGroup("ParameterGroup", {
416 dbParameterGroupName: props.parameterGroup.dbParameterGroupName,
417 family: props.parameterGroup.family,
418 description: props.parameterGroup.description,
419 tags: mergeTags(commonTags, props.parameterGroup.tags),
420 })
421 : undefined;
422
423 // Enhanced-monitoring role: reuse an explicit ARN, otherwise auto-create
424 // one when a non-zero interval is requested.
425 const monitoringInterval = props.monitoring?.interval;
426 const monitoringRole =
427 monitoringInterval !== undefined &&
428 (toWireSeconds(monitoringInterval) ?? 0) > 0 &&
429 !props.monitoring?.roleArn
430 ? yield* IAM.Role("MonitoringRole", {
431 assumeRolePolicyDocument: {
432 Version: "2012-10-17",
433 Statement: [
434 {
435 Effect: "Allow",
436 Principal: { Service: "monitoring.rds.amazonaws.com" },
437 Action: ["sts:AssumeRole"],
438 Resource: ["*"],
439 },
440 ],
441 },
442 managedPolicyArns: [
443 "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole",
444 ],
445 tags: mergeTags(commonTags, undefined),
446 })
447 : undefined;
448 const monitoringRoleArn = props.monitoring?.roleArn ?? monitoringRole?.roleArn; // prettier-ignore
449
450 const cluster = yield* DBCluster("Cluster", {
451 engine,
452 engineVersion,
453 databaseName,
454 dbSubnetGroupName: subnetGroup.dbSubnetGroupName,
455 dbClusterParameterGroupName:
456 clusterParameterGroup?.dbClusterParameterGroupName,
457 vpcSecurityGroupIds: securityGroupIds,
458 enableHttpEndpoint: props.dataApi ?? true,
459 copyTagsToSnapshot: props.cluster?.copyTagsToSnapshot ?? true,
460 deletionProtection:
461 props.cluster?.deletionProtection ??
462 props.deletionProtection ??
463 false,
464 backupRetentionPeriod:
465 props.cluster?.backupRetentionPeriod ?? props.backupRetentionPeriod,
466 preferredBackupWindow:
467 props.cluster?.preferredBackupWindow ?? props.preferredBackupWindow,
468 preferredMaintenanceWindow:
469 props.cluster?.preferredMaintenanceWindow ??
470 props.preferredMaintenanceWindow,
471 storageEncrypted:
472 props.cluster?.storageEncrypted ?? props.storageEncrypted,
473 kmsKeyId: props.cluster?.kmsKeyId ?? props.kmsKeyId,
474 enableIAMDatabaseAuthentication:
475 props.cluster?.enableIAMDatabaseAuthentication ??
476 props.enableIAMDatabaseAuthentication,
477 enableCloudwatchLogsExports:
478 props.cluster?.enableCloudwatchLogsExports ??
479 props.enableCloudwatchLogsExports,
480 caCertificateIdentifier:
481 props.cluster?.caCertificateIdentifier ??
482 props.caCertificateIdentifier,
483 port: props.cluster?.port ?? props.port,
484 backtrackWindow:
485 props.cluster?.backtrackWindow ?? props.backtrackWindow,
486 monitoringInterval:
487 props.cluster?.monitoringInterval ?? monitoringInterval,
488 monitoringRoleArn:
489 props.cluster?.monitoringRoleArn ?? monitoringRoleArn,
490 enablePerformanceInsights:
491 props.cluster?.enablePerformanceInsights ??
492 props.monitoring?.performanceInsights,
493 serverlessV2ScalingConfiguration:
494 props.cluster?.serverlessV2ScalingConfiguration ??
495 (props.scaling
496 ? {
497 MinCapacity: props.scaling.minCapacity ?? 0.5,
498 MaxCapacity: props.scaling.maxCapacity ?? 1,
499 }
500 : {
501 MinCapacity: 0.5,
502 MaxCapacity: 1,
503 }),
504 masterUserSecretArn: secret.secretArn,
505 tags: mergeTags(commonTags, props.cluster?.tags),
506 ...props.cluster,
507 });
508
509 const defaultInstanceClass =
510 props.instance?.dbInstanceClass ??
511 props.instanceClass ??
512 "db.serverless";
513
514 const writer = yield* DBInstance("Writer", {
515 dbClusterIdentifier: cluster.dbClusterIdentifier,
516 dbInstanceClass: defaultInstanceClass,
517 engine,
518 engineVersion,
519 dbSubnetGroupName: subnetGroup.dbSubnetGroupName,
520 dbParameterGroupName: parameterGroup?.dbParameterGroupName,
521 // No vpcSecurityGroupIds: cluster members inherit security groups from
522 // the DB cluster. Passing them here fails with "The requested DB
523 // Instance will be a member of a DB Cluster. Set vpc security group
524 // for the DB Cluster."
525 publiclyAccessible: props.instance?.publiclyAccessible ?? false,
526 promotionTier: props.instance?.promotionTier ?? 0,
527 autoMinorVersionUpgrade:
528 props.instance?.autoMinorVersionUpgrade ?? true,
529 copyTagsToSnapshot: props.instance?.copyTagsToSnapshot ?? true,
530 monitoringInterval: props.instance?.monitoringInterval ?? monitoringInterval, // prettier-ignore
531 monitoringRoleArn:
532 props.instance?.monitoringRoleArn ?? monitoringRoleArn,
533 enablePerformanceInsights:
534 props.instance?.enablePerformanceInsights ??
535 props.monitoring?.performanceInsights,
536 tags: mergeTags(commonTags, props.instance?.tags),
537 ...props.instance,
538 });
539
540 const readers = yield* Effect.all(
541 Array.from({ length: props.readers ?? 0 }, (_, index) =>
542 DBInstance(`Reader${index + 1}`, {
543 dbClusterIdentifier: cluster.dbClusterIdentifier,
544 dbInstanceClass: defaultInstanceClass,
545 engine,
546 engineVersion,
547 dbSubnetGroupName: subnetGroup.dbSubnetGroupName,
548 dbParameterGroupName: parameterGroup?.dbParameterGroupName,
549 // No vpcSecurityGroupIds: security groups belong on the DB
550 // cluster, not its member instances (see Writer above).
551 publiclyAccessible: props.instance?.publiclyAccessible ?? false,
552 promotionTier: index + 1,
553 autoMinorVersionUpgrade:
554 props.instance?.autoMinorVersionUpgrade ?? true,
555 copyTagsToSnapshot: props.instance?.copyTagsToSnapshot ?? true,
556 monitoringInterval: props.instance?.monitoringInterval ?? monitoringInterval, // prettier-ignore
557 monitoringRoleArn:
558 props.instance?.monitoringRoleArn ?? monitoringRoleArn,
559 enablePerformanceInsights:
560 props.instance?.enablePerformanceInsights ??
561 props.monitoring?.performanceInsights,
562 tags: mergeTags(commonTags, props.instance?.tags),
563 ...props.instance,
564 }),
565 ),
566 { concurrency: "unbounded" },
567 );
568
569 const proxy =
570 proxyConfig === undefined
571 ? undefined
572 : yield* Effect.gen(function* () {
573 const role = yield* IAM.Role("ProxyRole", {
574 assumeRolePolicyDocument: {
575 Version: "2012-10-17",
576 Statement: [
577 {
578 Effect: "Allow",
579 Principal: {
580 Service: "rds.amazonaws.com",
581 },
582 Action: ["sts:AssumeRole"],
583 Resource: ["*"],
584 },
585 ],
586 },
587 inlinePolicies: {
588 ReadSecret: {
589 Version: "2012-10-17",
590 Statement: [
591 {
592 Effect: "Allow",
593 Action: [
594 "secretsmanager:GetSecretValue",
595 "secretsmanager:DescribeSecret",
596 ],
597 Resource: [secret.secretArn],
598 },
599 ],
600 },
601 },
602 tags: mergeTags(commonTags, undefined),
603 });
604
605 const proxy = yield* DBProxy("Proxy", {
606 dbProxyName: proxyConfig.dbProxyName,
607 engineFamily: inferProxyEngineFamily(engine),
608 auth: proxyConfig.auth ?? [
609 {
610 AuthScheme: "SECRETS",
611 SecretArn: secret.secretArn,
612 IAMAuth: "DISABLED",
613 },
614 ],
615 roleArn: role.roleArn,
616 vpcSubnetIds: subnetIds,
617 vpcSecurityGroupIds: securityGroupIds,
618 requireTLS: proxyConfig.requireTLS ?? true,
619 idleClientTimeout: proxyConfig.idleClientTimeout,
620 debugLogging: proxyConfig.debugLogging,
621 endpointNetworkType: proxyConfig.endpointNetworkType,
622 targetConnectionNetworkType:
623 proxyConfig.targetConnectionNetworkType,
624 tags: mergeTags(commonTags, proxyConfig.tags),
625 });
626
627 const targetGroup = yield* DBProxyTargetGroup(
628 "ProxyTargetGroup",
629 {
630 targetGroupName: proxyConfig.targetGroup?.targetGroupName,
631 dbProxyName: proxy.dbProxyName,
632 dbClusterIdentifiers: [cluster.dbClusterIdentifier],
633 dbInstanceIdentifiers:
634 proxyConfig.targetGroup?.dbInstanceIdentifiers,
635 connectionPoolConfig:
636 proxyConfig.targetGroup?.connectionPoolConfig,
637 },
638 );
639
640 const endpoint =
641 proxyConfig.endpoint === undefined
642 ? undefined
643 : yield* DBProxyEndpoint("ProxyEndpoint", {
644 dbProxyName: proxy.dbProxyName,
645 vpcSubnetIds: subnetIds,
646 vpcSecurityGroupIds: securityGroupIds,
647 ...(proxyConfig.endpoint === true
648 ? {}
649 : proxyConfig.endpoint),
650 tags: mergeTags(
651 commonTags,
652 proxyConfig.endpoint === true
653 ? undefined
654 : proxyConfig.endpoint.tags,
655 ),
656 });
657
658 return {
659 role,
660 proxy,
661 targetGroup,
662 endpoint,
663 };
664 });
665
666 return {
667 secret,
668 subnetGroup,
669 clusterParameterGroup,
670 parameterGroup,
671 cluster,
672 writer,
673 readers,
674 instances: [writer, ...readers] as [
675 DBInstanceResource,
676 ...DBInstanceResource[],
677 ],
678 proxy,
679 } satisfies AuroraDatabase as AuroraDatabase;
680 }),
681 );