@fjall/components-infrastructure 7.2.0 → 7.3.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.
@@ -6,6 +6,7 @@ import { DynamoDBTable } from "../../resources/aws/database/dynamodb.js";
6
6
  import { ClickHouseDatabase } from "./clickhouseDatabase.js";
7
7
  import { FjallLogger, warnIfPropertiesIgnored } from "../../utils/validationLogger.js";
8
8
  import { toPascalCase } from "../../utils/capitaliseString.js";
9
+ import { ResourceNaming } from "../../utils/resourceNaming.js";
9
10
  import { resolveAlertsTopic as resolveAlertsTopicShared } from "../../utils/resolveAlertsTopic.js";
10
11
  import { Port } from "aws-cdk-lib/aws-ec2";
11
12
  import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
@@ -364,7 +365,7 @@ export class RelationalDatabase extends Construct {
364
365
  FjallLogger.warn(`Aurora database '${props.databaseName}' is configured with public accessibility. ` +
365
366
  "This is a security risk and should only be used for development.");
366
367
  }
367
- this.database = new RdsAurora(this, `${props.databaseName}Rds`, {
368
+ this.database = new RdsAurora(this, ResourceNaming.rdsChildConstructId(props.databaseName), {
368
369
  vpc: props.vpc,
369
370
  databaseName: props.databaseName,
370
371
  engine: resolvedEngine,
@@ -406,7 +407,7 @@ export class RelationalDatabase extends Construct {
406
407
  const resolvedDatabaseEngine = props.databaseEngine ?? "postgresql";
407
408
  const resolvedEngine = props.engine ?? getAuroraClusterEngine(resolvedDatabaseEngine);
408
409
  const engineConfig = getEngineConfig(resolvedDatabaseEngine);
409
- this.database = new RdsAuroraGlobal(this, `${props.databaseName}RdsGlobal`, {
410
+ this.database = new RdsAuroraGlobal(this, ResourceNaming.globalRdsChildConstructId(props.databaseName), {
410
411
  vpc: props.vpc,
411
412
  databaseName: props.databaseName,
412
413
  primaryRegion: props.primaryRegion,
@@ -439,7 +440,7 @@ export class RelationalDatabase extends Construct {
439
440
  const resolvedDatabaseEngine = props.databaseEngine ?? "postgresql";
440
441
  const resolvedEngine = props.engine ?? getInstanceEngine(resolvedDatabaseEngine);
441
442
  const engineConfig = getEngineConfig(resolvedDatabaseEngine);
442
- this.database = new RdsInstance(this, `${props.databaseName}Rds`, {
443
+ this.database = new RdsInstance(this, ResourceNaming.rdsChildConstructId(props.databaseName), {
443
444
  vpc: props.vpc,
444
445
  databaseName: props.databaseName,
445
446
  instanceType: props.instanceType,
@@ -10,6 +10,7 @@ import { AliasRecord } from "../networking/dnsRecord/aliasRecord.js";
10
10
  import { SecurityGroup } from "../networking/securityGroup.js";
11
11
  import { DNS_APEX, isManagedDomainBinding, isWithinZone } from "../../../utils/domainTypes.js";
12
12
  import { readInjectedManagedDomainBinding } from "../../../utils/managedDomainContext.js";
13
+ import { ResourceNaming } from "../../../utils/resourceNaming.js";
13
14
  import { getSafeZoneName, toPascalCase } from "../../../utils/capitaliseString.js";
14
15
  import { FjallLogger } from "../../../utils/validationLogger.js";
15
16
  import { isServiceEc2 } from "./ecsTaskDefinition.js";
@@ -18,12 +19,7 @@ import { addRoutingListener } from "./listenerRouting.js";
18
19
  import { buildRoutingConditions, deterministicHostPriority, getNextPriority } from "./hostHeaderListenerRule.js";
19
20
  export function addLoadBalancer(ctx, anyServiceUsesEc2, asgSecurityGroup) {
20
21
  const props = ctx.props;
21
- const defaultLoadBalancerName = `${props.clusterName}LoadBalancer`;
22
- const supportedNameLength = 32;
23
- let truncatedLoadBalancerName = defaultLoadBalancerName.length > supportedNameLength
24
- ? defaultLoadBalancerName.substring(0, supportedNameLength)
25
- : defaultLoadBalancerName;
26
- truncatedLoadBalancerName = truncatedLoadBalancerName.replace(/-+$/, "");
22
+ const truncatedLoadBalancerName = ResourceNaming.loadBalancerName(props.clusterName);
27
23
  const isInternal = props.cluster?.loadBalancer === "internal";
28
24
  let loadBalancerSecurityGroup;
29
25
  if (anyServiceUsesEc2) {
@@ -83,7 +83,7 @@ export class RdsAurora extends Construct {
83
83
  : (props.credentials?.username ?? this.engineConfig.defaultUsername);
84
84
  // Global secondary clusters import replicated secret instead of creating new
85
85
  if (props.isGlobalSecondary) {
86
- this.databaseCredentials = new Secret(this, `${this.databaseNameValue}Credentials`, {
86
+ this.databaseCredentials = new Secret(this, ResourceNaming.credentialsSecretConstructId(this.databaseNameValue), {
87
87
  secretName: props.credentialsSecretName ??
88
88
  ResourceNaming.credentialsSecretName(id),
89
89
  importExisting: true
@@ -97,7 +97,7 @@ export class RdsAurora extends Construct {
97
97
  snapshotUsername: props.snapshotUsername,
98
98
  assumedUsername: username
99
99
  });
100
- this.databaseCredentials = new Secret(this, `${this.databaseNameValue}Credentials`, {
100
+ this.databaseCredentials = new Secret(this, ResourceNaming.credentialsSecretConstructId(this.databaseNameValue), {
101
101
  secretName: props.credentialsSecretName ??
102
102
  ResourceNaming.credentialsSecretName(id),
103
103
  aliasName: props.credentialsKmsAliasName,
@@ -95,7 +95,7 @@ export class RdsAuroraGlobal extends Construct {
95
95
  };
96
96
  if (this.props.primaryRegion === this.currentRegion) {
97
97
  // primary
98
- this.primaryCluster = new RdsAurora(this, `${this.props.databaseName}Primary`, {
98
+ this.primaryCluster = new RdsAurora(this, ResourceNaming.globalPrimaryConstructId(this.props.databaseName), {
99
99
  vpc: this.props.vpc,
100
100
  databaseName: this.props.databaseName,
101
101
  clusterIdentifier: this.getPrimaryClusterIdentifier(),
@@ -32,7 +32,7 @@ const MULTI_USER_ROTATION_APPLICATIONS = {
32
32
  */
33
33
  export function resolveStorageEncryptionKey(scope, databaseName, storageKey) {
34
34
  if (isCMKRequested(storageKey)) {
35
- return new CustomerManagedKey(scope, `${databaseName}ClusterEncryptionKey`, { aliasName: `cmk/rds/${databaseName}/encryptionKey` }).key;
35
+ return new CustomerManagedKey(scope, `${databaseName}ClusterEncryptionKey`, { aliasName: ResourceNaming.storageEncryptionKeyAlias(databaseName) }).key;
36
36
  }
37
37
  if (isAwsManagedKey(storageKey) || storageKey === undefined) {
38
38
  return undefined;
@@ -45,7 +45,7 @@ export function resolveStorageEncryptionKey(scope, databaseName, storageKey) {
45
45
  */
46
46
  export function resolvePerformanceInsightsKey(scope, databaseName, piEnabled, encryptionKey) {
47
47
  if (piEnabled && isCMKRequested(encryptionKey)) {
48
- return new CustomerManagedKey(scope, `${databaseName}PerformanceInsightsKey`, { aliasName: `cmk/rds/${databaseName}/InsightsKey` }).key;
48
+ return new CustomerManagedKey(scope, `${databaseName}PerformanceInsightsKey`, { aliasName: ResourceNaming.insightsKeyAlias(databaseName) }).key;
49
49
  }
50
50
  return undefined;
51
51
  }
@@ -80,7 +80,7 @@ export class RdsInstance extends Construct {
80
80
  snapshotUsername: props.snapshotUsername,
81
81
  assumedUsername: username
82
82
  });
83
- this.databaseCredentials = new Secret(this, `${this.databaseNameValue}Credentials`, {
83
+ this.databaseCredentials = new Secret(this, ResourceNaming.credentialsSecretConstructId(this.databaseNameValue), {
84
84
  secretName: ResourceNaming.credentialsSecretName(this.constructId),
85
85
  generateSecretString: {
86
86
  secretStringTemplate: JSON.stringify({
@@ -237,7 +237,7 @@ export class RdsInstance extends Construct {
237
237
  const { piEnabled, piConfig } = resolveDatabaseInsights(props.databaseInsights);
238
238
  const readReplicaPerformanceInsightsKey = piEnabled && isCMKRequested(piConfig?.encryptionKey)
239
239
  ? new CustomerManagedKey(this, `${this.databaseNameValue}ReadReplicaReaderInsightsKey`, {
240
- aliasName: `cmk/rds/${this.databaseNameValue}/ReadReplicaInsightsKey`
240
+ aliasName: ResourceNaming.readReplicaInsightsKeyAlias(this.databaseNameValue)
241
241
  }).key
242
242
  : undefined;
243
243
  this.readReplicaSecurityGroup = new SecurityGroup(this, `${this.databaseNameValue}ReadReplicaSecurityGroup`, {
@@ -3,6 +3,7 @@ import * as ec2 from "aws-cdk-lib/aws-ec2";
3
3
  import * as s3 from "aws-cdk-lib/aws-s3";
4
4
  import { LogGroup } from "../logging/logGroup.js";
5
5
  import { S3Bucket } from "../storage/index.js";
6
+ import { ResourceNaming } from "../../../utils/resourceNaming.js";
6
7
  export const FLOW_LOG_TRAFFIC_TYPES = ["ALL", "ACCEPT", "REJECT"];
7
8
  // Read at two sites (resources-layer constructor default + patterns-layer natGateways guard) that must agree.
8
9
  export const DEFAULT_MAX_AZS = 3;
@@ -171,7 +172,7 @@ export class Vpc extends ec2.Vpc {
171
172
  const trafficType = Vpc.resolveTrafficType(config.trafficType);
172
173
  if (config.destination === "s3") {
173
174
  const bucket = new S3Bucket(scope, `${id}FlowLogBucket`, {
174
- bucketName: `vpc-flowlogs-${id.toLowerCase()}-${Stack.of(scope).account}`,
175
+ bucketName: ResourceNaming.vpcFlowLogBucketName(id, Stack.of(scope).account),
175
176
  encryption: s3.BucketEncryption.S3_MANAGED,
176
177
  lifecycleRules: config.retentionDays
177
178
  ? [{ expiration: Duration.days(config.retentionDays), enabled: true }]
@@ -2,6 +2,7 @@ import { CfnOutput, Duration, RemovalPolicy } from "aws-cdk-lib";
2
2
  import { Alias, Key } from "aws-cdk-lib/aws-kms";
3
3
  import { Construct } from "constructs";
4
4
  import { toPascalCase } from "../../../utils/capitaliseString.js";
5
+ import { ResourceNaming } from "../../../utils/resourceNaming.js";
5
6
  export class CustomerManagedKey extends Construct {
6
7
  key;
7
8
  alias;
@@ -22,7 +23,7 @@ export class CustomerManagedKey extends Construct {
22
23
  exportName: `${outputName}KeyArn`
23
24
  });
24
25
  this.alias = new Alias(this, `${id}KeyAlias`, {
25
- aliasName: props.aliasName || `cmk/${id}`,
26
+ aliasName: props.aliasName || ResourceNaming.defaultKeyAlias(id),
26
27
  targetKey: this.key
27
28
  });
28
29
  new CfnOutput(this, `${outputName}KeyAliasArn`, {
@@ -2,6 +2,7 @@ import { SecretValue } from "aws-cdk-lib";
2
2
  import { Secret as CdkSecret } from "aws-cdk-lib/aws-secretsmanager";
3
3
  import { Construct } from "constructs";
4
4
  import { CustomerManagedKey } from "./kms.js";
5
+ import { ResourceNaming } from "../../../utils/resourceNaming.js";
5
6
  /**
6
7
  * Context key carrying the deploy-time `secretName -> completeArn` map. Written by
7
8
  * @fjall/deploy-core (`CdkArgumentBuilder.buildContextArgs` emits
@@ -71,7 +72,7 @@ export class Secret extends Construct {
71
72
  }
72
73
  // Create KMS key for new secrets only
73
74
  const customerManagedKey = new CustomerManagedKey(this, `${id}CustomerManagedKey`, {
74
- aliasName: props.aliasName ?? `cmk/${id}`
75
+ aliasName: props.aliasName ?? ResourceNaming.defaultKeyAlias(id)
75
76
  });
76
77
  this.secretsCustomerManagedKey = customerManagedKey;
77
78
  /**
@@ -41,4 +41,54 @@ export declare class ResourceNaming {
41
41
  static proxySecurityGroupDescription(name: string): string;
42
42
  /** Security group description for read replica. */
43
43
  static readReplicaSecurityGroupDescription(name: string): string;
44
+ /**
45
+ * KMS alias for a database's storage encryption key. `name` is the logical
46
+ * database name, and the casing is load-bearing — KMS aliases are
47
+ * case-sensitive and these are live on deployed keys.
48
+ *
49
+ * The three alias helpers return the value handed to `CustomerManagedKey`,
50
+ * which is what CDK prefixes with `alias/`; `kms list-aliases` reports the
51
+ * prefixed form.
52
+ */
53
+ static storageEncryptionKeyAlias(name: string): string;
54
+ /** KMS alias for a database's Database Insights encryption key. */
55
+ static insightsKeyAlias(name: string): string;
56
+ /** KMS alias for a read replica's Database Insights encryption key. */
57
+ static readReplicaInsightsKeyAlias(name: string): string;
58
+ /**
59
+ * Construct id of the RDS child the Database pattern creates for an
60
+ * `Instance` or `Aurora` plan. The instance, cluster, proxy and secret
61
+ * helpers are all fed this id, NOT the Database construct's own id, so
62
+ * anything predicting a deployed name from a resource plan starts here.
63
+ */
64
+ static rdsChildConstructId(databaseName: string): string;
65
+ /** Construct id of the RdsAuroraGlobal child, which names the cluster. */
66
+ static globalRdsChildConstructId(databaseName: string): string;
67
+ /**
68
+ * Construct id of the RdsAurora primary inside RdsAuroraGlobal, which names
69
+ * its instances, proxy and credentials secret.
70
+ */
71
+ static globalPrimaryConstructId(databaseName: string): string;
72
+ /** Construct id of a database's credentials Secret. */
73
+ static credentialsSecretConstructId(databaseName: string): string;
74
+ /**
75
+ * Alias a Secret or CustomerManagedKey takes when the caller supplies none.
76
+ * CDK prefixes it with `alias/`, which is the form `kms list-aliases`
77
+ * reports.
78
+ */
79
+ static defaultKeyAlias(constructId: string): string;
80
+ /**
81
+ * S3 bucket holding a VPC's flow logs. `id` is the Network construct id —
82
+ * the network name for an additional VPC, `${stackPrefix}Vpc` for the
83
+ * primary. Lowercased rather than kebabed: S3 forbids uppercase and this
84
+ * name is already live on deployed buckets, so the transform cannot change.
85
+ */
86
+ static vpcFlowLogBucketName(id: string, accountId: string): string;
87
+ /**
88
+ * ALB physical name for an ECS cluster. ELBv2 caps names at 32 characters
89
+ * and rejects a trailing hyphen, so a long cluster name is truncated and
90
+ * then stripped — which means the name is NOT simply
91
+ * `${clusterName}LoadBalancer` and must not be re-derived as if it were.
92
+ */
93
+ static loadBalancerName(clusterName: string): string;
44
94
  }
@@ -1,4 +1,6 @@
1
1
  import { toKebab } from "./capitaliseString.js";
2
+ /** ELBv2's hard limit on an Application Load Balancer's name. */
3
+ const LOAD_BALANCER_NAME_MAX_LENGTH = 32;
2
4
  /**
3
5
  * Centralised resource naming for infrastructure.
4
6
  * Ensures consistent, predictable names across constructs and tests.
@@ -73,4 +75,76 @@ export class ResourceNaming {
73
75
  static readReplicaSecurityGroupDescription(name) {
74
76
  return `Security group for RDS Read Replica ${name}`;
75
77
  }
78
+ /**
79
+ * KMS alias for a database's storage encryption key. `name` is the logical
80
+ * database name, and the casing is load-bearing — KMS aliases are
81
+ * case-sensitive and these are live on deployed keys.
82
+ *
83
+ * The three alias helpers return the value handed to `CustomerManagedKey`,
84
+ * which is what CDK prefixes with `alias/`; `kms list-aliases` reports the
85
+ * prefixed form.
86
+ */
87
+ static storageEncryptionKeyAlias(name) {
88
+ return `cmk/rds/${name}/encryptionKey`;
89
+ }
90
+ /** KMS alias for a database's Database Insights encryption key. */
91
+ static insightsKeyAlias(name) {
92
+ return `cmk/rds/${name}/InsightsKey`;
93
+ }
94
+ /** KMS alias for a read replica's Database Insights encryption key. */
95
+ static readReplicaInsightsKeyAlias(name) {
96
+ return `cmk/rds/${name}/ReadReplicaInsightsKey`;
97
+ }
98
+ /**
99
+ * Construct id of the RDS child the Database pattern creates for an
100
+ * `Instance` or `Aurora` plan. The instance, cluster, proxy and secret
101
+ * helpers are all fed this id, NOT the Database construct's own id, so
102
+ * anything predicting a deployed name from a resource plan starts here.
103
+ */
104
+ static rdsChildConstructId(databaseName) {
105
+ return `${databaseName}Rds`;
106
+ }
107
+ /** Construct id of the RdsAuroraGlobal child, which names the cluster. */
108
+ static globalRdsChildConstructId(databaseName) {
109
+ return `${databaseName}RdsGlobal`;
110
+ }
111
+ /**
112
+ * Construct id of the RdsAurora primary inside RdsAuroraGlobal, which names
113
+ * its instances, proxy and credentials secret.
114
+ */
115
+ static globalPrimaryConstructId(databaseName) {
116
+ return `${databaseName}Primary`;
117
+ }
118
+ /** Construct id of a database's credentials Secret. */
119
+ static credentialsSecretConstructId(databaseName) {
120
+ return `${databaseName}Credentials`;
121
+ }
122
+ /**
123
+ * Alias a Secret or CustomerManagedKey takes when the caller supplies none.
124
+ * CDK prefixes it with `alias/`, which is the form `kms list-aliases`
125
+ * reports.
126
+ */
127
+ static defaultKeyAlias(constructId) {
128
+ return `cmk/${constructId}`;
129
+ }
130
+ /**
131
+ * S3 bucket holding a VPC's flow logs. `id` is the Network construct id —
132
+ * the network name for an additional VPC, `${stackPrefix}Vpc` for the
133
+ * primary. Lowercased rather than kebabed: S3 forbids uppercase and this
134
+ * name is already live on deployed buckets, so the transform cannot change.
135
+ */
136
+ static vpcFlowLogBucketName(id, accountId) {
137
+ return `vpc-flowlogs-${id.toLowerCase()}-${accountId}`;
138
+ }
139
+ /**
140
+ * ALB physical name for an ECS cluster. ELBv2 caps names at 32 characters
141
+ * and rejects a trailing hyphen, so a long cluster name is truncated and
142
+ * then stripped — which means the name is NOT simply
143
+ * `${clusterName}LoadBalancer` and must not be re-derived as if it were.
144
+ */
145
+ static loadBalancerName(clusterName) {
146
+ return `${clusterName}LoadBalancer`
147
+ .slice(0, LOAD_BALANCER_NAME_MAX_LENGTH)
148
+ .replace(/-+$/, "");
149
+ }
76
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "7.2.0",
3
+ "version": "7.3.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -51,6 +51,10 @@
51
51
  "typecheck:tests": "tsc --noEmit -p tsconfig.test.json",
52
52
  "check:scripts": "tsc --project tsconfig.scripts.json",
53
53
  "deploy-test": "tsx deploy-tests/run.mts",
54
+ "deploy-test:ops": "tsx deploy-tests/ops.mts",
55
+ "deploy-test:select": "tsx deploy-tests/select.mts",
56
+ "deploy-test:pipeline": "tsx deploy-tests/pipeline/renderPipelineSteps.mts",
57
+ "deploy-test:pipeline:check": "tsx deploy-tests/pipeline/renderPipelineSteps.mts --check",
54
58
  "lint": "eslint lib --no-warn-ignored",
55
59
  "lint:fix": "eslint lib --fix --no-warn-ignored",
56
60
  "format": "prettier --write \"lib/**/*.{ts,tsx,js,jsx,json}\" \"scripts/**/*.mts\" \"deploy-tests/**/*.mts\"",
@@ -74,8 +78,8 @@
74
78
  },
75
79
  "dependencies": {
76
80
  "@aws-sdk/client-organizations": "^3.1098.0",
77
- "@fjall/generator": "^7.2.0",
78
- "@fjall/util": "^7.2.0",
81
+ "@fjall/generator": "^7.3.0",
82
+ "@fjall/util": "^7.3.0",
79
83
  "constructs": "^10.7.2"
80
84
  },
81
85
  "overrides": {