@fjall/components-infrastructure 6.0.0 → 7.0.1

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 (29) hide show
  1. package/dist/lib/patterns/aws/clickhouseDatabase.js +4 -0
  2. package/dist/lib/resources/aws/compute/ec2.d.ts +31 -14
  3. package/dist/lib/resources/aws/compute/ec2.js +34 -10
  4. package/dist/lib/resources/aws/compute/ecsCapacityConfig.d.ts +56 -16
  5. package/dist/lib/resources/aws/compute/ecsCapacityConfig.js +226 -64
  6. package/dist/lib/resources/aws/compute/ecsConstants.d.ts +15 -0
  7. package/dist/lib/resources/aws/compute/ecsConstants.js +15 -0
  8. package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +16 -6
  9. package/dist/lib/resources/aws/compute/ecsServiceFactory.js +68 -19
  10. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +33 -4
  11. package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +15 -5
  12. package/dist/lib/resources/aws/compute/persistentDataVolume.js +9 -6
  13. package/dist/lib/utils/capacityIdentityContext.d.ts +20 -0
  14. package/dist/lib/utils/capacityIdentityContext.js +43 -0
  15. package/dist/lib/utils/manifestWriter.d.ts +19 -2
  16. package/dist/lib/utils/manifestWriter.js +35 -0
  17. package/package.json +4 -5
  18. package/dist/lib/config/aws/identityCenterMembership.d.ts +0 -11
  19. package/dist/lib/config/aws/identityCenterMembership.js +0 -61
  20. package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets +0 -30
  21. package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets.mjs +0 -212
  22. package/dist/lib/patterns/aws/buildkite/alarms.d.ts +0 -25
  23. package/dist/lib/patterns/aws/buildkite/alarms.js +0 -78
  24. package/dist/lib/patterns/aws/domainDelegation.d.ts +0 -8
  25. package/dist/lib/patterns/aws/domainDelegation.js +0 -45
  26. package/dist/lib/patterns/aws/domainFactory.d.ts +0 -23
  27. package/dist/lib/patterns/aws/domainFactory.js +0 -61
  28. package/dist/lib/utils/addSuffixToEmail.d.ts +0 -1
  29. package/dist/lib/utils/addSuffixToEmail.js +0 -3
@@ -389,6 +389,10 @@ export class ClickHouseDatabase extends Construct {
389
389
  securityGroups: [securityGroup],
390
390
  deployment: { minHealthyPercent: 0, maxHealthyPercent: 100 },
391
391
  ec2Config: {
392
+ // Pattern-chosen stable slot: the composed cluster must never
393
+ // collide with the app's own EC2 compute defaulting "primary"
394
+ // (slots are app-scoped; collision is a hard synth error).
395
+ slot: "clickhouse",
392
396
  instanceType,
393
397
  machineImage: EcsOptimizedImage.amazonLinux2023(amiHardwareType),
394
398
  instanceMonitoring: Monitoring.BASIC,
@@ -4,28 +4,37 @@ import { Duration, Stack, type StackProps } from "aws-cdk-lib";
4
4
  import { type Role } from "aws-cdk-lib/aws-iam";
5
5
  import { AutoScalingGroup, Monitoring, type WarmPoolOptions } from "aws-cdk-lib/aws-autoscaling";
6
6
  import { type PersistentDataVolumeProps } from "./persistentDataVolume.js";
7
- export type Ec2InstancePersistentDataVolumeConfig = Omit<PersistentDataVolumeProps, "autoScalingGroup">;
7
+ export type Ec2InstancePersistentDataVolumeConfig = Omit<PersistentDataVolumeProps, "autoScalingGroup" | "stableOwnerId">;
8
8
  /**
9
9
  * Caller-supplied ASG `UpdatePolicy` resolution. Default (`undefined`) →
10
- * `rollingUpdate({ minInstancesInService: 0, maxBatchSize: 1, pauseTime:
11
- * Duration.minutes(5) })`. The rolling shape propagates userdata mutations
12
- * to running instances on every `LaunchTemplateVersion` change; data
13
- * continuity for stateful patterns is preserved by `persistentDataVolume`.
10
+ * `instanceRefresh` with `minHealthyPercentage 0 / maxHealthyPercentage 100`
11
+ * when `persistentDataVolume` is set (single-attach EBS forbids a surge
12
+ * instance), `100 / 200` otherwise (surge-then-shrink, no capacity dip).
13
+ * Either shape propagates userdata/launch-template mutations to running
14
+ * instances on every `LaunchTemplateVersion` change; data continuity for
15
+ * stateful patterns is preserved by `persistentDataVolume`.
14
16
  *
15
17
  * Variants:
16
- * - `rollingUpdate` — single-instance roll; `pauseTime` overridable.
17
- * `minInstancesInService` and `maxBatchSize` are fixed at `0` and `1`.
18
+ * - `instanceRefresh` — ASG-native rolling replacement via CFN
19
+ * `UpdatePolicy.InstanceRefresh`; percentages overridable.
20
+ * - `rollingUpdate` — legacy CFN-driven single-instance roll; `pauseTime`
21
+ * overridable. `minInstancesInService` and `maxBatchSize` are fixed at
22
+ * `0` and `1`.
18
23
  * - `replacingUpdate` — explicit opt-in to the legacy full-replacement shape.
19
24
  * No Fjall consumer uses this today; retained for future bare-EC2 patterns.
20
- * - `none` — no caller-driven `UpdatePolicy` (omits both
21
- * `AutoScalingRollingUpdate` and `AutoScalingReplacingUpdate`). CDK still
22
- * auto-emits `AutoScalingScheduledAction.IgnoreUnmodifiedGroupSizeProperties`
25
+ * - `none` — no caller-driven `UpdatePolicy` (omits the rolling, replacing
26
+ * AND refresh blocks). CDK still auto-emits
27
+ * `AutoScalingScheduledAction.IgnoreUnmodifiedGroupSizeProperties`
23
28
  * regardless. For callers owning rollout externally.
24
29
  *
25
30
  * See `aiDocs/troubleshooting/clickhouse-rolling-update-window.md` for the
26
31
  * ClickHouse-specific downtime expectations.
27
32
  */
28
33
  export type Ec2InstanceUpdatePolicyConfig = {
34
+ type: "instanceRefresh";
35
+ minHealthyPercentage?: number;
36
+ maxHealthyPercentage?: number;
37
+ } | {
29
38
  type: "rollingUpdate";
30
39
  pauseTime?: Duration;
31
40
  } | {
@@ -105,10 +114,18 @@ export interface Ec2InstanceProps extends StackProps {
105
114
  */
106
115
  persistentDataVolume?: Ec2InstancePersistentDataVolumeConfig;
107
116
  /**
108
- * ASG `UpdatePolicy` resolution. Absent
109
- * `UpdatePolicy.rollingUpdate({ minInstancesInService: 0, maxBatchSize: 1,
110
- * pauseTime: Duration.minutes(5) })`. Userdata mutations propagate to
111
- * running instances via a single-batch rolling roll. See
117
+ * Stable owner identifier for the persistent data volume — becomes the
118
+ * volume's `fjall:OwnerLogicalId` tag and the re-attach Lambdas'
119
+ * `OWNER_LOGICAL_ID`. REQUIRED whenever `persistentDataVolume` is set
120
+ * (rejected at synth otherwise) so the re-attach identity is an explicit
121
+ * caller decision, never an accident of construct-tree position. Must
122
+ * remain byte-stable for the stack's whole life — changing it strands the
123
+ * deployed volume (the Lambda filter stops matching).
124
+ */
125
+ dataVolumeStableOwnerId?: string;
126
+ /**
127
+ * ASG `UpdatePolicy` resolution. Absent → `UpdatePolicy.instanceRefresh`
128
+ * (0/100 with `persistentDataVolume`, 100/200 otherwise). See
112
129
  * `Ec2InstanceUpdatePolicyConfig` for the variant menu and
113
130
  * `aiDocs/troubleshooting/clickhouse-rolling-update-window.md` for the
114
131
  * downtime-window runbook.
@@ -3,7 +3,8 @@ import { Construct } from "constructs";
3
3
  import { resolvePrivateSubnetType } from "../../../utils/vpcUtils.js";
4
4
  import { safeEbs } from "./blockDeviceVolume.js";
5
5
  import { Duration, Stack, Tags } from "aws-cdk-lib";
6
- import { AutoScalingGroup, GroupMetrics, Monitoring, TerminationPolicy, UpdatePolicy, OnDemandAllocationStrategy, SpotAllocationStrategy } from "aws-cdk-lib/aws-autoscaling";
6
+ import { AutoScalingGroup, GroupMetrics, InstanceRefreshStrategy, Monitoring, TerminationPolicy, UpdatePolicy, OnDemandAllocationStrategy, SpotAllocationStrategy } from "aws-cdk-lib/aws-autoscaling";
7
+ import { DEFAULT_ROLLING_UPDATE_PAUSE_SECONDS, INSTANCE_REFRESH_DEFAULT_MAX_HEALTHY, INSTANCE_REFRESH_DEFAULT_MIN_HEALTHY, INSTANCE_REFRESH_PDV_MAX_HEALTHY, INSTANCE_REFRESH_PDV_MIN_HEALTHY } from "./ecsConstants.js";
7
8
  import { AwsCustomResource } from "../utilities/awsCustomResource.js";
8
9
  import { PhysicalResourceId } from "aws-cdk-lib/custom-resources";
9
10
  import { Vpc } from "../networking/vpc.js";
@@ -22,17 +23,17 @@ const DEFAULT_ROOT_DEVICE_NAME = "/dev/xvda";
22
23
  const DEFAULT_ROOT_VOLUME_SIZE_GIB = 30;
23
24
  /**
24
25
  * Resolve the caller's `updatePolicy` prop to a CDK `UpdatePolicy`. Default
25
- * (`config` absent) → single-instance rolling roll on every
26
- * `LaunchTemplateVersion` change. The `none` branch returns `undefined` so
27
- * `addAutoScalingGroup` can conditionally spread the field — passing
28
- * `updatePolicy: undefined` directly emits an empty `UpdatePolicy: {}` block
29
- * on some CDK versions.
26
+ * (`config` absent) → instance refresh, percentages keyed on
27
+ * `hasPersistentDataVolume` (see the constants above). The `none` branch
28
+ * returns `undefined` so `addAutoScalingGroup` can conditionally spread the
29
+ * field — passing `updatePolicy: undefined` directly emits an empty
30
+ * `UpdatePolicy: {}` block on some CDK versions.
30
31
  *
31
32
  * Free function rather than a private method: `this` state is not read, and
32
33
  * a free function is easier to reach from focused tests.
33
34
  */
34
- function resolveUpdatePolicy(config) {
35
- const resolved = config ?? { type: "rollingUpdate" };
35
+ function resolveUpdatePolicy(config, hasPersistentDataVolume) {
36
+ const resolved = config ?? { type: "instanceRefresh" };
36
37
  switch (resolved.type) {
37
38
  case "none":
38
39
  return undefined;
@@ -42,7 +43,20 @@ function resolveUpdatePolicy(config) {
42
43
  return UpdatePolicy.rollingUpdate({
43
44
  minInstancesInService: 0,
44
45
  maxBatchSize: 1,
45
- pauseTime: resolved.pauseTime ?? Duration.minutes(5)
46
+ pauseTime: resolved.pauseTime ??
47
+ Duration.seconds(DEFAULT_ROLLING_UPDATE_PAUSE_SECONDS)
48
+ });
49
+ case "instanceRefresh":
50
+ return UpdatePolicy.instanceRefresh({
51
+ strategy: InstanceRefreshStrategy.ROLLING,
52
+ minHealthyPercentage: resolved.minHealthyPercentage ??
53
+ (hasPersistentDataVolume
54
+ ? INSTANCE_REFRESH_PDV_MIN_HEALTHY
55
+ : INSTANCE_REFRESH_DEFAULT_MIN_HEALTHY),
56
+ maxHealthyPercentage: resolved.maxHealthyPercentage ??
57
+ (hasPersistentDataVolume
58
+ ? INSTANCE_REFRESH_PDV_MAX_HEALTHY
59
+ : INSTANCE_REFRESH_DEFAULT_MAX_HEALTHY)
46
60
  });
47
61
  default:
48
62
  return assertNever(resolved);
@@ -139,7 +153,7 @@ export class Ec2Instance extends Construct {
139
153
  ? SubnetType.PUBLIC
140
154
  : resolvePrivateSubnetType(this.vpc)
141
155
  };
142
- const resolvedUpdatePolicy = resolveUpdatePolicy(props.updatePolicy);
156
+ const resolvedUpdatePolicy = resolveUpdatePolicy(props.updatePolicy, props.persistentDataVolume !== undefined);
143
157
  const baseAsgProps = {
144
158
  vpc: this.vpc,
145
159
  vpcSubnets,
@@ -242,8 +256,11 @@ export class Ec2Instance extends Construct {
242
256
  addPersistentDataVolume(props) {
243
257
  if (props.persistentDataVolume === undefined)
244
258
  return;
259
+ if (props.dataVolumeStableOwnerId === undefined)
260
+ return; // unreachable: validateEc2InstanceProps rejects the pair
245
261
  this.persistentDataVolume = new PersistentDataVolume(this, `${props.serviceName}PersistentDataVolume`, {
246
262
  autoScalingGroup: this.autoScalingGroup,
263
+ stableOwnerId: props.dataVolumeStableOwnerId,
247
264
  ...props.persistentDataVolume
248
265
  });
249
266
  }
@@ -282,6 +299,13 @@ function validateEc2InstanceProps(props) {
282
299
  throw new Error(`Ec2Instance.maxCapacity (${props.maxCapacity}) must be >= minCapacity (${props.minCapacity})`);
283
300
  }
284
301
  if (props.persistentDataVolume !== undefined) {
302
+ if (props.dataVolumeStableOwnerId === undefined ||
303
+ props.dataVolumeStableOwnerId === "") {
304
+ throw new Error("Ec2Instance.persistentDataVolume requires dataVolumeStableOwnerId — " +
305
+ "the volume's re-attach identity must be an explicit, stable " +
306
+ "caller decision (it becomes the fjall:OwnerLogicalId tag the " +
307
+ "lifecycle Lambdas filter on)");
308
+ }
285
309
  const azs = props.vpcSubnets?.availabilityZones;
286
310
  if (azs === undefined || azs.length !== 1) {
287
311
  throw new Error(`Ec2Instance.persistentDataVolume requires vpcSubnets.availabilityZones to be exactly 1 entry; got ${azs?.length ?? 0}`);
@@ -1,23 +1,58 @@
1
+ import { type CapacityIdentityPin } from "@fjall/util";
1
2
  import type { Ec2CapacityConfig, EcsCapacityProvider } from "./ecsTypes.js";
2
- /** The service + ec2Config that first claimed an EC2 capacity key. */
3
+ /** The service + ec2Config that first claimed a capacity slot. */
3
4
  export interface Ec2AsgOrigin {
4
5
  serviceName: string;
5
6
  ec2Config: Ec2CapacityConfig;
7
+ /** Identity anchor the slot's ASG was built with (pin-frozen or slot-derived). */
8
+ anchor: string;
6
9
  }
10
+ /** The slot this ec2Config binds to (`"primary"` when undeclared). */
11
+ export declare function resolveCapacitySlot(ec2Config: Ec2CapacityConfig): string;
12
+ /** Rejects at synth a slot name outside the kebab grammar. */
13
+ export declare function assertValidCapacitySlot(slot: string): void;
14
+ /**
15
+ * The identity anchor every CloudFormation identity surface derives from.
16
+ * Precedence: explicit `identityAnchor` config pin, then the deploy-injected
17
+ * context pin's `legacyAnchor`, then `toPascalCase(slot)`. The anchor grammar
18
+ * is validated whichever source wins — a bad anchor corrupts every logical
19
+ * ID and physical name downstream.
20
+ */
21
+ export declare function resolveCapacityAnchor(ec2Config: Ec2CapacityConfig, pin?: CapacityIdentityPin): string;
22
+ /**
23
+ * Rejects at synth two slots whose anchors collide (`web-1` and `web1` both
24
+ * normalise to `Web1`) — colliding anchors mean colliding logical IDs.
25
+ * `anchorsBySlot` carries every anchor already resolved for the cluster.
26
+ */
27
+ export declare function assertAnchorUnique(slot: string, anchor: string, anchorsBySlot: ReadonlyMap<string, string>): void;
7
28
  /**
8
29
  * Rejects at synth when a service reusing a shared ASG is incompatible with
9
- * it: either the origin's `persistentDataVolume` makes the ASG a singleton
10
- * (two tasks would race to attach the same EBS volume), or the reusing
11
- * service asks for ASG settings that differ from the ones the ASG was built
12
- * with those settings would be silently ignored (same silent-ignore class
13
- * as `validateEc2ServiceSizing`).
30
+ * it: the slot's identity is contested (`identityAnchor` disagreement), the
31
+ * origin's `persistentDataVolume` makes the slot a singleton (two tasks
32
+ * would race to attach the same EBS volume), or the reusing service asks for
33
+ * ASG settings that differ from the ones the ASG was built with — those
34
+ * settings would be silently ignored (same silent-ignore class as
35
+ * `validateEc2ServiceSizing`).
14
36
  */
15
- export declare function assertSharedAsgConfigMatches(serviceName: string, ec2Config: Ec2CapacityConfig, key: string, origin: Ec2AsgOrigin): void;
37
+ export declare function assertSharedAsgConfigMatches(serviceName: string, ec2Config: Ec2CapacityConfig, slot: string, origin: Ec2AsgOrigin): void;
16
38
  /**
17
- * Generates a unique key for EC2 config so services with matching
18
- * configurations share an ASG.
39
+ * Config fields that fed the pre-6.0 (config-keyed) identity derivation
40
+ * the "identity derives from …" causes named in manifest identity aliases.
41
+ */
42
+ export declare const LEGACY_KEY_FIELDS: readonly string[];
43
+ /**
44
+ * The pre-6.0 config-derived identity key. SURVIVES ONLY as the legacy
45
+ * checksum behind manifest identity-alias hints (`legacyKey`/`legacyAnchor`)
46
+ * — it feeds no construct IDs, no physical names and no template properties.
47
+ * ASG sharing keys on the capacity SLOT.
19
48
  */
20
49
  export declare function getEc2ConfigKey(ec2Config: Ec2CapacityConfig): string;
50
+ /**
51
+ * The anchor a pre-6.0 synth derived from a config key — the exact `safeKey`
52
+ * strip the factory used (`key.replace(/[^a-zA-Z0-9]/g, "")`). Feeds the
53
+ * manifest alias hints and pin capture; never a live identity surface.
54
+ */
55
+ export declare function legacyAnchorFromKey(key: string): string;
21
56
  /**
22
57
  * Minimal structural input for {@link validateSharedEc2CapacityConfig} —
23
58
  * exactly the fields the check reads. Both the resources-layer
@@ -31,13 +66,18 @@ export interface SharedEc2CapacityServiceInput {
31
66
  ec2Config?: Ec2CapacityConfig;
32
67
  }
33
68
  /**
34
- * Cross-service shared-ASG compatibility: groups EC2 services by their
35
- * capacity key and rejects a reuse that is incompatible with the ASG's origin
36
- * — config drift the shared ASG would silently discard, or a
37
- * `persistentDataVolume` singleton being shared. Pure; called from BOTH
38
- * validation layers resources `validateEcsClusterProps` and patterns
39
- * `validateEcsProps` (generator-standards § "Validate at the Lowest Layer the
40
- * Field Belongs To") with the same check at the construction site
69
+ * Cross-service capacity-slot compatibility: validates slot grammar, rejects
70
+ * anchor collisions across slots, and rejects a slot reuse that is
71
+ * incompatible with the slot's origin — config drift the shared ASG would
72
+ * silently discard, a contested identity, or a `persistentDataVolume`
73
+ * singleton being shared. Pure; called from BOTH validation layers —
74
+ * resources `validateEcsClusterProps` and patterns `validateEcsProps`
75
+ * (generator-standards § "Validate at the Lowest Layer the Field Belongs To")
76
+ * — with the same checks at the construction site
41
77
  * (`getOrCreateAsgCapacityProvider`) as defence-in-depth.
78
+ *
79
+ * Anchor collisions are checked on the PIN-BLIND anchor (`identityAnchor` or
80
+ * slot derivation) — this layer is pure and cannot read the deploy-injected
81
+ * context pins. The factory re-checks with pins applied.
42
82
  */
43
83
  export declare function validateSharedEc2CapacityConfig(services: readonly SharedEc2CapacityServiceInput[]): void;