@fjall/components-infrastructure 2.19.3 → 2.19.5

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.
@@ -5,6 +5,7 @@ import { ManagementEventsTrail } from "../../config/aws/cloudTrail.js";
5
5
  import { OrganisationTrail } from "../../config/aws/organisationTrail.js";
6
6
  import { ScpPreset } from "../../config/aws/scpPreset.js";
7
7
  import { OrganisationResource, OrganisationAccount, CostAllocationTagActivator } from "../../resources/aws/organisation/index.js";
8
+ import { Schedule } from "../../resources/aws/messaging/schedule.js";
8
9
  import { stripAndCamelCase } from "../../utils/stripAndCamelCase.js";
9
10
  import { CDK_CONTEXT_KEYS, resolveAccountTrailState } from "../../utils/cdkContext.js";
10
11
  import { getConfig } from "../../utils/getConfig.js";
@@ -134,13 +135,26 @@ export class Organisation extends Account {
134
135
  }
135
136
  setupOrganisationFeatures(identityCenter, managementAccountId) {
136
137
  this.setupIdentityCenter(identityCenter, managementAccountId);
137
- this.setupCostAllocationTagActivator();
138
+ // Home region only: Cost Explorer is a global service (single us-east-1
139
+ // endpoint), so an org-region cascade stack would otherwise synthesise a
140
+ // redundant daily activator Lambda per region against the same global
141
+ // endpoint — mirrors the OrganisationTrail home-region guard above.
142
+ if (!this.accountGlobalsConfigured) {
143
+ this.setupCostAllocationTagActivator();
144
+ }
138
145
  }
139
146
  setupCostAllocationTagActivator() {
140
147
  const activator = new CostAllocationTagActivator(this, "CostAllocationTagActivator");
141
- App.getInstance().addSchedule("CostAllocationTagActivatorSchedule", {
148
+ // Construct the schedule in THIS (the Organisation) stack rather than via
149
+ // App.addSchedule(...), which places the EventBridge rule + invoke
150
+ // permission in the App messaging stack — a stack the org deploy never
151
+ // targets, so the trigger never deploys (the Lambda exists but is never
152
+ // invoked). Co-locating the rule here means one `cdk deploy Organisation`
153
+ // ships the activator and its daily trigger together.
154
+ new Schedule(this, "CostAllocationTagActivatorSchedule", {
142
155
  schedule: "rate(24 hours)",
143
- target: activator
156
+ target: activator,
157
+ appName: App.getInstance().getName()
144
158
  });
145
159
  }
146
160
  setupIdentityCenter(identityCenter, managementAccountId) {
@@ -4,16 +4,24 @@ import { LambdaFunction } from "../compute/lambda.js";
4
4
  /**
5
5
  * Deploys a Lambda that auto-discovers and activates cost allocation tags.
6
6
  *
7
- * Runs daily in the management account. Any non-AWS tag applied to resources
8
- * (via app.addTags() or CDK aspects) is automatically activated in Cost
9
- * Explorer within 24 hours no manual intervention required.
7
+ * Runs in the management account. Any non-AWS tag applied to resources (via
8
+ * app.addTags() or CDK aspects) is automatically activated in Cost Explorer —
9
+ * no manual intervention required. Cost Explorer is a global service, so the
10
+ * handler pins the us-east-1 endpoint and the caller synthesises this activator
11
+ * in the organisation's home region only.
10
12
  *
11
- * The schedule is NOT wired by this construct — callers wire the cadence via
12
- * `app.addSchedule(...)` so the EventBridge rule lives in the App's messaging
13
- * stack alongside every other Fjall schedule (D9). The activator implements
14
- * the structural `LambdaComputeShape` (duck-typed by `eventTargets.ts`), so it
15
- * can be passed directly as `target` to `app.addSchedule(...)` without a
16
- * `LambdaCompute` wrapper.
13
+ * Activation runs on two cadences:
14
+ * - A deploy-time one-shot (the `AwsCustomResource` below) invokes the Lambda
15
+ * on org-stack create so tags already surfaced by AWS activate immediately,
16
+ * without waiting for the first daily tick.
17
+ * - A daily schedule, wired by the caller (`organisation.ts`) as a `Schedule`
18
+ * constructed in the Organisation stack — co-located with this Lambda so a
19
+ * single `cdk deploy Organisation` deploys the EventBridge rule + invoke
20
+ * permission. (A cross-stack `app.addSchedule(...)` placed that rule in the
21
+ * App messaging stack, which the org deploy never targets, so the trigger
22
+ * never materialised.) The activator implements the structural
23
+ * `LambdaComputeShape` (duck-typed by `eventTargets.ts`), so it can be passed
24
+ * directly as the `Schedule` target without a `LambdaCompute` wrapper.
17
25
  */
18
26
  export declare class CostAllocationTagActivator extends Construct {
19
27
  /** Marks this construct as a structural Lambda target for `eventTargets.ts`. */
@@ -1,21 +1,31 @@
1
1
  import { Construct } from "constructs";
2
2
  import { PhysicalName } from "aws-cdk-lib";
3
+ import { AwsCustomResourcePolicy, PhysicalResourceId } from "aws-cdk-lib/custom-resources";
3
4
  import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
4
5
  import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
5
6
  import { LambdaFunction } from "../compute/lambda.js";
7
+ import { AwsCustomResource } from "../utilities/awsCustomResource.js";
6
8
  /**
7
9
  * Deploys a Lambda that auto-discovers and activates cost allocation tags.
8
10
  *
9
- * Runs daily in the management account. Any non-AWS tag applied to resources
10
- * (via app.addTags() or CDK aspects) is automatically activated in Cost
11
- * Explorer within 24 hours no manual intervention required.
11
+ * Runs in the management account. Any non-AWS tag applied to resources (via
12
+ * app.addTags() or CDK aspects) is automatically activated in Cost Explorer —
13
+ * no manual intervention required. Cost Explorer is a global service, so the
14
+ * handler pins the us-east-1 endpoint and the caller synthesises this activator
15
+ * in the organisation's home region only.
12
16
  *
13
- * The schedule is NOT wired by this construct — callers wire the cadence via
14
- * `app.addSchedule(...)` so the EventBridge rule lives in the App's messaging
15
- * stack alongside every other Fjall schedule (D9). The activator implements
16
- * the structural `LambdaComputeShape` (duck-typed by `eventTargets.ts`), so it
17
- * can be passed directly as `target` to `app.addSchedule(...)` without a
18
- * `LambdaCompute` wrapper.
17
+ * Activation runs on two cadences:
18
+ * - A deploy-time one-shot (the `AwsCustomResource` below) invokes the Lambda
19
+ * on org-stack create so tags already surfaced by AWS activate immediately,
20
+ * without waiting for the first daily tick.
21
+ * - A daily schedule, wired by the caller (`organisation.ts`) as a `Schedule`
22
+ * constructed in the Organisation stack — co-located with this Lambda so a
23
+ * single `cdk deploy Organisation` deploys the EventBridge rule + invoke
24
+ * permission. (A cross-stack `app.addSchedule(...)` placed that rule in the
25
+ * App messaging stack, which the org deploy never targets, so the trigger
26
+ * never materialised.) The activator implements the structural
27
+ * `LambdaComputeShape` (duck-typed by `eventTargets.ts`), so it can be passed
28
+ * directly as the `Schedule` target without a `LambdaCompute` wrapper.
19
29
  */
20
30
  export class CostAllocationTagActivator extends Construct {
21
31
  /** Marks this construct as a structural Lambda target for `eventTargets.ts`. */
@@ -43,6 +53,36 @@ export class CostAllocationTagActivator extends Construct {
43
53
  })
44
54
  ]
45
55
  });
56
+ // Deploy-time one-shot: invoke the activator on org-stack create so tags
57
+ // already surfaced by AWS activate immediately rather than waiting for the
58
+ // first daily tick. Async (`Event`) and idempotent, so it never blocks or
59
+ // fails the deploy; the stable physicalResourceId means it fires on create
60
+ // (and only re-fires if the target function is replaced).
61
+ const activationParameters = {
62
+ FunctionName: this.lambda.functionName,
63
+ InvocationType: "Event"
64
+ };
65
+ new AwsCustomResource(this, "DeployTimeActivation", {
66
+ onCreate: {
67
+ service: "Lambda",
68
+ action: "invoke",
69
+ parameters: activationParameters,
70
+ physicalResourceId: PhysicalResourceId.of("cost-allocation-tag-activation")
71
+ },
72
+ onUpdate: {
73
+ service: "Lambda",
74
+ action: "invoke",
75
+ parameters: activationParameters,
76
+ physicalResourceId: PhysicalResourceId.of("cost-allocation-tag-activation")
77
+ },
78
+ policy: AwsCustomResourcePolicy.fromStatements([
79
+ new PolicyStatement({
80
+ effect: Effect.ALLOW,
81
+ actions: ["lambda:InvokeFunction"],
82
+ resources: [this.lambda.functionArn]
83
+ })
84
+ ])
85
+ });
46
86
  }
47
87
  /**
48
88
  * Structural `LambdaComputeShape` accessor — returns the activator's Lambda
@@ -56,7 +96,8 @@ const HANDLER_CODE = `
56
96
  const { CostExplorerClient, ListCostAllocationTagsCommand, UpdateCostAllocationTagsStatusCommand } = require("@aws-sdk/client-cost-explorer");
57
97
 
58
98
  exports.handler = async () => {
59
- const client = new CostExplorerClient({});
99
+ // Cost Explorer is a global service reachable only via the us-east-1 endpoint.
100
+ const client = new CostExplorerClient({ region: "us-east-1" });
60
101
  const { CostAllocationTags = [] } = await client.send(new ListCostAllocationTagsCommand({ MaxResults: 1000 }));
61
102
 
62
103
  const inactive = CostAllocationTags.filter(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "2.19.3",
3
+ "version": "2.19.5",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,8 +62,8 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@aws-sdk/client-organizations": "^3.1038.0",
65
- "@fjall/generator": "^2.19.3",
66
- "@fjall/util": "^2.19.3",
65
+ "@fjall/generator": "^2.19.5",
66
+ "@fjall/util": "^2.19.5",
67
67
  "constructs": "^10.0.0"
68
68
  },
69
69
  "overrides": {
@@ -77,5 +77,5 @@
77
77
  "engines": {
78
78
  "node": ">=18.0.0"
79
79
  },
80
- "gitHead": "622bf104895ff3f51147de46d5b70c5928aaaa86"
80
+ "gitHead": "8179898f55c1467404e6a715ead5404404e45be0"
81
81
  }