@fjall/components-infrastructure 21.0.0 → 22.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.
@@ -2,6 +2,7 @@ import { CfnOutput } from "aws-cdk-lib";
2
2
  import { Construct } from "constructs";
3
3
  import { Ipam as IpamClass } from "../../resources/aws/networking/ipam.js";
4
4
  import { getConfig } from "../../utils/getConfig.js";
5
+ import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../utils/costAllocationTags.js";
5
6
  import { IPAM_PRIVATE_DEFAULT_SCOPE_EXPORT_NAME } from "../../utils/exportNaming.js";
6
7
  export class Ipam extends Construct {
7
8
  privateDefaultScopeId;
@@ -18,8 +19,11 @@ export class Ipam extends Construct {
18
19
  })),
19
20
  tags: [
20
21
  {
21
- key: "fjall:costAllocation:environment",
22
- value: "management"
22
+ // Explicit claim, not a fallback: the IPAM is an org-wide pool that
23
+ // genuinely lives in the management estate regardless of which app
24
+ // synthesises it.
25
+ key: COST_ALLOCATION_TAGS.ENVIRONMENT,
26
+ value: DEFAULT_COST_ALLOCATION_ENVIRONMENT
23
27
  }
24
28
  ]
25
29
  });
@@ -1,4 +1,4 @@
1
- import { CfnOutput, Stack, Tags } from "aws-cdk-lib";
1
+ import { CfnOutput, Lazy, Stack, Tags } from "aws-cdk-lib";
2
2
  import { EcrDefaultImage, SharedAlarmTopic } from "../../config/aws/index.js";
3
3
  import { ManagementEventsTrail } from "../../config/aws/cloudTrail.js";
4
4
  import { OidcConnector } from "../../config/aws/oidcConnector.js";
@@ -16,6 +16,18 @@ import { SecurityHubHub } from "../../config/aws/securityHubHub.js";
16
16
  import { ConfigRecorder } from "../../config/aws/configRecorder.js";
17
17
  import { AccountAccessAnalyser } from "../../config/aws/accessAnalyser.js";
18
18
  import { InspectorEnablement } from "../../config/aws/inspectorEnablement.js";
19
+ /**
20
+ * `fjall:description` values for the governance tiers — the same user-facing
21
+ * gold-plating the domain-side constructs carry, so a governance stack's
22
+ * resources explain themselves in the console exactly like a zone or
23
+ * certificate does. Keyed by `organisationType` so each tier self-describes
24
+ * from ONE stamp in this base class.
25
+ */
26
+ const GOVERNANCE_TIER_DESCRIPTIONS = {
27
+ organisation: "Fjall-managed organisation governance: org root, member accounts and organisation-wide policy",
28
+ platform: "Fjall-managed platform governance: shared platform services and CI trust",
29
+ account: "Fjall-managed account governance: account security and operations baseline"
30
+ };
19
31
  export class Account extends Stack {
20
32
  organisationType = "account";
21
33
  resolvedRegion;
@@ -33,6 +45,13 @@ export class Account extends Stack {
33
45
  // read "FjallApp"; the stack-scope tag wins over App scope and names the
34
46
  // governance tier instead ("Organisation"/"Account"/"Platform").
35
47
  Tags.of(this).add(COST_ALLOCATION_TAGS.SERVICE, id);
48
+ // Lazy, NOT a direct read: subclass field initialisers run after super(),
49
+ // so `this.organisationType` here would say "account" for every tier (the
50
+ // same trap the connector-mode note below documents). By synth the field
51
+ // is final and the tag renders the true tier.
52
+ Tags.of(this).add("fjall:description", Lazy.string({
53
+ produce: () => GOVERNANCE_TIER_DESCRIPTIONS[this.organisationType]
54
+ }));
36
55
  this.resolvedRegion = region ?? this.region;
37
56
  const orgId = this.node.tryGetContext(CDK_CONTEXT_KEYS.ORG_ID);
38
57
  if (orgId) {
@@ -24,8 +24,8 @@ export const DATABASE_ENGINE_CONFIG = Object.freeze({
24
24
  sslParameters: { "rds.force_ssl": "1" }
25
25
  },
26
26
  mysql: {
27
- // majorVersion must be Aurora version (3.12.0), NOT MySQL version (8.0)
28
- aurora: { version: "8.0.mysql_aurora.3.12.0", majorVersion: "3.12.0" },
27
+ // majorVersion must be Aurora version (3.13.0), NOT MySQL version (8.0)
28
+ aurora: { version: "8.0.mysql_aurora.3.13.0", majorVersion: "3.13.0" },
29
29
  instance: { version: "8.4.9", majorVersion: "8.4" },
30
30
  defaultUsername: "admin",
31
31
  sslParameters: { require_secure_transport: "ON" }
@@ -303,13 +303,11 @@ export function addHostedZone(ctx, loadBalancer) {
303
303
  domainName,
304
304
  ...(subjectAlternativeNames.length > 0 && { subjectAlternativeNames }),
305
305
  hostedZone,
306
- // Honest environment tag: the cluster's app already tags the
306
+ // No costAllocationEnvironment: the cluster's app already tags the
307
307
  // account-stage-resolved environment tree-wide (App.globalTags via
308
- // getConfig); the mint must inherit it, not out-tag it with the
309
- // resource-level "management" default. No sniffing here any local
310
- // re-resolution (context/env-var channels) can diverge from the app
311
- // cascade, and did on the first live plan.
312
- inheritCostAllocationEnvironment: true,
308
+ // getConfig) and the mint inherits it. No local re-resolution
309
+ // (context/env-var channels) it can diverge from the app cascade,
310
+ // and did on the first live plan.
313
311
  // The cert is consumed in-stack by the ALB listener; exporting would
314
312
  // collide with a managed-domain/apex stack that owns the domain's
315
313
  // export names.
@@ -1,11 +1,11 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Construct } from "constructs";
3
- import { ArnFormat, CfnOutput, Stack, Tags } from "aws-cdk-lib";
3
+ import { ArnFormat, CfnOutput, Stack } from "aws-cdk-lib";
4
4
  import { OrganizationPrincipal, PolicyDocument, PolicyStatement } from "aws-cdk-lib/aws-iam";
5
5
  import { getDomainExportNames } from "@fjall/util";
6
6
  import { Role } from "./role.js";
7
7
  import { capitaliseString, toPascalCase, getSafeZoneName } from "../../../utils/capitaliseString.js";
8
- import { DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../utils/costAllocationTags.js";
8
+ import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
9
9
  // The suffix is a deployed contract: the SCP dev-isolation carve-out
10
10
  // wildcards arn:aws:iam::*:role/*DelegateHostedZoneRole (scpPreset).
11
11
  const ROLE_NAME_SUFFIX = "DelegateHostedZoneRole";
@@ -86,9 +86,11 @@ export class DelegationRole extends Construct {
86
86
  props.hostedZone.grantDelegation(role);
87
87
  this.role = role;
88
88
  this.roleArn = role.roleArn;
89
- Tags.of(this).add("fjall:costAllocation:environment", props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
90
- Tags.of(this).add("fjall:costAllocation:service", "delegationRole");
91
- Tags.of(this).add("fjall:costAllocation:domain", props.costAllocationDomain ?? props.zoneName);
89
+ applyCostAllocationTags(this, {
90
+ service: "delegationRole",
91
+ domain: props.costAllocationDomain ?? props.zoneName,
92
+ environment: props.costAllocationEnvironment
93
+ });
92
94
  const exportName = getDomainExportNames(props.zoneName).delegationRoleArn;
93
95
  this.exportName = exportName;
94
96
  new CfnOutput(this, `${safeZone}DelegationRoleArn`, {
@@ -7,16 +7,8 @@ export interface DomainCertificateProps {
7
7
  readonly hostedZone: IHostedZone;
8
8
  readonly description?: string;
9
9
  readonly transparencyLogging?: boolean;
10
+ /** Omit to inherit the App cascade's environment tag (see CostAllocationTagsArgs). */
10
11
  readonly costAllocationEnvironment?: string;
11
- /**
12
- * Inherit the environment tag from an outer cascade (the fjall App's
13
- * global tags) instead of stamping the "management" default when
14
- * `costAllocationEnvironment` is not given. In-stack certificates minted
15
- * inside an application (the ECS fallthrough mint) must set this: the
16
- * cluster's app already tags the resolved environment tree-wide, and the
17
- * resource-level default would out-tag it (deeper scope wins).
18
- */
19
- readonly inheritCostAllocationEnvironment?: boolean;
20
12
  readonly costAllocationDomain?: string;
21
13
  /**
22
14
  * Publish the `${domain}-certificate-arn` CloudFormation export. Default true.
@@ -40,8 +40,7 @@ export class DomainCertificate extends Certificate {
40
40
  applyCostAllocationTags(this, {
41
41
  service: "certificate",
42
42
  domain: sanitiseAcmTagValue(props.costAllocationDomain ?? props.domainName),
43
- environment: props.costAllocationEnvironment,
44
- inheritEnvironment: props.inheritCostAllocationEnvironment
43
+ environment: props.costAllocationEnvironment
45
44
  });
46
45
  if (props.exportCertificateArn !== false) {
47
46
  const safeKey = toPascalCase(props.domainName.split(".").join(""));
@@ -6,7 +6,7 @@ export interface VpcPeeringAccepterRoleProps {
6
6
  readonly requesterAccountIds: string[];
7
7
  /** The local VPC this role is scoped to. */
8
8
  readonly localVpc: IVpc;
9
- /** Cost-allocation override defaults to `"management"`. */
9
+ /** Omit to inherit the App cascade's environment tag (see CostAllocationTagsArgs). */
10
10
  readonly costAllocationEnvironment?: string;
11
11
  /** Cost-allocation override — defaults to the local VPC's construct id. */
12
12
  readonly costAllocationDomain?: string;
@@ -1,7 +1,7 @@
1
1
  import { Construct } from "constructs";
2
2
  import { ArnFormat, Stack, Tags } from "aws-cdk-lib";
3
3
  import { AccountPrincipal, CompositePrincipal, PolicyDocument, PolicyStatement, Role } from "aws-cdk-lib/aws-iam";
4
- import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../utils/costAllocationTags.js";
4
+ import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
5
5
  export class VpcPeeringAccepterRole extends Construct {
6
6
  role;
7
7
  roleArn;
@@ -54,8 +54,10 @@ export class VpcPeeringAccepterRole extends Construct {
54
54
  });
55
55
  this.roleArn = this.role.roleArn;
56
56
  Tags.of(this).add("fjall:resource:type", "vpc-peering-accepter");
57
- Tags.of(this).add(COST_ALLOCATION_TAGS.ENVIRONMENT, props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
58
- Tags.of(this).add(COST_ALLOCATION_TAGS.SERVICE, "vpcPeeringAccepter");
59
- Tags.of(this).add(COST_ALLOCATION_TAGS.DOMAIN, props.costAllocationDomain ?? props.localVpc.node.id);
57
+ applyCostAllocationTags(this, {
58
+ service: "vpcPeeringAccepter",
59
+ domain: props.costAllocationDomain ?? props.localVpc.node.id,
60
+ environment: props.costAllocationEnvironment
61
+ });
60
62
  }
61
63
  }
@@ -3,7 +3,7 @@ import { Stack, Tags } from "aws-cdk-lib";
3
3
  import { CfnRoute, CfnVPCPeeringConnection, SubnetType } from "aws-cdk-lib/aws-ec2";
4
4
  import { PolicyStatement } from "aws-cdk-lib/aws-iam";
5
5
  import { AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
6
- import { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT } from "../../../utils/costAllocationTags.js";
6
+ import { applyCostAllocationTags } from "../../../utils/costAllocationTags.js";
7
7
  import { AwsCustomResource } from "../utilities/awsCustomResource.js";
8
8
  import { CrossAccountReturnRoutes } from "./crossAccountReturnRoutes.js";
9
9
  export class VpcPeeringConnection extends Construct {
@@ -100,8 +100,10 @@ export class VpcPeeringConnection extends Construct {
100
100
  if (props.peerAppName) {
101
101
  Tags.of(this).add("fjall:peering:peer-app", props.peerAppName);
102
102
  }
103
- Tags.of(this).add(COST_ALLOCATION_TAGS.ENVIRONMENT, props.costAllocationEnvironment ?? DEFAULT_COST_ALLOCATION_ENVIRONMENT);
104
- Tags.of(this).add(COST_ALLOCATION_TAGS.SERVICE, "vpcPeering");
105
- Tags.of(this).add(COST_ALLOCATION_TAGS.DOMAIN, props.costAllocationDomain ?? props.peerAppName ?? props.peerVpcId);
103
+ applyCostAllocationTags(this, {
104
+ service: "vpcPeering",
105
+ domain: props.costAllocationDomain ?? props.peerAppName ?? props.peerVpcId,
106
+ environment: props.costAllocationEnvironment
107
+ });
106
108
  }
107
109
  }
@@ -4,16 +4,17 @@ export { COST_ALLOCATION_TAGS, DEFAULT_COST_ALLOCATION_ENVIRONMENT };
4
4
  export interface CostAllocationTagsArgs {
5
5
  readonly service: string;
6
6
  readonly domain: string;
7
- readonly environment?: string;
8
7
  /**
9
- * When `environment` is not given, skip the environment tag at this scope
10
- * entirely instead of stamping the "management" default, so an outer
11
- * cascade (the fjall App's global tags carry the account-stage-resolved
12
- * environment) provides it. Without this, the deeper-scope default wins
13
- * CDK tag resolution and a production stack's resource is mis-tagged
14
- * "management". An explicit `environment` always wins over inheritance.
8
+ * Omit to INHERIT: the fjall App root always stamps the account-stage-
9
+ * resolved environment tree-wide ("unknown" when unresolvable getConfig
10
+ * never leaves it blank), and CDK resolves tag conflicts deeper-scope-wins,
11
+ * so a construct-level fallback here would out-tag the truth. That is how
12
+ * live zones were mis-tagged "management" inside staged apps. A construct
13
+ * stamps an environment ONLY when its caller makes an explicit claim —
14
+ * including an explicit `DEFAULT_COST_ALLOCATION_ENVIRONMENT` for genuinely
15
+ * stage-less, management-estate resources.
15
16
  */
16
- readonly inheritEnvironment?: boolean;
17
+ readonly environment?: string;
17
18
  readonly owner?: string;
18
19
  }
19
20
  export declare function applyCostAllocationTags(scope: IConstruct, args: CostAllocationTagsArgs): void;
@@ -8,9 +8,6 @@ export function applyCostAllocationTags(scope, args) {
8
8
  if (args.environment !== undefined) {
9
9
  Tags.of(scope).add(COST_ALLOCATION_TAGS.ENVIRONMENT, args.environment);
10
10
  }
11
- else if (args.inheritEnvironment !== true) {
12
- Tags.of(scope).add(COST_ALLOCATION_TAGS.ENVIRONMENT, DEFAULT_COST_ALLOCATION_ENVIRONMENT);
13
- }
14
11
  Tags.of(scope).add(COST_ALLOCATION_TAGS.SERVICE, args.service);
15
12
  Tags.of(scope).add(COST_ALLOCATION_TAGS.DOMAIN, args.domain);
16
13
  if (args.owner !== undefined) {
@@ -4,20 +4,29 @@
4
4
  * Also writes to JSONL file for debugging and support.
5
5
  */
6
6
  import { appendFileSync, existsSync, mkdirSync } from "fs";
7
- import { homedir } from "os";
8
7
  import { join } from "path";
9
- const ENV_LOG_DIR = process.env.FJALL_LOG_DIR;
10
- const LOG_DIR = ENV_LOG_DIR !== undefined && ENV_LOG_DIR !== ""
11
- ? ENV_LOG_DIR
12
- : join(homedir(), ".fjall", "logs");
13
- const LOG_FILE = join(LOG_DIR, "infrastructure.jsonl");
8
+ import { fjallLogDir } from "@fjall/util";
9
+ import { FileRotator, LOG_ROTATION_DEFAULTS } from "@fjall/util/logRotation";
10
+ const LOG_FILENAME = "infrastructure.jsonl";
11
+ /**
12
+ * Shared with the CLI's own logs, so this file cannot outgrow the budget
13
+ * the rest of Fjall's logging keeps to. Without it this log had no bound of
14
+ * any kind: it is appended to on every CDK synth, and one developer machine
15
+ * reached 267MB of it inside a single release cycle.
16
+ */
17
+ const rotator = new FileRotator({
18
+ maxFiles: LOG_ROTATION_DEFAULTS.MAX_FILES
19
+ });
14
20
  /**
15
21
  * Write a log entry to the infrastructure JSONL file
16
22
  */
17
23
  function writeToLog(level, message) {
18
24
  try {
19
- if (!existsSync(LOG_DIR)) {
20
- mkdirSync(LOG_DIR, { recursive: true });
25
+ // Resolved per write, not at module load: the log dir is an env var and
26
+ // CDK imports this module long before a command has read its environment.
27
+ const logDir = fjallLogDir();
28
+ if (!existsSync(logDir)) {
29
+ mkdirSync(logDir, { recursive: true });
21
30
  }
22
31
  const entry = JSON.stringify({
23
32
  timestamp: new Date().toISOString(),
@@ -26,7 +35,9 @@ function writeToLog(level, message) {
26
35
  message,
27
36
  source: "cdk-subprocess"
28
37
  });
29
- appendFileSync(LOG_FILE, entry + "\n");
38
+ const logPath = join(logDir, LOG_FILENAME);
39
+ rotator.rotateIfNeeded(logPath, LOG_ROTATION_DEFAULTS.MAX_FILE_SIZE);
40
+ appendFileSync(logPath, entry + "\n");
30
41
  }
31
42
  catch (err) {
32
43
  console.debug("[Fjall] writeToLog failed:", err instanceof Error ? err.message : String(err));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "21.0.0",
3
+ "version": "22.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -80,8 +80,8 @@
80
80
  },
81
81
  "dependencies": {
82
82
  "@aws-sdk/client-organizations": "^3.1098.0",
83
- "@fjall/generator": "^21.0.0",
84
- "@fjall/util": "^21.0.0",
83
+ "@fjall/generator": "^22.0.0",
84
+ "@fjall/util": "^22.0.0",
85
85
  "constructs": "^10.7.2"
86
86
  },
87
87
  "overrides": {