@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
package/README.md CHANGED
@@ -102,7 +102,7 @@ requires you explicitly configure your `AwsStack` with an `account` and `region.
102
102
  ```typescript
103
103
  const env = {
104
104
  region: "us-east-1",
105
- account: "058430617814"
105
+ account: "123456789012"
106
106
  };
107
107
 
108
108
  const vpcStack = new AwsStack(`${id}Network`, { ...env }).addNetwork(
@@ -1,3 +1,4 @@
1
+ import { FJALL_PLATFORM_ACCOUNT_ID } from "./aws/platform.js";
1
2
  /**
2
3
  * Audit configuration for cross-account CloudQuery access
3
4
  */
@@ -6,7 +7,7 @@ export const FJALL_AUDIT_CONFIG = {
6
7
  * Fjall webapp AWS account ID
7
8
  * This account will be granted permission to assume audit roles for CloudQuery
8
9
  */
9
- webappAwsAccountId: "985539798308", // Fjall platform account
10
+ webappAwsAccountId: FJALL_PLATFORM_ACCOUNT_ID,
10
11
  /**
11
12
  * IAM role naming convention
12
13
  */
@@ -69,8 +69,7 @@ export function resolvePinnedAmiId(region, architecture) {
69
69
  if (!isSupportedRegion(region)) {
70
70
  throw new Error(`Buildkite: no pinned AMI for region '${region}'. Supported: ` +
71
71
  `${Object.keys(BUILDKITE_STACK_PINS.amiIdsByRegion).join(", ")}. ` +
72
- `Add the region to BUILDKITE_STACK_PINS via the bump runbook ` +
73
- `(aiDocs/runbooks/buildkite-stack-bump-runbook.md).`);
72
+ `Add the region to BUILDKITE_STACK_PINS.`);
74
73
  }
75
74
  return BUILDKITE_STACK_PINS.amiIdsByRegion[region][architecture];
76
75
  }
@@ -7,6 +7,7 @@ import type App from "../../app.js";
7
7
  import { CloudFrontDistribution, type CachePolicyPreset, type AccessGateConfig } from "../../resources/aws/cdn/index.js";
8
8
  import { type ICdn } from "./interfaces/cdn.js";
9
9
  import { type StaticSiteRouting, type CloudFrontPriceClass } from "@fjall/util";
10
+ import { type BackupTier } from "../../utils/backupTierMapping.js";
10
11
  import { type ManagedDomainBinding, type ManagedDomainExports } from "../../utils/domainTypes.js";
11
12
  import { type Storage } from "./storage.js";
12
13
  import { type AnyCompute } from "./compute.js";
@@ -81,11 +82,24 @@ interface BaseCdnProps {
81
82
  certificate?: ICertificate;
82
83
  certificateArn?: string;
83
84
  comment?: string;
85
+ /**
86
+ * Standard (S3) access logging. Default: the app backup tier's
87
+ * `cloudFrontLogging` column — enterprise logs to the stack's shared,
88
+ * lifecycle-expired access-log bucket under `cloudfront/<id>/`. Set either
89
+ * knob to take over: `enableLogging: false` opts out at any tier,
90
+ * `logBucket` delivers to a bucket of your own.
91
+ */
84
92
  enableLogging?: boolean;
85
93
  logBucket?: IBucket;
86
94
  priceClass?: CloudFrontPriceClass;
87
95
  forwardHostHeader?: boolean;
88
96
  accessGate?: false | AccessGateConfig;
97
+ /**
98
+ * The owning app's backup tier, which the logging default follows.
99
+ * `CdnFactory.build` fills it from the `App`; only set by hand for a
100
+ * directly-instantiated `Cdn`.
101
+ */
102
+ appBackupTier?: BackupTier;
89
103
  }
90
104
  /**
91
105
  * S3 origin CDN props - use with S3 bucket
@@ -3,12 +3,40 @@ import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
3
3
  import { CloudFrontTarget, LoadBalancerTarget } from "aws-cdk-lib/aws-route53-targets";
4
4
  import { CloudFrontDistribution } from "../../resources/aws/cdn/index.js";
5
5
  import { AliasRecord } from "../../resources/aws/networking/index.js";
6
+ import { tierDefault } from "../../utils/backupTierMapping.js";
7
+ import { stackAccessLogBucket } from "../../resources/aws/logging/index.js";
6
8
  import { getSafeZoneName, toPascalCase } from "../../utils/capitaliseString.js";
7
9
  import { recordLabelWithin } from "../../utils/domainTypes.js";
8
10
  import { resolvePatternZone, resolvePatternCloudFrontCertificate } from "./patternDomain.js";
9
11
  import { isStorage } from "./storage.js";
10
12
  import { CdnEcsOriginResolver, cdnOriginRefusal, internalAlbMessage, normaliseDnsName } from "./cdnAppOrigin.js";
11
13
  import { isCompute, isEcsCompute, isLambdaCompute } from "./compute.js";
14
+ /**
15
+ * An explicit `enableLogging` or `logBucket` is the override and passes
16
+ * through untouched; otherwise the tier decides, and a tier-enabled CDN logs
17
+ * to the stack's shared access-log bucket under its own prefix.
18
+ */
19
+ function resolveCdnLogging(scope, id, props) {
20
+ if (props.enableLogging !== undefined || props.logBucket !== undefined) {
21
+ return {
22
+ enableLogging: props.enableLogging,
23
+ logBucket: props.logBucket,
24
+ logFilePrefix: undefined
25
+ };
26
+ }
27
+ if (!tierDefault(props.appBackupTier, "cloudFrontLogging")) {
28
+ return {
29
+ enableLogging: undefined,
30
+ logBucket: undefined,
31
+ logFilePrefix: undefined
32
+ };
33
+ }
34
+ return {
35
+ enableLogging: true,
36
+ logBucket: stackAccessLogBucket(scope),
37
+ logFilePrefix: `cloudfront/${id}/`
38
+ };
39
+ }
12
40
  /**
13
41
  * The distribution's own alias names, normalised — consumed by the
14
42
  * origin-loop guard and by the ECS origin resolver (auto-resolution excludes
@@ -458,8 +486,7 @@ export class Cdn extends CloudFrontDistribution {
458
486
  domainNames,
459
487
  certificate,
460
488
  comment: props.comment,
461
- enableLogging: props.enableLogging,
462
- logBucket: props.logBucket,
489
+ ...resolveCdnLogging(scope, id, props),
463
490
  priceClass: props.priceClass,
464
491
  forwardHostHeader: props.forwardHostHeader ?? impliedForwardHostHeader,
465
492
  accessGate: props.accessGate,
@@ -676,7 +703,8 @@ export class CdnFactory {
676
703
  return new Cdn(scope, id, {
677
704
  ...props,
678
705
  appName: props.appName ?? app.getName(),
679
- app: props.app ?? app
706
+ app: props.app ?? app,
707
+ appBackupTier: props.appBackupTier ?? app.getBackupTier()
680
708
  });
681
709
  };
682
710
  }
@@ -3,6 +3,7 @@ import { type TaskDefinition } from "aws-cdk-lib/aws-ecs";
3
3
  import { type IBucket } from "aws-cdk-lib/aws-s3";
4
4
  import { Construct } from "constructs";
5
5
  import { Secret, type SecretImport } from "../../resources/aws/secrets/secret.js";
6
+ import { type BackupTier } from "../../utils/backupTierMapping.js";
6
7
  import { type ClickHouseAlarmThresholds } from "../../resources/aws/monitoring/index.js";
7
8
  import { type ClickHouseSchemaAdmin, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
8
9
  import { type ISecret } from "aws-cdk-lib/aws-secretsmanager";
@@ -283,6 +284,13 @@ export interface ClickHouseDatabaseProps {
283
284
  * RDS branch of `DatabaseFactory.build`.
284
285
  */
285
286
  applicationId?: string;
287
+ /**
288
+ * The owning app's backup tier, which the tier-owned defaults of the
289
+ * compute this pattern builds (Container Insights, exec-session and
290
+ * log-group encryption) follow. `DatabaseFactory` fills it from the App;
291
+ * pass it only for direct `new ClickHouseDatabase(...)` construction.
292
+ */
293
+ appBackupTier?: BackupTier;
286
294
  }
287
295
  /**
288
296
  * Public-facing re-export of the alarm knob so factory consumers can type
@@ -15,6 +15,8 @@ import App from "../../app.js";
15
15
  import { S3Bucket } from "../../resources/aws/storage/s3.js";
16
16
  import { Secret } from "../../resources/aws/secrets/secret.js";
17
17
  import { LogGroup } from "../../resources/aws/logging/logGroup.js";
18
+ import { StackEncryptionKey } from "../../resources/aws/secrets/stackEncryptionKey.js";
19
+ import { tierDefault } from "../../utils/backupTierMapping.js";
18
20
  import { createClickHouseSecurityGroup } from "../../resources/aws/database/clickhouseSecurityGroup.js";
19
21
  import { buildMaintenancePreamble, maintenanceOkMarker, maintenanceRun } from "../../resources/aws/database/clickhouseMaintenanceScript.js";
20
22
  import { buildClickHouseEntrypointWrapper, buildClickHouseUserData, generateUsersConfigXml } from "../../resources/aws/database/clickhouseUserData.js";
@@ -385,16 +387,24 @@ export class ClickHouseDatabase extends Construct {
385
387
  this.#backupBucket = backupBucket;
386
388
  this.#coldTierBucket = coldTierBucket;
387
389
  this.#migrationsConfig = props.migrations;
390
+ // The maintenance-task log groups sit beside the compute's own container
391
+ // and exec-audit groups and follow the same tier: the stack's shared key
392
+ // at enterprise, nothing otherwise.
393
+ const taskLogGroupEncryption = tierDefault(props.appBackupTier, "customerManagedKeys")
394
+ ? { encryptionKey: StackEncryptionKey.of(this).forLogGroups() }
395
+ : {};
388
396
  const backupTaskLogGroup = backupEnabled
389
397
  ? new LogGroup(this, "ClickHouseBackupTaskLogGroup", {
390
- retention: RetentionDays.TWO_WEEKS
398
+ retention: RetentionDays.TWO_WEEKS,
399
+ ...taskLogGroupEncryption
391
400
  })
392
401
  : undefined;
393
402
  // Explicit rather than `logRetention` so the maintenance-status metric
394
403
  // filters can attach to it alongside the backup task's group.
395
404
  const optimiseTaskLogGroup = optimiseEnabled
396
405
  ? new LogGroup(this, "ClickHouseOptimiseTaskLogGroup", {
397
- retention: RetentionDays.ONE_WEEK
406
+ retention: RetentionDays.ONE_WEEK,
407
+ ...taskLogGroupEncryption
398
408
  })
399
409
  : undefined;
400
410
  // Resolved BEFORE the user data is built: the declared volume size is
@@ -576,6 +586,9 @@ export class ClickHouseDatabase extends Construct {
576
586
  containerInsights: true
577
587
  }),
578
588
  ...(applicationId !== undefined && { applicationId }),
589
+ ...(props.appBackupTier !== undefined && {
590
+ appBackupTier: props.appBackupTier
591
+ }),
579
592
  cluster: {
580
593
  loadBalancer: false,
581
594
  securityGroup,
@@ -164,7 +164,8 @@ export class ComputeFactory {
164
164
  computeProps = {
165
165
  ...computeProps,
166
166
  appName: computeProps.appName ?? app.getName(),
167
- applicationId: computeProps.applicationId ?? app.getName()
167
+ applicationId: computeProps.applicationId ?? app.getName(),
168
+ appBackupTier: computeProps.appBackupTier ?? app.getBackupTier()
168
169
  };
169
170
  }
170
171
  // Create specific compute class based on type discriminator
@@ -241,7 +242,8 @@ export class ComputeFactory {
241
242
  // Add appName to props for Lambda (needed for secrets path derivation)
242
243
  const lambdaComputeProps = {
243
244
  ...computeProps,
244
- appName: app.getName()
245
+ appName: app.getName(),
246
+ appBackupTier: computeProps.appBackupTier ?? app.getBackupTier()
245
247
  };
246
248
  // `ecrRepository` is already defaulted to the app's shared default
247
249
  // container registry above (line ~323) when omitted — the same
@@ -171,6 +171,13 @@ export declare class EcsCompute extends Construct implements IEcsCompute {
171
171
  * the version entry the app containers get.
172
172
  */
173
173
  private resolveSchemaGateMaterialisation;
174
+ /**
175
+ * Encryption for a log group this pattern creates beside the cluster's
176
+ * own (scheduled-task and migration-task logs): the stack's shared key
177
+ * when the app backup tier encrypts, nothing otherwise — the same key the
178
+ * cluster resolved for its service and exec-audit groups.
179
+ */
180
+ private tierLogGroupEncryption;
174
181
  private materialiseScheduledTasks;
175
182
  private buildScheduledTaskDefinition;
176
183
  /**
@@ -955,6 +955,12 @@ export class EcsCompute extends Construct {
955
955
  domainConfig: props.cluster.domainConfig,
956
956
  ...(props.cluster.securityGroup !== undefined && {
957
957
  securityGroup: props.cluster.securityGroup
958
+ }),
959
+ ...(props.cluster.loadBalancerDeletionProtection !== undefined && {
960
+ loadBalancerDeletionProtection: props.cluster.loadBalancerDeletionProtection
961
+ }),
962
+ ...(props.cluster.loadBalancerAccessLogs !== undefined && {
963
+ loadBalancerAccessLogs: props.cluster.loadBalancerAccessLogs
958
964
  })
959
965
  }
960
966
  : undefined;
@@ -970,7 +976,12 @@ export class EcsCompute extends Construct {
970
976
  applicationId: props.applicationId ??
971
977
  (App.hasInstance() ? App.getInstance().getName() : undefined),
972
978
  ...(props.taskStopWatchdog === false && { taskStopWatchdog: false }),
973
- ...(props.containerInsights === true && { containerInsights: true })
979
+ ...(props.containerInsights !== undefined && {
980
+ containerInsights: props.containerInsights
981
+ }),
982
+ ...(props.appBackupTier !== undefined && {
983
+ appBackupTier: props.appBackupTier
984
+ })
974
985
  };
975
986
  this.ecsCluster = new EcsCluster(this, `${id}Ecs`, ecsProps);
976
987
  this.connections = this.ecsCluster.connections;
@@ -1155,6 +1166,16 @@ export class EcsCompute extends Construct {
1155
1166
  defaultImageTag: CONSTRUCTS_VERSION
1156
1167
  };
1157
1168
  }
1169
+ /**
1170
+ * Encryption for a log group this pattern creates beside the cluster's
1171
+ * own (scheduled-task and migration-task logs): the stack's shared key
1172
+ * when the app backup tier encrypts, nothing otherwise — the same key the
1173
+ * cluster resolved for its service and exec-audit groups.
1174
+ */
1175
+ tierLogGroupEncryption() {
1176
+ const key = this.ecsCluster.encryptionKey;
1177
+ return key === undefined ? {} : { encryptionKey: key.forLogGroups() };
1178
+ }
1158
1179
  materialiseScheduledTasks(id, props) {
1159
1180
  const scheduledTasks = props.cluster?.scheduledTasks;
1160
1181
  if (!scheduledTasks || scheduledTasks.length === 0)
@@ -1200,7 +1221,8 @@ export class EcsCompute extends Construct {
1200
1221
  // removal default (D17).
1201
1222
  const logGroup = entry.logGroup ??
1202
1223
  new LogGroup(this, `${id}${toPascalCase(entry.name)}LogGroup`, {
1203
- retention: entry.logRetention ?? DEFAULT_LOG_RETENTION
1224
+ retention: entry.logRetention ?? DEFAULT_LOG_RETENTION,
1225
+ ...this.tierLogGroupEncryption()
1204
1226
  });
1205
1227
  const logging = new AwsLogDriver({
1206
1228
  streamPrefix: `/ecs-scheduled/${id}/${entry.name}`,
@@ -1429,7 +1451,8 @@ export class EcsCompute extends Construct {
1429
1451
  const inheritedImage = service.taskDefinition.defaultContainer?.imageName;
1430
1452
  const image = resolveMigrationImage(svcConfig, migrations, inheritedImage);
1431
1453
  const logGroup = new LogGroup(this, `${idPrefix}LogGroup`, {
1432
- retention: RetentionDays.ONE_MONTH
1454
+ retention: RetentionDays.ONE_MONTH,
1455
+ ...this.tierLogGroupEncryption()
1433
1456
  });
1434
1457
  const executionRole = new Role(this, `${idPrefix}ExecutionRole`, {
1435
1458
  assumedBy: new ServicePrincipal("ecs-tasks.amazonaws.com")
@@ -5,6 +5,7 @@ import { type ILogGroup, type RetentionDays } from "aws-cdk-lib/aws-logs";
5
5
  import { type PolicyDocument, type IManagedPolicy, type PolicyStatement } from "aws-cdk-lib/aws-iam";
6
6
  import type { ITopic } from "aws-cdk-lib/aws-sns";
7
7
  import { type ConnectionSpec } from "./interfaces/connector.js";
8
+ import type { BackupTier } from "../../utils/backupTierMapping.js";
8
9
  import { type RemoteConnectionSpec } from "../../resources/aws/compute/ecsRemoteConnections.js";
9
10
  import { type EcsRoutingConfig, type EcsContainerDependency } from "../../resources/aws/compute/ecsTypes.js";
10
11
  import { ScalingType, type DomainConfig, type EcsCapacityProvider, type Ec2CapacityConfig, type QueueScalingConfig } from "../../resources/aws/compute/ecs.js";
@@ -623,6 +624,19 @@ export interface EcsClusterConfig {
623
624
  * (e.g. ClickHouseDatabase) that own their security group lifecycle.
624
625
  */
625
626
  securityGroup?: ISecurityGroup;
627
+ /**
628
+ * Deletion protection on the shared load balancer. Default: the app backup
629
+ * tier's `deletionProtection` column, applied only when the stack's
630
+ * environment resolves to production. The destroy pre-flight switches it
631
+ * off before a consented destroy.
632
+ */
633
+ loadBalancerDeletionProtection?: boolean;
634
+ /**
635
+ * Access logs from the shared load balancer into the stack's
636
+ * lifecycle-expired access-log bucket. Default: the app backup tier's
637
+ * `albAccessLogs` column.
638
+ */
639
+ loadBalancerAccessLogs?: boolean;
626
640
  /**
627
641
  * Scheduled tasks materialised into `Ec2TaskDefinition`s and registered via
628
642
  * `app.addSchedule(...)` against the existing `EcsScheduleTarget` shape
@@ -1074,9 +1088,18 @@ export interface EcsComputeProps {
1074
1088
  * `RunningTaskCount` liveness alarms (`ecsAlarms.ts` gates them on this
1075
1089
  * flag because the metric only exists in `ECS/ContainerInsights`).
1076
1090
  * Scale-to-zero services should pair it with
1077
- * `alarms: { runningTasksMinimum: 0 }` or they idle in ALARM.
1091
+ * `alarms: { runningTasksMinimum: 0 }` or they idle in ALARM. Default:
1092
+ * the app backup tier's `containerInsights` column (on at enterprise);
1093
+ * `false` is an explicit, auditable opt-out.
1078
1094
  */
1079
- containerInsights?: true;
1095
+ containerInsights?: boolean;
1080
1096
  /** Application ID for alarm tagging (used by webhook to map alarms to applications). */
1081
1097
  applicationId?: string;
1098
+ /**
1099
+ * The owning app's backup tier. Auto-derived from the App by
1100
+ * ComputeFactory; the tier-owned defaults (Container Insights, exec-session
1101
+ * and log-group encryption, load-balancer deletion protection and access
1102
+ * logs) follow it. Only specify for direct construction.
1103
+ */
1104
+ appBackupTier?: BackupTier;
1082
1105
  }
@@ -4,6 +4,7 @@ import { Connections, type IVpc, type ISecurityGroup } from "aws-cdk-lib/aws-ec2
4
4
  import { Repository } from "aws-cdk-lib/aws-ecr";
5
5
  import { type RepositoryImage } from "aws-cdk-lib/aws-ecs";
6
6
  import { Construct } from "constructs";
7
+ import type { BackupTier } from "../../utils/backupTierMapping.js";
7
8
  import { type ILambdaCompute } from "./interfaces/compute.js";
8
9
  import { type ConnectionSpec } from "./interfaces/connector.js";
9
10
  import { LambdaFunction } from "../../resources/aws/compute/lambda.js";
@@ -95,6 +96,17 @@ interface BaseLambdaProps {
95
96
  * Only specify for advanced use cases.
96
97
  */
97
98
  appName?: string;
99
+ /**
100
+ * The owning app's backup tier. Auto-derived from the App by
101
+ * ComputeFactory; the tier-owned defaults (X-Ray tracing, environment and
102
+ * log-group encryption) follow it. Only specify for direct construction.
103
+ */
104
+ appBackupTier?: BackupTier;
105
+ /**
106
+ * Active X-Ray tracing. Default: the app backup tier's `lambdaTracing`
107
+ * column (on at enterprise).
108
+ */
109
+ tracing?: boolean;
98
110
  /**
99
111
  * Resources this Lambda needs to connect to (e.g., databases, S3 buckets, SQS queues).
100
112
  * Creates security group rules for IConnectable resources and IAM grants for IAM resources.
@@ -167,6 +167,8 @@ export class LambdaCompute extends Construct {
167
167
  secretsImport: props.secretsImport,
168
168
  environment: props.environment,
169
169
  appName: props.appName,
170
+ appBackupTier: props.appBackupTier,
171
+ tracing: props.tracing,
170
172
  functionName: id
171
173
  };
172
174
  this.lambdaFunction = new LambdaFunction(scope, `${id}Lambda`, lambdaProps);
@@ -10,6 +10,8 @@ const COMPUTE_PROP_APPLICABILITY = {
10
10
  type: "all",
11
11
  vpc: "all",
12
12
  appName: ["ecs", "lambda"],
13
+ appBackupTier: ["ecs", "lambda"],
14
+ tracing: ["lambda"],
13
15
  docker: ["ecs", "lambda"],
14
16
  ecrRepository: ["ecs", "lambda"],
15
17
  cluster: ["ecs"],
@@ -5,6 +5,7 @@ import type { RdsAlarmThresholds } from "../../resources/aws/monitoring/index.js
5
5
  import type App from "../../app.js";
6
6
  import { type DynamoDBKeySchema, type DynamoDBGlobalSecondaryIndex } from "../../resources/aws/database/dynamodb.js";
7
7
  import { ClickHouseDatabase, type ClickHouseDatabaseProps, type ClickHouseAlarmThresholds } from "./clickhouseDatabase.js";
8
+ import { type BackupTier } from "../../utils/backupTierMapping.js";
8
9
  import { type Connections, type IConnectable, type IVpc } from "aws-cdk-lib/aws-ec2";
9
10
  import { type ITable } from "aws-cdk-lib/aws-dynamodb";
10
11
  import { type Secret, type SecretImport } from "../../resources/aws/secrets/index.js";
@@ -40,6 +41,13 @@ type BaseDatabaseProps = {
40
41
  databaseName: string;
41
42
  instanceType?: string;
42
43
  deletionProtection?: boolean;
44
+ /**
45
+ * The owning app's backup tier (`App.getApp({ backup: { tier } })`), which
46
+ * the tier-owned defaults (backup retention) follow. `DatabaseFactory`
47
+ * fills it from the App as it does `vpc` and `applicationId`; pass it only
48
+ * for direct `new RelationalDatabase(...)` construction.
49
+ */
50
+ appBackupTier?: BackupTier;
43
51
  /** SNS topic for alarm notifications. Accepts ITopic, ARN string, or "import:ExportName". */
44
52
  alertsTopic?: ITopic | string;
45
53
  /** Alarm thresholds. false to disable, undefined for defaults, object to override. */
@@ -194,6 +202,12 @@ export interface DynamoDBDatabaseProps {
194
202
  encryption?: "AWS_OWNED" | "AWS_MANAGED" | "CUSTOMER_MANAGED";
195
203
  deletionProtection?: boolean;
196
204
  removalPolicy?: "DESTROY" | "RETAIN" | "SNAPSHOT";
205
+ /**
206
+ * The owning app's backup tier, which the tier-owned defaults (encryption,
207
+ * deletion protection) follow. `DatabaseFactory` fills it from the App;
208
+ * pass it only for direct `new DynamoDBDatabase(...)` construction.
209
+ */
210
+ appBackupTier?: BackupTier;
197
211
  }
198
212
  /**
199
213
  * Relational database props union (Aurora, Instance, GlobalAurora).
@@ -130,7 +130,7 @@ function validateRelationalDatabaseProps(props) {
130
130
  // Instance-only options on Aurora/GlobalAurora
131
131
  warnIfPropertiesIgnored(props, ["readReplica", "multiAz", "allocatedStorage", "iamAuthentication"], "Instance", "Instance database");
132
132
  // Aurora-only options on Instance
133
- warnIfPropertiesIgnored(props, ["readers", "writer", "backupRetention"], ["Aurora", "GlobalAurora"], "Aurora database");
133
+ warnIfPropertiesIgnored(props, ["readers", "writer"], ["Aurora", "GlobalAurora"], "Aurora database");
134
134
  // GlobalAurora-only options on non-global databases
135
135
  warnIfPropertiesIgnored(props, ["primaryRegion", "secondaryRegions", "enableGlobalWriteForwarding"], "GlobalAurora", "GlobalAurora database");
136
136
  }
@@ -170,21 +170,27 @@ export class DatabaseFactory {
170
170
  static build(id, props) {
171
171
  return (app, scope) => {
172
172
  if (props.type === "DynamoDB") {
173
- validateDatabaseProps(props);
174
- return new DynamoDBDatabase(scope, id, props);
173
+ const dynamoProps = {
174
+ ...props,
175
+ appBackupTier: props.appBackupTier ?? app.getBackupTier()
176
+ };
177
+ validateDatabaseProps(dynamoProps);
178
+ return new DynamoDBDatabase(scope, id, dynamoProps);
175
179
  }
176
180
  if (props.type === "ClickHouse") {
177
181
  const clickhouseProps = {
178
182
  ...props,
179
183
  vpc: props.vpc ?? app.getVpc(),
180
- applicationId: props.applicationId ?? app.getName()
184
+ applicationId: props.applicationId ?? app.getName(),
185
+ appBackupTier: props.appBackupTier ?? app.getBackupTier()
181
186
  };
182
187
  return new ClickHouseDatabase(scope, id, clickhouseProps);
183
188
  }
184
189
  const databaseProps = {
185
190
  ...props,
186
191
  vpc: props.vpc ?? app.getVpc(),
187
- applicationId: props.applicationId ?? app.getName()
192
+ applicationId: props.applicationId ?? app.getName(),
193
+ appBackupTier: props.appBackupTier ?? app.getBackupTier()
188
194
  };
189
195
  validateDatabaseProps(databaseProps);
190
196
  return new RelationalDatabase(scope, id, databaseProps);
@@ -223,7 +229,8 @@ export class DynamoDBDatabase extends Construct {
223
229
  pointInTimeRecovery: props.pointInTimeRecovery,
224
230
  encryption: props.encryption,
225
231
  deletionProtection: props.deletionProtection,
226
- removalPolicy: props.removalPolicy
232
+ removalPolicy: props.removalPolicy,
233
+ appBackupTier: props.appBackupTier
227
234
  };
228
235
  this.table = new DynamoDBTable(this, id, tableProps);
229
236
  }
@@ -390,6 +397,7 @@ export class RelationalDatabase extends Construct {
390
397
  credentials: props.credentials,
391
398
  encryption: props.encryption,
392
399
  backupRetention: props.backupRetention,
400
+ appBackupTier: props.appBackupTier,
393
401
  monitoringInterval: props.monitoringInterval,
394
402
  preferredMaintenanceWindow: props.preferredMaintenanceWindow,
395
403
  port: props.port,
@@ -429,6 +437,7 @@ export class RelationalDatabase extends Construct {
429
437
  credentials: props.credentials,
430
438
  encryption: props.encryption,
431
439
  backupRetention: props.backupRetention,
440
+ appBackupTier: props.appBackupTier,
432
441
  monitoringInterval: props.monitoringInterval,
433
442
  preferredMaintenanceWindow: props.preferredMaintenanceWindow,
434
443
  port: props.port,
@@ -459,6 +468,7 @@ export class RelationalDatabase extends Construct {
459
468
  backupRetention: props.backupRetention !== undefined
460
469
  ? Duration.days(props.backupRetention)
461
470
  : undefined,
471
+ appBackupTier: props.appBackupTier,
462
472
  databaseInsights: props.databaseInsights,
463
473
  proxy: props.proxy,
464
474
  readReplica: props.readReplica,
@@ -7,7 +7,6 @@ interface ProxyAgentProps extends StackProps {
7
7
  accountId?: string;
8
8
  vpc?: IVpc;
9
9
  publicKey: string;
10
- enableSSH?: boolean;
11
10
  instanceType: string;
12
11
  databaseConnection?: Connections;
13
12
  tags?: KeyValue;
@@ -11,7 +11,12 @@ export class FivetranProxy extends Stack {
11
11
  serviceName: "fivetranProxy",
12
12
  vpc: props.vpc,
13
13
  instanceType: props.instanceType || "t2.micro",
14
- enableSSH: props.enableSSH || true,
14
+ // Not a caller's choice. This proxy exists so Fivetran can reach a
15
+ // private database over SSH — it provisions the `fivetran` user and its
16
+ // authorized_keys above — so the instance must be publicly reachable on
17
+ // port 22. The prop that used to sit here was `props.enableSSH || true`,
18
+ // which accepted a `false` it could never honour.
19
+ enableSSH: true,
15
20
  env: {
16
21
  account: props.env?.account,
17
22
  region: props.env?.region
@@ -162,7 +162,7 @@ export interface PatternLambdaConfig {
162
162
  memorySize?: number;
163
163
  /** Timeout in seconds. Range: 1-900. */
164
164
  timeout?: number;
165
- /** Ephemeral storage size in MB. Range: 512-10240. Default: 512. */
165
+ /** Ephemeral storage size in MiB. Range: 512-10240. Default: 512. */
166
166
  ephemeralStorageSize?: number;
167
167
  }
168
168
  /**
@@ -468,16 +468,16 @@ export interface StaticSiteCdnConfig {
468
468
  * optional contact-form endpoint (Lambda Function URL → SES, CloudFront-fronted).
469
469
  *
470
470
  * @example
471
- * app.addPattern(PatternFactory.build("LbexcSite", {
471
+ * app.addPattern(PatternFactory.build("StorefrontSite", {
472
472
  * type: "staticsite",
473
- * name: "lbexc",
474
- * source: "../../lbexc-static-export",
473
+ * name: "storefront",
474
+ * source: "../../storefront-static-export",
475
475
  * build: { command: "npm run build", outputDir: "out" },
476
476
  * routing: "multipage",
477
477
  * security: { headers: true },
478
- * domain: "lbexc.com",
479
- * zoneName: "lbexc.com", // or a CLI-injected `managedDomain` binding
480
- * forms: { to: "hello@lbexc.com", corsOrigin: "https://lbexc.com" }
478
+ * domain: "storefront.example",
479
+ * zoneName: "storefront.example", // or a CLI-injected `managedDomain` binding
480
+ * forms: { to: "hello@storefront.example", corsOrigin: "https://storefront.example" }
481
481
  * }));
482
482
  */
483
483
  export interface IStaticSiteProps {
@@ -315,7 +315,10 @@ export class MessagingFactory {
315
315
  applicationId: props.applicationId ?? app.getName()
316
316
  });
317
317
  case "topic":
318
- return new TopicMessaging(scope, id, props);
318
+ return new TopicMessaging(scope, id, {
319
+ ...props,
320
+ appBackupTier: props.appBackupTier ?? app.getBackupTier()
321
+ });
319
322
  case "eventBus":
320
323
  return new EventBusMessaging(scope, id, props);
321
324
  default: {
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * - Nothing here guesses a hosted zone. The old `extractRootDomain` label
9
9
  * split resolved every multi-label public suffix wrongly (e.g.
10
- * 'littlebearearthworks.com.au' → zone 'com.au') and mis-placed delegated
10
+ * 'example.com.au' → zone 'com.au') and mis-placed delegated
11
11
  * sub-zone records; it is deleted. Zone identity now arrives explicitly, in
12
12
  * priority order: a CLI-injected {@link ManagedDomainBinding} (literal
13
13
  * values — crosses accounts and regions), the legacy
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * - Nothing here guesses a hosted zone. The old `extractRootDomain` label
9
9
  * split resolved every multi-label public suffix wrongly (e.g.
10
- * 'littlebearearthworks.com.au' → zone 'com.au') and mis-placed delegated
10
+ * 'example.com.au' → zone 'com.au') and mis-placed delegated
11
11
  * sub-zone records; it is deleted. Zone identity now arrives explicitly, in
12
12
  * priority order: a CLI-injected {@link ManagedDomainBinding} (literal
13
13
  * values — crosses accounts and regions), the legacy
@@ -41,8 +41,6 @@ const PAYLOAD_DEFAULTS = {
41
41
  SOURCE: "../..",
42
42
  DATABASE: {
43
43
  TYPE: "Instance",
44
- /** AWS default; 7 days covers most accidental data loss recovery scenarios */
45
- BACKUP_RETENTION: 7,
46
44
  DELETION_PROTECTION: true
47
45
  },
48
46
  COMPUTE: {
@@ -172,7 +170,7 @@ export class Payload extends Construct {
172
170
  return {
173
171
  databaseName: dbConfig.databaseName ?? toValidDatabaseName(this.props.name),
174
172
  databaseEngine: dbConfig.databaseEngine ?? "postgresql",
175
- backupRetention: dbConfig.backupRetention ?? PAYLOAD_DEFAULTS.DATABASE.BACKUP_RETENTION,
173
+ backupRetention: dbConfig.backupRetention,
176
174
  deletionProtection: dbConfig.deletionProtection ??
177
175
  PAYLOAD_DEFAULTS.DATABASE.DELETION_PROTECTION,
178
176
  publiclyAccessible: dbConfig.publiclyAccessible,
@@ -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";