@fjall/components-infrastructure 32.0.0 → 33.1.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 (88) hide show
  1. package/README.md +15 -76
  2. package/dist/lib/app.d.ts +1 -1
  3. package/dist/lib/config/audit.js +2 -1
  4. package/dist/lib/config/aws/identityCentreConfig.d.ts +1 -1
  5. package/dist/lib/config/aws/identityCentreConfig.js +1 -1
  6. package/dist/lib/config/aws/scpPreset.js +1 -2
  7. package/dist/lib/lambda-assets/cert-generator/asset/index.js +126 -0
  8. package/dist/lib/patterns/aws/buildkite/buildkite.d.ts +1 -1
  9. package/dist/lib/patterns/aws/buildkite/buildkite.js +1 -1
  10. package/dist/lib/patterns/aws/buildkite/factory.d.ts +2 -2
  11. package/dist/lib/patterns/aws/buildkite/pins.d.ts +2 -2
  12. package/dist/lib/patterns/aws/buildkite/pins.js +3 -4
  13. package/dist/lib/patterns/aws/buildkite/schema.d.ts +1 -1
  14. package/dist/lib/patterns/aws/buildkite/schema.js +1 -1
  15. package/dist/lib/patterns/aws/buildkite/userData.d.ts +1 -1
  16. package/dist/lib/patterns/aws/buildkite/userData.js +1 -1
  17. package/dist/lib/patterns/aws/buildkite.d.ts +1 -1
  18. package/dist/lib/patterns/aws/buildkite.js +1 -1
  19. package/dist/lib/patterns/aws/cdn.d.ts +14 -0
  20. package/dist/lib/patterns/aws/cdn.js +31 -3
  21. package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +8 -0
  22. package/dist/lib/patterns/aws/clickhouseDatabase.js +19 -3
  23. package/dist/lib/patterns/aws/clickhouseTls/types.d.ts +1 -1
  24. package/dist/lib/patterns/aws/compute.js +4 -2
  25. package/dist/lib/patterns/aws/computeEcs.d.ts +7 -0
  26. package/dist/lib/patterns/aws/computeEcs.js +26 -3
  27. package/dist/lib/patterns/aws/computeEcsTypes.d.ts +30 -7
  28. package/dist/lib/patterns/aws/computeLambda.d.ts +12 -0
  29. package/dist/lib/patterns/aws/computeLambda.js +2 -0
  30. package/dist/lib/patterns/aws/computePropApplicability.js +2 -0
  31. package/dist/lib/patterns/aws/database.d.ts +14 -0
  32. package/dist/lib/patterns/aws/database.js +16 -6
  33. package/dist/lib/patterns/aws/interfaces/pattern.d.ts +6 -6
  34. package/dist/lib/patterns/aws/messaging.js +4 -1
  35. package/dist/lib/patterns/aws/patternDomain.d.ts +1 -1
  36. package/dist/lib/patterns/aws/patternDomain.js +1 -1
  37. package/dist/lib/patterns/aws/payload.js +7 -5
  38. package/dist/lib/patterns/aws/staticSite.d.ts +6 -6
  39. package/dist/lib/patterns/aws/staticSite.js +9 -6
  40. package/dist/lib/patterns/aws/storage.d.ts +8 -0
  41. package/dist/lib/patterns/aws/storage.js +1 -0
  42. package/dist/lib/resources/aws/cdn/cloudFront.d.ts +2 -0
  43. package/dist/lib/resources/aws/cdn/cloudFront.js +1 -0
  44. package/dist/lib/resources/aws/cdn/responseHeadersPolicy.d.ts +1 -1
  45. package/dist/lib/resources/aws/cdn/responseHeadersPolicy.js +1 -1
  46. package/dist/lib/resources/aws/compute/applicationLoadBalancer.d.ts +10 -0
  47. package/dist/lib/resources/aws/compute/applicationLoadBalancer.js +7 -1
  48. package/dist/lib/resources/aws/compute/ec2.d.ts +2 -2
  49. package/dist/lib/resources/aws/compute/ec2.js +1 -1
  50. package/dist/lib/resources/aws/compute/ecs.d.ts +11 -0
  51. package/dist/lib/resources/aws/compute/ecs.js +51 -6
  52. package/dist/lib/resources/aws/compute/ecsContext.d.ts +10 -1
  53. package/dist/lib/resources/aws/compute/ecsNetworking.js +3 -1
  54. package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +4 -1
  55. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +31 -8
  56. package/dist/lib/resources/aws/compute/lambda.d.ts +18 -1
  57. package/dist/lib/resources/aws/compute/lambda.js +21 -3
  58. package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +5 -5
  59. package/dist/lib/resources/aws/database/clickhouseConstants.js +5 -5
  60. package/dist/lib/resources/aws/database/dynamodb.d.ts +19 -0
  61. package/dist/lib/resources/aws/database/dynamodb.js +13 -2
  62. package/dist/lib/resources/aws/database/rdsAurora.d.ts +11 -0
  63. package/dist/lib/resources/aws/database/rdsAurora.js +24 -3
  64. package/dist/lib/resources/aws/database/rdsAuroraGlobal.d.ts +11 -1
  65. package/dist/lib/resources/aws/database/rdsAuroraGlobal.js +1 -0
  66. package/dist/lib/resources/aws/database/rdsDefaults.d.ts +0 -2
  67. package/dist/lib/resources/aws/database/rdsDefaults.js +0 -2
  68. package/dist/lib/resources/aws/database/rdsInstance.d.ts +11 -0
  69. package/dist/lib/resources/aws/database/rdsInstance.js +19 -4
  70. package/dist/lib/resources/aws/iam/identityCenter/user.d.ts +1 -1
  71. package/dist/lib/resources/aws/iam/identityCenter/user.js +1 -1
  72. package/dist/lib/resources/aws/logging/accessLogBucket.d.ts +22 -0
  73. package/dist/lib/resources/aws/logging/accessLogBucket.js +41 -0
  74. package/dist/lib/resources/aws/logging/index.d.ts +1 -0
  75. package/dist/lib/resources/aws/logging/index.js +1 -0
  76. package/dist/lib/resources/aws/messaging/sns.d.ts +14 -5
  77. package/dist/lib/resources/aws/messaging/sns.js +7 -1
  78. package/dist/lib/resources/aws/secrets/index.d.ts +1 -0
  79. package/dist/lib/resources/aws/secrets/index.js +1 -0
  80. package/dist/lib/resources/aws/secrets/stackEncryptionKey.d.ts +47 -0
  81. package/dist/lib/resources/aws/secrets/stackEncryptionKey.js +114 -0
  82. package/dist/lib/resources/aws/storage/bucketDeployment.d.ts +2 -3
  83. package/dist/lib/resources/aws/storage/bucketDeployment.js +2 -3
  84. package/dist/lib/resources/aws/storage/s3.d.ts +13 -1
  85. package/dist/lib/resources/aws/storage/s3.js +6 -3
  86. package/dist/lib/utils/backupTierMapping.d.ts +10 -2
  87. package/dist/lib/utils/backupTierMapping.js +3 -1
  88. package/package.json +5 -4
@@ -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;
@@ -155,7 +155,7 @@ export declare const CLICKHOUSE_NATIVE_PORT = 9000;
155
155
  export declare const CLICKHOUSE_PROMETHEUS_PORT = 9363;
156
156
  /** TLS-mode ClickHouse ports.
157
157
  * HTTPS replaces 8123; secure native protocol replaces 9000.
158
- * See aiDocs/patterns/clickhouse-tls-pattern.md § "The contract". */
158
+ * See the ClickHouse TLS pattern, § "The contract". */
159
159
  export declare const CLICKHOUSE_HTTPS_PORT = 8443;
160
160
  export declare const CLICKHOUSE_TCP_SECURE_PORT = 9440;
161
161
  /** Mount path inside the ClickHouse container for the materialised TLS cert
@@ -188,8 +188,8 @@ export declare function clickHouseUserSecretName(userName: string): string;
188
188
  * Canonical source is `@fjall/util/migration § userPasswordEnvName` — re-exported
189
189
  * here so the construct-side import chain stays unchanged while the runner
190
190
  * (`webapp/scripts/migration-runner.mjs`) and any future consumer share a single
191
- * declaration. Coupled values per `.claude/rules/code-quality.md § "Coupled values:
192
- * shared source at 2 occurrences"`. */
191
+ * declaration. Coupled values (the shared-source-at-two-occurrences
192
+ * rule). */
193
193
  export { userPasswordEnvName } from "@fjall/util/migration";
194
194
  /** Env var name carrying the SHA-256 of a user's plaintext password.
195
195
  * The container entrypoint wrapper derives this name from the
@@ -305,8 +305,8 @@ export declare const BACKUP_RETENTION_DAYS = 14;
305
305
  * wrote can be read back. Dropped on the way in and on the way out; never
306
306
  * queried by the application. Canonical source is `@fjall/util/migration`
307
307
  * because `@fjall/clickhouse` grants `fjall_maintenance` DROP on exactly
308
- * this database — coupled values per `.claude/rules/code-quality.md §
309
- * "Coupled values: shared source at 2 occurrences"`. */
308
+ * this database — coupled values (the shared-source-at-two-occurrences
309
+ * rule). */
310
310
  export { CLICKHOUSE_BACKUP_SCRATCH_DATABASE } from "@fjall/util/migration";
311
311
  /** The two scheduled maintenance jobs that authenticate as
312
312
  * `fjall_maintenance`. Drives the per-job log-group → metric-filter wiring
@@ -175,7 +175,7 @@ export const CLICKHOUSE_NATIVE_PORT = 9000;
175
175
  export const CLICKHOUSE_PROMETHEUS_PORT = 9363;
176
176
  /** TLS-mode ClickHouse ports.
177
177
  * HTTPS replaces 8123; secure native protocol replaces 9000.
178
- * See aiDocs/patterns/clickhouse-tls-pattern.md § "The contract". */
178
+ * See the ClickHouse TLS pattern, § "The contract". */
179
179
  export const CLICKHOUSE_HTTPS_PORT = 8443;
180
180
  export const CLICKHOUSE_TCP_SECURE_PORT = 9440;
181
181
  /** Mount path inside the ClickHouse container for the materialised TLS cert
@@ -210,8 +210,8 @@ export function clickHouseUserSecretName(userName) {
210
210
  * Canonical source is `@fjall/util/migration § userPasswordEnvName` — re-exported
211
211
  * here so the construct-side import chain stays unchanged while the runner
212
212
  * (`webapp/scripts/migration-runner.mjs`) and any future consumer share a single
213
- * declaration. Coupled values per `.claude/rules/code-quality.md § "Coupled values:
214
- * shared source at 2 occurrences"`. */
213
+ * declaration. Coupled values (the shared-source-at-two-occurrences
214
+ * rule). */
215
215
  export { userPasswordEnvName } from "@fjall/util/migration";
216
216
  /** Env var name carrying the SHA-256 of a user's plaintext password.
217
217
  * The container entrypoint wrapper derives this name from the
@@ -347,8 +347,8 @@ export const BACKUP_RETENTION_DAYS = 14;
347
347
  * wrote can be read back. Dropped on the way in and on the way out; never
348
348
  * queried by the application. Canonical source is `@fjall/util/migration`
349
349
  * because `@fjall/clickhouse` grants `fjall_maintenance` DROP on exactly
350
- * this database — coupled values per `.claude/rules/code-quality.md §
351
- * "Coupled values: shared source at 2 occurrences"`. */
350
+ * this database — coupled values (the shared-source-at-two-occurrences
351
+ * rule). */
352
352
  export { CLICKHOUSE_BACKUP_SCRATCH_DATABASE } from "@fjall/util/migration";
353
353
  /** The two scheduled maintenance jobs that authenticate as
354
354
  * `fjall_maintenance`. Drives the per-job log-group → metric-filter wiring
@@ -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 */
@@ -4,8 +4,6 @@ export const RDS_DEFAULTS = Object.freeze({
4
4
  PREFERRED_MAINTENANCE_WINDOW: "Sat:12:30-Sat:20:30",
5
5
  /** 1 minute — balances monitoring granularity against CloudWatch costs */
6
6
  MONITORING_INTERVAL: Duration.minutes(1),
7
- /** Default backup retention in days — applied uniformly across Aurora and RDS Instance */
8
- BACKUP_RETENTION_DEFAULT_DAYS: 14,
9
7
  /** Default instance type — primary and read-replica must stay in lockstep */
10
8
  DEFAULT_INSTANCE_TYPE: "t4g.large",
11
9
  /** Default storage autoscaling ceiling (GiB) — applied to primary and replica */
@@ -7,6 +7,7 @@ import { SecurityGroup } from "../networking/securityGroup.js";
7
7
  import { Secret } from "../secrets/index.js";
8
8
  import { type StackBuilder } from "../base/awsStack.js";
9
9
  import { type EngineConfig, type ProxyConfig, type ReadReplicaConfig, type CredentialsConfig, type EncryptionConfig, type DatabaseInsightsConfig, type SnapshotTarget } from "../../../utils/databaseTypes.js";
10
+ import { type BackupTier } from "../../../utils/backupTierMapping.js";
10
11
  import type { ITopic } from "aws-cdk-lib/aws-sns";
11
12
  import { type RdsAlarmThresholds } from "../monitoring/index.js";
12
13
  interface RdsProps {
@@ -17,7 +18,17 @@ interface RdsProps {
17
18
  instanceType?: string;
18
19
  maxAllocatedStorage?: number;
19
20
  allocatedStorage?: number;
21
+ /**
22
+ * Automated backup retention. Defaults to the app backup tier's column
23
+ * (`tierDefault(appBackupTier, "rdsBackupRetentionDays")`).
24
+ */
20
25
  backupRetention?: Duration;
26
+ /**
27
+ * The owning app's backup tier (`App.getApp({ backup: { tier } })`), which
28
+ * the retention default follows. `DatabaseFactory` fills it from the App;
29
+ * pass it only for direct `new RdsInstance(...)` construction.
30
+ */
31
+ appBackupTier?: BackupTier;
21
32
  preferredBackupWindow?: string;
22
33
  clusterIdentifier?: string;
23
34
  monitoringInterval?: Duration;