@fjall/components-infrastructure 31.0.0 → 33.0.0

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.
Files changed (60) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/config/audit.js +2 -1
  3. package/dist/lib/patterns/aws/buildkite/pins.js +1 -2
  4. package/dist/lib/patterns/aws/cdn.d.ts +14 -0
  5. package/dist/lib/patterns/aws/cdn.js +31 -3
  6. package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +8 -0
  7. package/dist/lib/patterns/aws/clickhouseDatabase.js +15 -2
  8. package/dist/lib/patterns/aws/compute.js +4 -2
  9. package/dist/lib/patterns/aws/computeEcs.d.ts +7 -0
  10. package/dist/lib/patterns/aws/computeEcs.js +26 -3
  11. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +25 -2
  12. package/dist/lib/patterns/aws/computeLambda.d.ts +12 -0
  13. package/dist/lib/patterns/aws/computeLambda.js +2 -0
  14. package/dist/lib/patterns/aws/computePropApplicability.js +2 -0
  15. package/dist/lib/patterns/aws/database.d.ts +14 -0
  16. package/dist/lib/patterns/aws/database.js +16 -6
  17. package/dist/lib/patterns/aws/fivetranProxy.d.ts +0 -1
  18. package/dist/lib/patterns/aws/fivetranProxy.js +6 -1
  19. package/dist/lib/patterns/aws/interfaces/pattern.d.ts +7 -7
  20. package/dist/lib/patterns/aws/messaging.js +4 -1
  21. package/dist/lib/patterns/aws/patternDomain.d.ts +1 -1
  22. package/dist/lib/patterns/aws/patternDomain.js +1 -1
  23. package/dist/lib/patterns/aws/payload.js +1 -3
  24. package/dist/lib/patterns/aws/staticSite.d.ts +6 -6
  25. package/dist/lib/patterns/aws/staticSite.js +9 -6
  26. package/dist/lib/resources/aws/cdn/cloudFront.d.ts +2 -0
  27. package/dist/lib/resources/aws/cdn/cloudFront.js +1 -0
  28. package/dist/lib/resources/aws/compute/applicationLoadBalancer.d.ts +10 -0
  29. package/dist/lib/resources/aws/compute/applicationLoadBalancer.js +7 -1
  30. package/dist/lib/resources/aws/compute/ecs.d.ts +11 -0
  31. package/dist/lib/resources/aws/compute/ecs.js +51 -6
  32. package/dist/lib/resources/aws/compute/ecsContext.d.ts +10 -1
  33. package/dist/lib/resources/aws/compute/ecsNetworking.js +3 -1
  34. package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +4 -1
  35. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +31 -8
  36. package/dist/lib/resources/aws/compute/lambda.d.ts +18 -1
  37. package/dist/lib/resources/aws/compute/lambda.js +21 -3
  38. package/dist/lib/resources/aws/database/dynamodb.d.ts +19 -0
  39. package/dist/lib/resources/aws/database/dynamodb.js +13 -2
  40. package/dist/lib/resources/aws/database/rdsAurora.d.ts +11 -0
  41. package/dist/lib/resources/aws/database/rdsAurora.js +24 -3
  42. package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +11 -1
  43. package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +1 -0
  44. package/dist/lib/resources/aws/database/rdsDefaults.d.ts +0 -2
  45. package/dist/lib/resources/aws/database/rdsDefaults.js +0 -2
  46. package/dist/lib/resources/aws/database/rdsInstance.d.ts +11 -0
  47. package/dist/lib/resources/aws/database/rdsInstance.js +19 -4
  48. package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +22 -0
  49. package/dist/lib/resources/aws/logging/accessLogBucket.js +41 -0
  50. package/dist/lib/resources/aws/logging/index.d.ts +1 -0
  51. package/dist/lib/resources/aws/logging/index.js +1 -0
  52. package/dist/lib/resources/aws/messaging/sns.d.ts +14 -5
  53. package/dist/lib/resources/aws/messaging/sns.js +7 -1
  54. package/dist/lib/resources/aws/secrets/index.d.ts +1 -0
  55. package/dist/lib/resources/aws/secrets/index.js +1 -0
  56. package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +47 -0
  57. package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +114 -0
  58. package/dist/lib/utils/backupTierMapping.d.ts +10 -2
  59. package/dist/lib/utils/backupTierMapping.js +3 -1
  60. package/package.json +4 -4
@@ -13,16 +13,16 @@
13
13
  * - Contact-form Lambda + Function URL (when `forms` is set)
14
14
  *
15
15
  * @example
16
- * const site = app.addPattern(PatternFactory.build("LbexcSite", {
16
+ * const site = app.addPattern(PatternFactory.build("StorefrontSite", {
17
17
  * type: "staticsite",
18
- * name: "lbexc",
19
- * source: "../../lbexc-static-export",
18
+ * name: "storefront",
19
+ * source: "../../storefront-static-export",
20
20
  * build: { command: "npm run build", outputDir: "out" },
21
21
  * routing: "multipage",
22
22
  * security: { headers: true },
23
- * domain: "lbexc.com",
24
- * zoneName: "lbexc.com", // or a CLI-injected `managedDomain` binding
25
- * forms: { to: "hello@lbexc.com", corsOrigin: "https://lbexc.com" }
23
+ * domain: "storefront.example",
24
+ * zoneName: "storefront.example", // or a CLI-injected `managedDomain` binding
25
+ * forms: { to: "hello@storefront.example", corsOrigin: "https://storefront.example" }
26
26
  * }));
27
27
  */
28
28
  import { Construct } from "constructs";
@@ -207,6 +207,9 @@ export class StaticSite extends Construct {
207
207
  reservedConcurrentExecutions: forms.maxConcurrency ?? FORMS_DEFAULTS.MAX_CONCURRENCY,
208
208
  lambdaDescription: `Contact-form handler for ${this.props.name}`,
209
209
  roleDescription: `Contact-form send role for ${this.props.name}`,
210
+ // An app workload, not a framework handler: tracing and encryption
211
+ // follow the app backup tier like every factory-built Lambda.
212
+ appBackupTier: this.app.getBackupTier(),
210
213
  enableFunctionUrl: true,
211
214
  functionUrlAuthType: FunctionUrlAuthType.NONE,
212
215
  functionUrlCors: {
@@ -60,6 +60,8 @@ export interface CloudFrontDistributionProps {
60
60
  comment?: string;
61
61
  enableLogging?: boolean;
62
62
  logBucket?: IBucket;
63
+ /** Key prefix for standard logs delivered to `logBucket`. */
64
+ logFilePrefix?: string;
63
65
  priceClass?: CloudFrontPriceClass;
64
66
  /** Adds a CloudFront Function to copy the viewer Host header into X-Forwarded-Host.
65
67
  * Required when the origin is a Lambda Function URL or ALB behind CloudFront,
@@ -110,6 +110,7 @@ export class CloudFrontDistribution extends Construct {
110
110
  comment: props.comment,
111
111
  enableLogging: props.enableLogging,
112
112
  logBucket: props.logBucket,
113
+ logFilePrefix: props.logFilePrefix,
113
114
  priceClass: this.resolvePriceClass(props.priceClass)
114
115
  });
115
116
  // The CDN-qualified export is unconditional and always unique, so an app
@@ -7,6 +7,16 @@ export interface ApplicationLoadBalancerOptions {
7
7
  readonly internetFacing: boolean;
8
8
  readonly loadBalancerName?: string;
9
9
  readonly securityGroup?: ISecurityGroup;
10
+ /**
11
+ * `deletion_protection.enabled` on the load balancer (CDK emits the
12
+ * attribute as `false` when unset, so an unprotected template is unchanged).
13
+ */
14
+ readonly deletionProtection?: boolean;
15
+ /**
16
+ * Deliver access logs to the stack's shared access-log bucket under
17
+ * `alb/<id>/`. The bucket is created on first use.
18
+ */
19
+ readonly accessLogs?: boolean;
10
20
  }
11
21
  /**
12
22
  * Context-free Application Load Balancer factory shared by the ECS cluster path
@@ -2,6 +2,7 @@ import { Duration } from "aws-cdk-lib";
2
2
  import { ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
3
3
  import { SubnetType } from "aws-cdk-lib/aws-ec2";
4
4
  import { DEFAULT_ALB_IDLE_TIMEOUT_SECONDS } from "@fjall/util/httpKeepAlive";
5
+ import { stackAccessLogBucket } from "../logging/accessLogBucket.js";
5
6
  /**
6
7
  * Context-free Application Load Balancer factory shared by the ECS cluster path
7
8
  * and the dev substrate. Subnet placement is derived from `internetFacing`
@@ -9,7 +10,7 @@ import { DEFAULT_ALB_IDLE_TIMEOUT_SECONDS } from "@fjall/util/httpKeepAlive";
9
10
  * ECS path's existing derivation so the extraction is byte-identical.
10
11
  */
11
12
  export function createApplicationLoadBalancer(scope, id, options) {
12
- return new ApplicationLoadBalancer(scope, id, {
13
+ const loadBalancer = new ApplicationLoadBalancer(scope, id, {
13
14
  vpc: options.vpc,
14
15
  internetFacing: options.internetFacing,
15
16
  // Pinned (rather than left at the AWS default of the same value) because
@@ -23,10 +24,15 @@ export function createApplicationLoadBalancer(scope, id, options) {
23
24
  ...(options.loadBalancerName && {
24
25
  loadBalancerName: options.loadBalancerName
25
26
  }),
27
+ deletionProtection: options.deletionProtection,
26
28
  vpcSubnets: {
27
29
  subnetType: options.internetFacing
28
30
  ? SubnetType.PUBLIC
29
31
  : SubnetType.PRIVATE_WITH_EGRESS
30
32
  }
31
33
  });
34
+ if (options.accessLogs === true) {
35
+ loadBalancer.logAccessLogs(stackAccessLogBucket(scope), `alb/${id}`);
36
+ }
37
+ return loadBalancer;
32
38
  }
@@ -3,6 +3,7 @@ import { Connections, type IConnectable } from "aws-cdk-lib/aws-ec2";
3
3
  import { Construct } from "constructs";
4
4
  import type { StackBuilder } from "../base/awsStack.js";
5
5
  import type { ApplicationListener, ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
6
+ import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
6
7
  import { type EcsClusterProps } from "./ecsTypes.js";
7
8
  import { type EcsIngressProfile } from "./ingressProfile.js";
8
9
  export * from "./ecsTypes.js";
@@ -65,6 +66,16 @@ export default class EcsCluster extends Construct implements IConnectable {
65
66
  private outputName;
66
67
  private loadBalancerDisabled;
67
68
  private directAccessEnabled;
69
+ private readonly containerInsightsEnabled;
70
+ /**
71
+ * The stack's shared key when the app backup tier encrypts (resolved once,
72
+ * here); undefined otherwise. Read by the patterns layer for the log
73
+ * groups it creates beside the cluster's own, so the tier is consulted in
74
+ * one place.
75
+ */
76
+ readonly encryptionKey: StackEncryptionKey | undefined;
77
+ private readonly loadBalancerDeletionProtection;
78
+ private readonly loadBalancerAccessLogs;
68
79
  private priorityState;
69
80
  constructor(scope: Construct, id: string, props: EcsClusterProps);
70
81
  private get ctx();
@@ -3,6 +3,9 @@ import { Connections, Port } from "aws-cdk-lib/aws-ec2";
3
3
  import { Construct } from "constructs";
4
4
  import { CfnOutput, Aspects } from "aws-cdk-lib";
5
5
  import { processConnections } from "../../../utils/connections.js";
6
+ import { tierDefault } from "../../../utils/backupTierMapping.js";
7
+ import { envAwareRemovalPolicyDefault } from "../../../utils/removalPolicy.js";
8
+ import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
6
9
  import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../logging/logGroup.js";
7
10
  import { stackScopedExportName } from "../../../utils/exportNaming.js";
8
11
  import { toPascalCase } from "../../../utils/capitaliseString.js";
@@ -84,6 +87,16 @@ export default class EcsCluster extends Construct {
84
87
  outputName;
85
88
  loadBalancerDisabled;
86
89
  directAccessEnabled;
90
+ containerInsightsEnabled;
91
+ /**
92
+ * The stack's shared key when the app backup tier encrypts (resolved once,
93
+ * here); undefined otherwise. Read by the patterns layer for the log
94
+ * groups it creates beside the cluster's own, so the tier is consulted in
95
+ * one place.
96
+ */
97
+ encryptionKey;
98
+ loadBalancerDeletionProtection;
99
+ loadBalancerAccessLogs;
87
100
  // ALB priority management
88
101
  priorityState = {
89
102
  nextPriority: 100,
@@ -98,6 +111,21 @@ export default class EcsCluster extends Construct {
98
111
  this.directAccessEnabled = props.cluster?.directAccess === true;
99
112
  this.loadBalancerDisabled =
100
113
  props.cluster?.loadBalancer === false || this.directAccessEnabled;
114
+ // Tier-owned defaults resolve exactly once, here; every helper reads the
115
+ // resolved value from ctx. An explicit prop is the override.
116
+ const tier = props.appBackupTier;
117
+ this.containerInsightsEnabled =
118
+ props.containerInsights ?? tierDefault(tier, "containerInsights");
119
+ this.encryptionKey = tierDefault(tier, "customerManagedKeys")
120
+ ? StackEncryptionKey.of(this)
121
+ : undefined;
122
+ this.loadBalancerDeletionProtection =
123
+ props.cluster?.loadBalancerDeletionProtection ??
124
+ (tierDefault(tier, "deletionProtection") &&
125
+ envAwareRemovalPolicyDefault() === "RETAIN");
126
+ this.loadBalancerAccessLogs =
127
+ props.cluster?.loadBalancerAccessLogs ??
128
+ tierDefault(tier, "albAccessLogs");
101
129
  validateEcsClusterProps(props, this);
102
130
  this.cluster = this.addCluster(props);
103
131
  for (const serviceProps of props.services) {
@@ -161,7 +189,10 @@ export default class EcsCluster extends Construct {
161
189
  cluster: this.cluster,
162
190
  outputName: this.outputName,
163
191
  loadBalancerDisabled: this.loadBalancerDisabled,
164
- directAccessEnabled: this.directAccessEnabled
192
+ directAccessEnabled: this.directAccessEnabled,
193
+ encryptionKey: this.encryptionKey,
194
+ loadBalancerDeletionProtection: this.loadBalancerDeletionProtection,
195
+ loadBalancerAccessLogs: this.loadBalancerAccessLogs
165
196
  };
166
197
  }
167
198
  /** Get the cluster's load balancer. Undefined if disabled. */
@@ -317,7 +348,7 @@ export default class EcsCluster extends Construct {
317
348
  config: typeof serviceProps.alarms === "object" ? serviceProps.alarms : {},
318
349
  alarmTopic: this.props.alertsTopic,
319
350
  applicationId: this.props.applicationId,
320
- containerInsightsEnabled: this.props.containerInsights === true
351
+ containerInsightsEnabled: this.containerInsightsEnabled
321
352
  });
322
353
  if (serviceProps.logAlarms && serviceProps.logAlarms.length > 0) {
323
354
  createLogPatternAlarms({
@@ -385,18 +416,32 @@ export default class EcsCluster extends Construct {
385
416
  const needsFargate = this.anyServiceUsesFargate();
386
417
  // Every service enables execute command; without an OVERRIDE here the
387
418
  // exec session output interleaves into the container app log groups
388
- // (DEFAULT mode). No kmsKey — session encryption is tier-gated.
389
- const execAuditLogGroup = new LogGroup(this, `${props.clusterName}ExecAuditLogGroup`, { retention: DEFAULT_FRAMEWORK_LOG_RETENTION });
419
+ // (DEFAULT mode). Session encryption follows the tier: the shared stack
420
+ // key encrypts the session AND its audit log group (ECS requires the
421
+ // audit group to be CMK-encrypted before it will encrypt the session).
422
+ const encryptionKey = this.encryptionKey;
423
+ const execAuditLogGroup = new LogGroup(this, `${props.clusterName}ExecAuditLogGroup`, {
424
+ retention: DEFAULT_FRAMEWORK_LOG_RETENTION,
425
+ ...(encryptionKey !== undefined && {
426
+ encryptionKey: encryptionKey.forLogGroups()
427
+ })
428
+ });
390
429
  const cluster = new CdkCluster(this, `${props.clusterName}Cluster`, {
391
430
  vpc: props.vpc,
392
431
  clusterName: props.clusterName,
393
- containerInsightsV2: props.containerInsights === true
432
+ containerInsightsV2: this.containerInsightsEnabled
394
433
  ? ContainerInsights.ENABLED
395
434
  : ContainerInsights.DISABLED,
396
435
  enableFargateCapacityProviders: needsFargate,
397
436
  executeCommandConfiguration: {
437
+ ...(encryptionKey !== undefined && { kmsKey: encryptionKey.key }),
398
438
  logging: ExecuteCommandLogging.OVERRIDE,
399
- logConfiguration: { cloudWatchLogGroup: execAuditLogGroup }
439
+ logConfiguration: {
440
+ cloudWatchLogGroup: execAuditLogGroup,
441
+ ...(encryptionKey !== undefined && {
442
+ cloudWatchEncryptionEnabled: true
443
+ })
444
+ }
400
445
  }
401
446
  });
402
447
  new CfnOutput(this, `${this.outputName}DeployableCluster`, {
@@ -1,7 +1,12 @@
1
1
  import type { Construct } from "constructs";
2
2
  import type { Cluster as CdkCluster } from "aws-cdk-lib/aws-ecs";
3
+ import type { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
3
4
  import type { EcsClusterProps } from "./ecsTypes.js";
4
- /** Shared state passed from EcsCluster to extracted helper functions. */
5
+ /**
6
+ * Shared state passed from EcsCluster to extracted helper functions. The
7
+ * tier-owned knobs are resolved once in the constructor and read here as
8
+ * plain values — helpers never consult the tier themselves.
9
+ */
5
10
  export interface EcsConstructContext {
6
11
  scope: Construct;
7
12
  props: EcsClusterProps;
@@ -9,4 +14,8 @@ export interface EcsConstructContext {
9
14
  outputName: string;
10
15
  loadBalancerDisabled: boolean;
11
16
  directAccessEnabled: boolean;
17
+ /** The stack's shared key when the tier encrypts; undefined otherwise. */
18
+ encryptionKey: StackEncryptionKey | undefined;
19
+ loadBalancerDeletionProtection: boolean;
20
+ loadBalancerAccessLogs: boolean;
12
21
  }
@@ -44,7 +44,9 @@ export function addLoadBalancer(ctx, anyServiceUsesEc2, asgSecurityGroup) {
44
44
  loadBalancerName: truncatedLoadBalancerName,
45
45
  ...(loadBalancerSecurityGroup && {
46
46
  securityGroup: loadBalancerSecurityGroup
47
- })
47
+ }),
48
+ deletionProtection: ctx.loadBalancerDeletionProtection,
49
+ accessLogs: ctx.loadBalancerAccessLogs
48
50
  });
49
51
  new CfnOutput(ctx.scope, `${ctx.outputName}LoadBalancerDnsName`, {
50
52
  key: `${ctx.outputName}LoadBalancerDnsName`,
@@ -148,7 +148,10 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
148
148
  // Removal: env-aware wrapper default (D17) — production RETAIN keeps
149
149
  // fail-closed rollback evidence; non-production DESTROY avoids orphans.
150
150
  const logGroup = new LogGroup(ctx.scope, `${ctx.props.clusterName}${serviceName}LogGroup`, {
151
- retention: DEFAULT_LOG_RETENTION
151
+ retention: DEFAULT_LOG_RETENTION,
152
+ ...(ctx.encryptionKey !== undefined && {
153
+ encryptionKey: ctx.encryptionKey.forLogGroups()
154
+ })
152
155
  });
153
156
  for (const containerConfig of serviceProps.containers) {
154
157
  const image = getContainerImage(ctx, serviceName, containerConfig, serviceProps);
@@ -16,6 +16,7 @@ import { type Role } from "aws-cdk-lib/aws-iam";
16
16
  import { type HostedZone as FjallHostedZone } from "../networking/hostedZone.js";
17
17
  import { type Certificate } from "aws-cdk-lib/aws-certificatemanager";
18
18
  import { type ConnectionSpec } from "../../../utils/connector.js";
19
+ import type { BackupTier } from "../../../utils/backupTierMapping.js";
19
20
  import { type RemoteConnectionSpec } from "./ecsRemoteConnections.js";
20
21
  import { type SecretImport } from "../secrets/index.js";
21
22
  import type { ManagedDomainBinding, ManagedDomainExports } from "../../../utils/domainTypes.js";
@@ -472,6 +473,20 @@ export interface EcsClusterClusterConfig {
472
473
  * by the patterns layer (AC26 — `EcsClusterConfig.securityGroup`).
473
474
  */
474
475
  securityGroup?: ISecurityGroup;
476
+ /**
477
+ * Deletion protection on the shared load balancer. Default: the app backup
478
+ * tier's `deletionProtection` column, applied only when the stack's
479
+ * environment resolves to production — a dev or staging stack is torn
480
+ * down by design. The destroy pre-flight switches it off before a
481
+ * consented destroy.
482
+ */
483
+ loadBalancerDeletionProtection?: boolean;
484
+ /**
485
+ * Access logs from the shared load balancer, delivered to the stack's
486
+ * lifecycle-expired access-log bucket under `alb/<load balancer id>/`.
487
+ * Default: the app backup tier's `albAccessLogs` column.
488
+ */
489
+ loadBalancerAccessLogs?: boolean;
475
490
  }
476
491
  /**
477
492
  * Routing configuration for path/host-based routing on the ALB.
@@ -749,15 +764,23 @@ export interface EcsClusterProps {
749
764
  */
750
765
  taskStopWatchdog?: false;
751
766
  /**
752
- * CloudWatch Container Insights (default off). Enabling it bills
753
- * ~$0.30/metric-month of custom `ECS/ContainerInsights` metrics per cluster
754
- * plus performance-log ingestion. The per-service running-tasks alarm is
755
- * the one construct consumer of that namespace (`RunningTaskCount` exists
756
- * nowhere else) and is skipped when insights are off; every other alarm
757
- * sits on `AWS/ECS` and `Fjall/ECS`. Explicit opt-in for clusters whose
758
- * operators want the running-tasks alarm or the per-task console breakdown.
767
+ * CloudWatch Container Insights. Default: the app backup tier's
768
+ * `containerInsights` column (on at enterprise, off below). Enabling it
769
+ * bills ~$0.30/metric-month of custom `ECS/ContainerInsights` metrics per
770
+ * cluster plus performance-log ingestion. The per-service running-tasks
771
+ * alarm is the one construct consumer of that namespace (`RunningTaskCount`
772
+ * exists nowhere else) and is skipped when insights are off; every other
773
+ * alarm sits on `AWS/ECS` and `Fjall/ECS`. `false` is an explicit,
774
+ * auditable opt-out at enterprise.
775
+ */
776
+ containerInsights?: boolean;
777
+ /**
778
+ * The owning app's backup tier, which the tier-owned defaults (Container
779
+ * Insights, exec-session and log-group encryption, load-balancer deletion
780
+ * protection and access logs) follow. `ComputeFactory` fills it from the
781
+ * App; pass it only for direct `new EcsCluster(...)` construction.
759
782
  */
760
- containerInsights?: true;
783
+ appBackupTier?: BackupTier;
761
784
  }
762
785
  /**
763
786
  * Data tracked for each service in the cluster.
@@ -3,6 +3,7 @@ import { type Bucket } from "aws-cdk-lib/aws-s3";
3
3
  import { PolicyStatement, type IRole } from "aws-cdk-lib/aws-iam";
4
4
  import { type IVpc } from "aws-cdk-lib/aws-ec2";
5
5
  import type { RetentionDays } from "aws-cdk-lib/aws-logs";
6
+ import { type BackupTier } from "../../../utils/backupTierMapping.js";
6
7
  import { type IQueue } from "aws-cdk-lib/aws-sqs";
7
8
  import { type ITable } from "aws-cdk-lib/aws-dynamodb";
8
9
  import { type Construct } from "constructs";
@@ -92,8 +93,24 @@ export interface LambdaFunctionProps {
92
93
  alarms?: LambdaAlarmThresholds | false;
93
94
  /** Application ID for alarm tagging. */
94
95
  applicationId?: string;
96
+ /**
97
+ * Active X-Ray tracing. Default: the app backup tier's `lambdaTracing`
98
+ * column (on at enterprise).
99
+ */
100
+ tracing?: boolean;
101
+ /**
102
+ * The owning app's backup tier, which the tier-owned defaults (tracing,
103
+ * environment-variable and log-group encryption) follow. `ComputeFactory`
104
+ * fills it from the App; pass it only for direct `new LambdaFunction(...)`
105
+ * construction.
106
+ */
107
+ appBackupTier?: BackupTier;
95
108
  }
96
- export interface SingletonFunctionProps extends LambdaFunctionProps {
109
+ /**
110
+ * Framework custom-resource handlers are tier-agnostic: the tier-owned
111
+ * knobs are excluded at the type rather than silently ignored.
112
+ */
113
+ export interface SingletonFunctionProps extends Omit<LambdaFunctionProps, "tracing" | "appBackupTier"> {
97
114
  uuid?: string;
98
115
  ephemeralStorageSize?: number;
99
116
  }
@@ -1,5 +1,5 @@
1
1
  import { Annotations, CfnOutput, Duration, Stack, Size } from "aws-cdk-lib";
2
- import { SingletonFunction as singletonFunction, Function, Code, Architecture, FunctionUrlAuthType, StartingPosition, LayerVersion } from "aws-cdk-lib/aws-lambda";
2
+ import { SingletonFunction as singletonFunction, Function, Code, Architecture, FunctionUrlAuthType, StartingPosition, LayerVersion, Tracing } from "aws-cdk-lib/aws-lambda";
3
3
  import { FactName } from "aws-cdk-lib/region-info";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -8,6 +8,8 @@ import { SqsEventSource, DynamoEventSource, S3EventSource } from "aws-cdk-lib/aw
8
8
  import { EventType } from "aws-cdk-lib/aws-s3";
9
9
  import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
10
10
  import { DEFAULT_FRAMEWORK_LOG_RETENTION, LogGroup } from "../logging/logGroup.js";
11
+ import { tierDefault } from "../../../utils/backupTierMapping.js";
12
+ import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
11
13
  import { Queue, QueueEncryption } from "aws-cdk-lib/aws-sqs";
12
14
  import { SQS_LIMITS } from "../messaging/sqs.js";
13
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -167,12 +169,25 @@ export class LambdaFunction extends Function {
167
169
  asyncFailureQueueWired;
168
170
  constructor(scope, id, props) {
169
171
  validateRuntimeNotDeprecated(props.runtime, id);
172
+ // Both are Fjall props, not CDK ones: `tracing` collides with the CDK
173
+ // enum of the same name and `appBackupTier` is unknown to CDK.
174
+ const { tracing, appBackupTier, ...cdkProps } = props;
170
175
  const vpcSubnets = props.vpc
171
176
  ? { subnetType: resolvePrivateSubnetType(props.vpc) }
172
177
  : undefined;
178
+ const tracingEnabled = tracing ?? tierDefault(appBackupTier, "lambdaTracing");
179
+ // Grant-based: Lambda encrypts the environment through a KMS grant the
180
+ // deploying role creates, so the execution role needs no key policy.
181
+ const encryptionKey = tierDefault(appBackupTier, "customerManagedKeys")
182
+ ? StackEncryptionKey.of(scope)
183
+ : undefined;
173
184
  super(scope, id, {
174
- ...props,
185
+ ...cdkProps,
175
186
  vpcSubnets,
187
+ ...(tracingEnabled && { tracing: Tracing.ACTIVE }),
188
+ ...(encryptionKey !== undefined && {
189
+ environmentEncryption: encryptionKey.key
190
+ }),
176
191
  timeout: Duration.seconds(props.timeout ?? LAMBDA_DEFAULT_TIMEOUT_SECONDS),
177
192
  memorySize: props.memorySize ?? 128,
178
193
  ephemeralStorageSize: props.ephemeralStorageSize
@@ -181,7 +196,10 @@ export class LambdaFunction extends Function {
181
196
  description: props.lambdaDescription ?? `${id} Lambda`,
182
197
  environment: props.environment,
183
198
  logGroup: new LogGroup(scope, `${id}LogGroup`, {
184
- retention: props.logGroupRetention ?? DEFAULT_FRAMEWORK_LOG_RETENTION
199
+ retention: props.logGroupRetention ?? DEFAULT_FRAMEWORK_LOG_RETENTION,
200
+ ...(encryptionKey !== undefined && {
201
+ encryptionKey: encryptionKey.forLogGroups()
202
+ })
185
203
  })
186
204
  });
187
205
  this.asyncFailureQueueProp = props.asyncFailureQueue;
@@ -1,4 +1,5 @@
1
1
  import { Construct } from "constructs";
2
+ import { type BackupTier } from "../../../utils/backupTierMapping.js";
2
3
  import { type ITable } from "aws-cdk-lib/aws-dynamodb";
3
4
  import { type IGrantable, type Grant } from "aws-cdk-lib/aws-iam";
4
5
  export type DynamoDBAttributeType = "S" | "N" | "B";
@@ -24,9 +25,27 @@ export interface DynamoDBTableProps {
24
25
  timeToLiveAttribute?: string;
25
26
  stream?: "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | "KEYS_ONLY";
26
27
  pointInTimeRecovery?: boolean;
28
+ /**
29
+ * Encryption at rest. Default: the app backup tier's `customerManagedKeys`
30
+ * column — `CUSTOMER_MANAGED` at enterprise, `AWS_MANAGED` otherwise.
31
+ * `CUSTOMER_MANAGED` always encrypts with the stack's shared
32
+ * [[StackEncryptionKey]]; switching a live table's key is an in-place
33
+ * update.
34
+ */
27
35
  encryption?: "AWS_OWNED" | "AWS_MANAGED" | "CUSTOMER_MANAGED";
36
+ /**
37
+ * DynamoDB `DeletionProtectionEnabled`. Default: the app backup tier's
38
+ * `deletionProtection` column (on at resilient and enterprise). The
39
+ * destroy pre-flight switches it off before a consented destroy.
40
+ */
28
41
  deletionProtection?: boolean;
29
42
  removalPolicy?: "DESTROY" | "RETAIN" | "SNAPSHOT";
43
+ /**
44
+ * The owning app's backup tier, which the tier-owned defaults (encryption,
45
+ * deletion protection) follow. `DatabaseFactory` fills it from the App;
46
+ * pass it only for direct `new DynamoDBTable(...)` construction.
47
+ */
48
+ appBackupTier?: BackupTier;
30
49
  }
31
50
  export declare class DynamoDBTable extends Construct {
32
51
  readonly id: string;
@@ -1,6 +1,8 @@
1
1
  import { Construct } from "constructs";
2
2
  import { CfnOutput } from "aws-cdk-lib";
3
+ import { tierDefault } from "../../../utils/backupTierMapping.js";
3
4
  import { toRemovalPolicy } from "../../../utils/removalPolicy.js";
5
+ import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
4
6
  import { Table, AttributeType, BillingMode, StreamViewType, ProjectionType, TableEncryption } from "aws-cdk-lib/aws-dynamodb";
5
7
  function toAttributeType(type) {
6
8
  switch (type) {
@@ -64,6 +66,12 @@ export class DynamoDBTable extends Construct {
64
66
  constructor(scope, id, props) {
65
67
  super(scope, id);
66
68
  this.id = id;
69
+ const encryption = props.encryption ??
70
+ (tierDefault(props.appBackupTier, "customerManagedKeys")
71
+ ? "CUSTOMER_MANAGED"
72
+ : "AWS_MANAGED");
73
+ const deletionProtection = props.deletionProtection ??
74
+ tierDefault(props.appBackupTier, "deletionProtection");
67
75
  this.table = new Table(this, `${id}Table`, {
68
76
  tableName: props.tableName,
69
77
  partitionKey: {
@@ -83,8 +91,11 @@ export class DynamoDBTable extends Construct {
83
91
  pointInTimeRecoverySpecification: {
84
92
  pointInTimeRecoveryEnabled: props.pointInTimeRecovery ?? true
85
93
  },
86
- encryption: toTableEncryption(props.encryption),
87
- deletionProtection: props.deletionProtection ?? false,
94
+ encryption: toTableEncryption(encryption),
95
+ ...(encryption === "CUSTOMER_MANAGED" && {
96
+ encryptionKey: StackEncryptionKey.of(this).key
97
+ }),
98
+ deletionProtection,
88
99
  removalPolicy: toRemovalPolicy(props.removalPolicy),
89
100
  timeToLiveAttribute: props.timeToLiveAttribute
90
101
  });
@@ -3,6 +3,7 @@ import { Connections, type IConnectable, type IVpc } from "aws-cdk-lib/aws-ec2";
3
3
  import { DatabaseCluster, type CfnDBCluster, type IClusterEngine } from "aws-cdk-lib/aws-rds";
4
4
  import { Construct } from "constructs";
5
5
  import { Secret } from "../secrets/index.js";
6
+ import { type BackupTier } from "../../../utils/backupTierMapping.js";
6
7
  import type { ITopic } from "aws-cdk-lib/aws-sns";
7
8
  import { type RdsAlarmThresholds } from "../monitoring/index.js";
8
9
  import { type EngineConfig, type ProxyConfig, type CredentialsConfig, type AuroraEncryptionConfig, type AuroraWriterConfig, type AuroraReadersConfig, type DatabaseInsightsConfig, type SnapshotTarget } from "../../../utils/databaseTypes.js";
@@ -40,7 +41,17 @@ interface RdsProps {
40
41
  proxy?: ProxyConfig | false;
41
42
  credentials?: CredentialsConfig;
42
43
  encryption?: AuroraEncryptionConfig;
44
+ /**
45
+ * Automated backup retention in days. Defaults to the app backup tier's
46
+ * column (`tierDefault(appBackupTier, "rdsBackupRetentionDays")`).
47
+ */
43
48
  backupRetention?: number;
49
+ /**
50
+ * The owning app's backup tier (`App.getApp({ backup: { tier } })`), which
51
+ * the retention default follows. `DatabaseFactory` fills it from the App;
52
+ * pass it only for direct `new RdsAurora(...)` construction.
53
+ */
54
+ appBackupTier?: BackupTier;
44
55
  monitoringInterval?: Duration;
45
56
  preferredMaintenanceWindow?: string;
46
57
  port?: number;
@@ -6,8 +6,9 @@ import { Construct } from "constructs";
6
6
  import { SecurityGroup } from "../networking/securityGroup.js";
7
7
  import { Secret } from "../secrets/index.js";
8
8
  import { ResourceNaming } from "../../../utils/resourceNaming.js";
9
+ import { tierDefault } from "../../../utils/backupTierMapping.js";
9
10
  import { addProxyCfnOutput } from "./rdsProxyOutput.js";
10
- import { getDatabaseInsightsRetention } from "./index.js";
11
+ import { getCdkDatabaseInsightsMode, getDatabaseInsightsRetention } from "./index.js";
11
12
  import { RDS_DEFAULTS, SERVERLESS_V2_AUTO_PAUSE_MIN_SECONDS, SERVERLESS_V2_AUTO_PAUSE_MAX_SECONDS, MIN_AUTO_PAUSE_POSTGRES_VERSION } from "./rdsDefaults.js";
12
13
  import { createRdsAlarms } from "../monitoring/index.js";
13
14
  import { DEFAULT_POSTGRES_ENGINE_CONFIG, resolveDatabaseInsights, resolveStorageEncryptionKey, resolvePerformanceInsightsKey, addMultiUserSecretRotation, warnIfSnapshotUsernameAssumed } from "./rdsHelpers.js";
@@ -138,8 +139,26 @@ export class RdsAurora extends Construct {
138
139
  const writerConfig = props.writer ?? {};
139
140
  const writerPI = writerConfig.enableDatabaseInsights ?? piEnabled;
140
141
  const writerSuffix = writerConfig.identifierSuffix;
141
- const diMode = piConfig?.mode ?? "standard";
142
+ // The tier owns the Database Insights mode; an explicit `mode` wins.
143
+ const diMode = piConfig?.mode ??
144
+ tierDefault(props.appBackupTier, "databaseInsightsMode");
142
145
  const performanceInsightsRetention = getDatabaseInsightsRetention(diMode);
146
+ // Advanced mode is a cluster-level setting that CDK validates against
147
+ // every instance: the cluster must carry Performance Insights with the
148
+ // same 15-month retention and key the instances do, or synth throws a
149
+ // retention mismatch. Emitted only when it applies, so a standard
150
+ // template stays byte-identical.
151
+ const advancedInsights = piEnabled && diMode === "advanced";
152
+ const clusterInsightsProps = advancedInsights
153
+ ? {
154
+ enablePerformanceInsights: true,
155
+ performanceInsightRetention: performanceInsightsRetention,
156
+ ...(performanceInsightsKey && {
157
+ performanceInsightEncryptionKey: performanceInsightsKey
158
+ }),
159
+ databaseInsightsMode: getCdkDatabaseInsightsMode(diMode)
160
+ }
161
+ : {};
143
162
  const writer = ClusterInstance.serverlessV2(`${this.databaseNameValue}Writer`, {
144
163
  enablePerformanceInsights: writerPI,
145
164
  performanceInsightEncryptionKey: writerPI
@@ -170,7 +189,8 @@ export class RdsAurora extends Construct {
170
189
  ? { subnetType: SubnetType.PUBLIC }
171
190
  : { subnetType: SubnetType.PRIVATE_WITH_EGRESS };
172
191
  this.backupRetentionDaysValue =
173
- props.backupRetention ?? RDS_DEFAULTS.BACKUP_RETENTION_DEFAULT_DAYS;
192
+ props.backupRetention ??
193
+ tierDefault(props.appBackupTier, "rdsBackupRetentionDays");
174
194
  // Common props shared across all cluster creation paths
175
195
  const baseClusterProps = {
176
196
  vpc: props.vpc,
@@ -194,6 +214,7 @@ export class RdsAurora extends Construct {
194
214
  removalPolicy: props.removalPolicy ?? RemovalPolicy.SNAPSHOT,
195
215
  deletionProtection: props.deletionProtection ?? true,
196
216
  iamAuthentication: true,
217
+ ...clusterInsightsProps,
197
218
  ...(props.serverlessV2MinCapacity !== undefined && {
198
219
  serverlessV2MinCapacity: props.serverlessV2MinCapacity
199
220
  }),
@@ -6,6 +6,7 @@ import { type ITopic } from "aws-cdk-lib/aws-sns";
6
6
  import { type Secret } from "../secrets/index.js";
7
7
  import { type RdsAlarmThresholds } from "../monitoring/index.js";
8
8
  import { type EngineConfig, type ProxyConfig, type CredentialsConfig, type AuroraEncryptionConfig, type AuroraWriterConfig, type AuroraReadersConfig, type DatabaseInsightsConfig, type SnapshotTarget } from "../../../utils/databaseTypes.js";
9
+ import { type BackupTier } from "../../../utils/backupTierMapping.js";
9
10
  interface RdsAuroraGlobalProps {
10
11
  vpc: IVpc;
11
12
  databaseName: string;
@@ -37,8 +38,17 @@ interface RdsAuroraGlobalProps {
37
38
  credentials?: CredentialsConfig;
38
39
  /** Encryption configuration */
39
40
  encryption?: AuroraEncryptionConfig;
40
- /** Backup retention in days. Default: 14 */
41
+ /**
42
+ * Backup retention in days. Defaults to the app backup tier's column
43
+ * (`tierDefault(appBackupTier, "rdsBackupRetentionDays")`) on every
44
+ * regional cluster.
45
+ */
41
46
  backupRetention?: number;
47
+ /**
48
+ * The owning app's backup tier, forwarded to each regional `RdsAurora`.
49
+ * `DatabaseFactory` fills it from the App.
50
+ */
51
+ appBackupTier?: BackupTier;
42
52
  /** CloudWatch monitoring interval. Default: 1 minute */
43
53
  monitoringInterval?: Duration;
44
54
  /** Preferred maintenance window. Default: "Sat:12:30-Sat:20:30" */
@@ -83,6 +83,7 @@ export class RdsAuroraGlobal extends Construct {
83
83
  encryption: this.props.encryption,
84
84
  // Operational configuration
85
85
  backupRetention: this.props.backupRetention,
86
+ appBackupTier: this.props.appBackupTier,
86
87
  monitoringInterval: this.props.monitoringInterval,
87
88
  preferredMaintenanceWindow: this.props.preferredMaintenanceWindow,
88
89
  port: this.props.port,
@@ -4,8 +4,6 @@ export declare const RDS_DEFAULTS: Readonly<{
4
4
  readonly PREFERRED_MAINTENANCE_WINDOW: "Sat:12:30-Sat:20:30";
5
5
  /** 1 minute — balances monitoring granularity against CloudWatch costs */
6
6
  readonly MONITORING_INTERVAL: Duration;
7
- /** Default backup retention in days — applied uniformly across Aurora and RDS Instance */
8
- readonly BACKUP_RETENTION_DEFAULT_DAYS: 14;
9
7
  /** Default instance type — primary and read-replica must stay in lockstep */
10
8
  readonly DEFAULT_INSTANCE_TYPE: "t4g.large";
11
9
  /** Default storage autoscaling ceiling (GiB) — applied to primary and replica */