@fjall/components-infrastructure 5.0.0 → 7.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 (52) hide show
  1. package/dist/lib/config/aws/ebsDefaultEncryption.js +1 -1
  2. package/dist/lib/config/aws/inspectorEnablement.js +1 -1
  3. package/dist/lib/config/aws/oidcConnector.js +1 -1
  4. package/dist/lib/config/aws/s3BlockPublicAccess.js +1 -1
  5. package/dist/lib/config/aws/securityServicesAdmin.js +2 -2
  6. package/dist/lib/lambda-assets/cert-generator/asset/index.js +31 -31
  7. package/dist/lib/patterns/aws/clickhouseDatabase.js +4 -0
  8. package/dist/lib/patterns/aws/compute.js +1 -1
  9. package/dist/lib/patterns/aws/computeEcs.js +5 -0
  10. package/dist/lib/patterns/aws/computeLambda.d.ts +1 -1
  11. package/dist/lib/patterns/aws/devSubstrate.js +1 -1
  12. package/dist/lib/patterns/aws/payload.js +3 -3
  13. package/dist/lib/patterns/aws/staticSite.js +1 -1
  14. package/dist/lib/resources/aws/compute/ec2.d.ts +31 -14
  15. package/dist/lib/resources/aws/compute/ec2.js +34 -10
  16. package/dist/lib/resources/aws/compute/ec2GracefulTerminationHandler.js +1 -1
  17. package/dist/lib/resources/aws/compute/ecsCapacityConfig.d.ts +83 -0
  18. package/dist/lib/resources/aws/compute/ecsCapacityConfig.js +323 -0
  19. package/dist/lib/resources/aws/compute/ecsConstants.d.ts +19 -0
  20. package/dist/lib/resources/aws/compute/ecsConstants.js +24 -0
  21. package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.js +1 -1
  22. package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +20 -16
  23. package/dist/lib/resources/aws/compute/ecsServiceFactory.js +73 -118
  24. package/dist/lib/resources/aws/compute/ecsTypes.d.ts +33 -4
  25. package/dist/lib/resources/aws/compute/ecsValidation.js +6 -0
  26. package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +15 -5
  27. package/dist/lib/resources/aws/compute/persistentDataVolume.js +10 -7
  28. package/dist/lib/resources/aws/database/rdsInstance.js +1 -1
  29. package/dist/lib/resources/aws/iam/identityCenter/user.js +1 -1
  30. package/dist/lib/resources/aws/networking/crossAccountReturnRoutes.js +1 -1
  31. package/dist/lib/resources/aws/networking/ipamPool.js +1 -1
  32. package/dist/lib/resources/aws/organisation/costAllocationTagActivator.js +1 -1
  33. package/dist/lib/resources/aws/utilities/tlsCertGenerator.js +1 -1
  34. package/dist/lib/utils/capacityIdentityContext.d.ts +20 -0
  35. package/dist/lib/utils/capacityIdentityContext.js +43 -0
  36. package/dist/lib/utils/engineCompat.d.ts +12 -5
  37. package/dist/lib/utils/engineCompat.js +41 -22
  38. package/dist/lib/utils/manifestWriter.d.ts +19 -2
  39. package/dist/lib/utils/manifestWriter.js +35 -0
  40. package/package.json +10 -6
  41. package/dist/lib/config/aws/identityCenterMembership.d.ts +0 -11
  42. package/dist/lib/config/aws/identityCenterMembership.js +0 -61
  43. package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets +0 -30
  44. package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets.mjs +0 -212
  45. package/dist/lib/patterns/aws/buildkite/alarms.d.ts +0 -25
  46. package/dist/lib/patterns/aws/buildkite/alarms.js +0 -78
  47. package/dist/lib/patterns/aws/domainDelegation.d.ts +0 -8
  48. package/dist/lib/patterns/aws/domainDelegation.js +0 -45
  49. package/dist/lib/patterns/aws/domainFactory.d.ts +0 -23
  50. package/dist/lib/patterns/aws/domainFactory.js +0 -61
  51. package/dist/lib/utils/addSuffixToEmail.d.ts +0 -1
  52. package/dist/lib/utils/addSuffixToEmail.js +0 -3
@@ -0,0 +1,323 @@
1
+ import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
2
+ import { Token } from "aws-cdk-lib";
3
+ import { CAPACITY_ANCHOR_MAX_LENGTH, CAPACITY_ANCHOR_PATTERN, CAPACITY_SLOT_MAX_LENGTH, CAPACITY_SLOT_PATTERN, DEFAULT_CAPACITY_SLOT } from "@fjall/util";
4
+ import { toPascalCase } from "../../../utils/capitaliseString.js";
5
+ import { DEFAULT_EC2_INSTANCE_TYPE, DEFAULT_EC2_MIN_CAPACITY, DEFAULT_EC2_MAX_CAPACITY, DEFAULT_EC2_INSTANCE_MONITORING, DEFAULT_ROLLING_UPDATE_PAUSE_SECONDS, DEFAULT_WARM_POOL_MIN_SIZE, DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN, INSTANCE_REFRESH_DEFAULT_MAX_HEALTHY, INSTANCE_REFRESH_DEFAULT_MIN_HEALTHY, INSTANCE_REFRESH_PDV_MAX_HEALTHY, INSTANCE_REFRESH_PDV_MIN_HEALTHY, inferAmiHardwareType } from "./ecsConstants.js";
6
+ /**
7
+ * Every `Ec2CapacityConfig` field, classified. The `Record<keyof …, …>`
8
+ * satisfies-shape is a compile-time exhaustiveness guard in compiled `src/`
9
+ * (typescript-standards § "Compile-time guards must live in compiled src/"):
10
+ * adding a field to `Ec2CapacityConfig` without classifying it here fails
11
+ * `tsc`, so a new ASG-shaping field cannot silently join the "not compared,
12
+ * silently discarded on a shared ASG" bucket.
13
+ */
14
+ const EC2_CONFIG_FIELD_ROLES = {
15
+ slot: "identity",
16
+ identityAnchor: "identity",
17
+ instanceType: "property:scalar",
18
+ amiHardwareType: "property:scalar",
19
+ minCapacity: "property:scalar",
20
+ maxCapacity: "property:scalar",
21
+ desiredCapacity: "property:scalar",
22
+ instanceMonitoring: "property:scalar",
23
+ associatePublicIpAddress: "property:scalar",
24
+ warmPool: "property:deep",
25
+ persistentDataVolume: "property:deep",
26
+ availabilityZones: "property:deep",
27
+ updatePolicy: "property:deep",
28
+ machineImage: "property:opaque",
29
+ userData: "property:opaque",
30
+ blockDevices: "property:opaque",
31
+ tags: "property:tags",
32
+ memoryLimitMiB: "validation"
33
+ };
34
+ function fieldsWithRole(role) {
35
+ return Object.keys(EC2_CONFIG_FIELD_ROLES).filter((field) => EC2_CONFIG_FIELD_ROLES[field] === role);
36
+ }
37
+ const SCALAR_ASG_FIELDS = fieldsWithRole("property:scalar");
38
+ const DEEP_ASG_FIELDS = fieldsWithRole("property:deep");
39
+ const OPAQUE_ASG_FIELDS = fieldsWithRole("property:opaque");
40
+ /** The `"ARM" | "STANDARD"` label the config key and drift compare share. */
41
+ function resolvedAmiHardwareLabel(config) {
42
+ return (config.amiHardwareType ??
43
+ (inferAmiHardwareType(config.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE) ===
44
+ AmiHardwareType.ARM
45
+ ? "ARM"
46
+ : "STANDARD"));
47
+ }
48
+ /**
49
+ * Per-field resolution before a scalar field is compared — the SAME values
50
+ * the ASG construction applies (`getOrCreateAsgCapacityProvider`), imported
51
+ * from their `ecsConstants.ts` single source, so an explicit value equal to
52
+ * the default compares equal to an omitted one instead of tripping the drift
53
+ * throw on a pair that synthesises identically. Fields with no construction
54
+ * default (`desiredCapacity`, `associatePublicIpAddress`) compare raw.
55
+ */
56
+ const SCALAR_ASG_FIELD_RESOLVERS = {
57
+ instanceType: (c) => c.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE,
58
+ amiHardwareType: resolvedAmiHardwareLabel,
59
+ minCapacity: (c) => c.minCapacity ?? DEFAULT_EC2_MIN_CAPACITY,
60
+ maxCapacity: (c) => c.maxCapacity ?? DEFAULT_EC2_MAX_CAPACITY,
61
+ instanceMonitoring: (c) => c.instanceMonitoring ?? DEFAULT_EC2_INSTANCE_MONITORING
62
+ };
63
+ function resolvedScalar(config, field) {
64
+ const resolver = SCALAR_ASG_FIELD_RESOLVERS[field];
65
+ return resolver !== undefined ? resolver(config) : config[field];
66
+ }
67
+ /**
68
+ * JSON-stable projections for the plain-data config objects, defaults
69
+ * resolved. `persistentDataVolume.alarmTopic` is a CDK `ITopic` — projected
70
+ * to defined-vs-undefined like the opaque fields. `updatePolicy.pauseTime`
71
+ * is a literal `Duration` — projected to seconds.
72
+ */
73
+ const DEEP_ASG_FIELD_PROJECTIONS = {
74
+ warmPool: (c) => c.warmPool === undefined
75
+ ? undefined
76
+ : {
77
+ minSize: c.warmPool.minSize ?? DEFAULT_WARM_POOL_MIN_SIZE,
78
+ reuseOnScaleIn: c.warmPool.reuseOnScaleIn ?? DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN
79
+ },
80
+ persistentDataVolume: (c) => c.persistentDataVolume === undefined
81
+ ? undefined
82
+ : {
83
+ sizeGb: c.persistentDataVolume.sizeGb,
84
+ deviceName: c.persistentDataVolume.deviceName,
85
+ availabilityZone: stableAzSegment(c.persistentDataVolume.availabilityZone),
86
+ volumeType: c.persistentDataVolume.volumeType,
87
+ iops: c.persistentDataVolume.iops,
88
+ throughputMbps: c.persistentDataVolume.throughputMbps,
89
+ alarmTopicDefined: c.persistentDataVolume.alarmTopic !== undefined
90
+ },
91
+ availabilityZones: (c) => c.availabilityZones === undefined
92
+ ? undefined
93
+ : [...c.availabilityZones].map(stableAzSegment).sort(),
94
+ updatePolicy: (c) => {
95
+ // Mirrors resolveUpdatePolicy (ec2.ts): explicit-equal-to-default ≠ drift.
96
+ const policy = c.updatePolicy ?? { type: "instanceRefresh" };
97
+ const hasPdv = c.persistentDataVolume !== undefined;
98
+ switch (policy.type) {
99
+ case "rollingUpdate":
100
+ return {
101
+ type: policy.type,
102
+ pauseTimeSeconds: policy.pauseTime?.toSeconds() ??
103
+ DEFAULT_ROLLING_UPDATE_PAUSE_SECONDS
104
+ };
105
+ case "instanceRefresh":
106
+ return {
107
+ type: policy.type,
108
+ minHealthyPercentage: policy.minHealthyPercentage ??
109
+ (hasPdv
110
+ ? INSTANCE_REFRESH_PDV_MIN_HEALTHY
111
+ : INSTANCE_REFRESH_DEFAULT_MIN_HEALTHY),
112
+ maxHealthyPercentage: policy.maxHealthyPercentage ??
113
+ (hasPdv
114
+ ? INSTANCE_REFRESH_PDV_MAX_HEALTHY
115
+ : INSTANCE_REFRESH_DEFAULT_MAX_HEALTHY)
116
+ };
117
+ default:
118
+ return { type: policy.type };
119
+ }
120
+ }
121
+ };
122
+ function projectedDeep(config, field) {
123
+ const projection = DEEP_ASG_FIELD_PROJECTIONS[field];
124
+ return JSON.stringify(projection !== undefined ? projection(config) : config[field]);
125
+ }
126
+ /** Deep tag-record equality, treating `{}` and absent as the same ASG shape. */
127
+ function tagRecordsEqual(a, b) {
128
+ const aKeys = a === undefined ? [] : Object.keys(a);
129
+ const bKeys = b === undefined ? [] : Object.keys(b);
130
+ if (aKeys.length !== bKeys.length)
131
+ return false;
132
+ if (a === undefined || b === undefined)
133
+ return true;
134
+ return aKeys.every((key) => Object.is(a[key], b[key]));
135
+ }
136
+ function findDifferingAsgFields(ec2Config, origin) {
137
+ const differing = SCALAR_ASG_FIELDS.filter((field) => !Object.is(resolvedScalar(ec2Config, field), resolvedScalar(origin, field)));
138
+ for (const field of DEEP_ASG_FIELDS) {
139
+ if (projectedDeep(ec2Config, field) !== projectedDeep(origin, field)) {
140
+ differing.push(field);
141
+ }
142
+ }
143
+ if (!tagRecordsEqual(ec2Config.tags, origin.tags))
144
+ differing.push("tags");
145
+ for (const field of OPAQUE_ASG_FIELDS) {
146
+ if ((ec2Config[field] === undefined) !== (origin[field] === undefined)) {
147
+ differing.push(field);
148
+ }
149
+ }
150
+ return differing;
151
+ }
152
+ /** The slot this ec2Config binds to (`"primary"` when undeclared). */
153
+ export function resolveCapacitySlot(ec2Config) {
154
+ return ec2Config.slot ?? DEFAULT_CAPACITY_SLOT;
155
+ }
156
+ /** Rejects at synth a slot name outside the kebab grammar. */
157
+ export function assertValidCapacitySlot(slot) {
158
+ if (slot.length > CAPACITY_SLOT_MAX_LENGTH ||
159
+ !CAPACITY_SLOT_PATTERN.test(slot)) {
160
+ throw new Error(`EC2 capacity slot '${slot}' is invalid — slot names are lowercase kebab ` +
161
+ `(letter first, e.g. 'primary', 'clickhouse-data'), max ` +
162
+ `${CAPACITY_SLOT_MAX_LENGTH} characters.`);
163
+ }
164
+ }
165
+ /**
166
+ * The identity anchor every CloudFormation identity surface derives from.
167
+ * Precedence: explicit `identityAnchor` config pin, then the deploy-injected
168
+ * context pin's `legacyAnchor`, then `toPascalCase(slot)`. The anchor grammar
169
+ * is validated whichever source wins — a bad anchor corrupts every logical
170
+ * ID and physical name downstream.
171
+ */
172
+ export function resolveCapacityAnchor(ec2Config, pin) {
173
+ const anchor = ec2Config.identityAnchor ??
174
+ pin?.legacyAnchor ??
175
+ toPascalCase(resolveCapacitySlot(ec2Config));
176
+ if (anchor.length > CAPACITY_ANCHOR_MAX_LENGTH ||
177
+ !CAPACITY_ANCHOR_PATTERN.test(anchor)) {
178
+ throw new Error(`EC2 capacity identity anchor '${anchor}' is invalid — anchors are ` +
179
+ `alphanumeric, start with a letter, max ${CAPACITY_ANCHOR_MAX_LENGTH} ` +
180
+ `characters.`);
181
+ }
182
+ return anchor;
183
+ }
184
+ /**
185
+ * Rejects at synth two slots whose anchors collide (`web-1` and `web1` both
186
+ * normalise to `Web1`) — colliding anchors mean colliding logical IDs.
187
+ * `anchorsBySlot` carries every anchor already resolved for the cluster.
188
+ */
189
+ export function assertAnchorUnique(slot, anchor, anchorsBySlot) {
190
+ for (const [otherSlot, otherAnchor] of anchorsBySlot) {
191
+ if (otherSlot !== slot && otherAnchor === anchor) {
192
+ throw new Error(`EC2 capacity slots '${otherSlot}' and '${slot}' both resolve to ` +
193
+ `identity anchor '${anchor}' — their CloudFormation logical IDs ` +
194
+ `would collide. Rename one slot (or adjust its identityAnchor).`);
195
+ }
196
+ }
197
+ }
198
+ /**
199
+ * Rejects at synth when a service reusing a shared ASG is incompatible with
200
+ * it: the slot's identity is contested (`identityAnchor` disagreement), the
201
+ * origin's `persistentDataVolume` makes the slot a singleton (two tasks
202
+ * would race to attach the same EBS volume), or the reusing service asks for
203
+ * ASG settings that differ from the ones the ASG was built with — those
204
+ * settings would be silently ignored (same silent-ignore class as
205
+ * `validateEc2ServiceSizing`).
206
+ */
207
+ export function assertSharedAsgConfigMatches(serviceName, ec2Config, slot, origin) {
208
+ if (ec2Config.identityAnchor !== origin.ec2Config.identityAnchor) {
209
+ const describe = (anchor) => anchor === undefined ? "unset" : `'${anchor}'`;
210
+ throw new Error(`Service '${serviceName}': ec2Config.identityAnchor ` +
211
+ `(${describe(ec2Config.identityAnchor)}) disagrees with service ` +
212
+ `'${origin.serviceName}' (${describe(origin.ec2Config.identityAnchor)}) ` +
213
+ `for capacity slot '${slot}' — a slot has exactly one identity. ` +
214
+ `Align the pin, or declare a second slot.`);
215
+ }
216
+ // Cross-service only: the construction path legitimately re-resolves the
217
+ // SAME service's provider (e.g. a second task definition riding its ASG),
218
+ // and that re-entry must not trip the singleton throw.
219
+ if (origin.ec2Config.persistentDataVolume !== undefined &&
220
+ serviceName !== origin.serviceName) {
221
+ throw new Error(`Service '${serviceName}': it declares the same capacity slot ` +
222
+ `('${slot}') as service '${origin.serviceName}', whose ` +
223
+ `persistentDataVolume pairs that auto scaling group with a single ` +
224
+ `EBS data volume. A persistent-data-volume slot is a singleton — ` +
225
+ `two services sharing its ASG would race to attach the same volume. ` +
226
+ `Declare a separate slot for this service.`);
227
+ }
228
+ const differing = findDifferingAsgFields(ec2Config, origin.ec2Config);
229
+ if (differing.length === 0)
230
+ return;
231
+ throw new Error(`Service '${serviceName}': ec2Config ${differing.map((f) => `'${f}'`).join(", ")} ` +
232
+ `${differing.length === 1 ? "is" : "are"} ignored — it shares capacity ` +
233
+ `slot '${slot}' with service '${origin.serviceName}', and that slot's ` +
234
+ `ASG is built from the first service's ec2Config. Align the two ` +
235
+ `services' values for ${differing.map((f) => `'${f}'`).join(", ")}, or ` +
236
+ `declare a second slot to give this service its own ASG.`);
237
+ }
238
+ /**
239
+ * Config fields that fed the pre-6.0 (config-keyed) identity derivation —
240
+ * the "identity derives from …" causes named in manifest identity aliases.
241
+ */
242
+ export const LEGACY_KEY_FIELDS = [
243
+ "instanceType",
244
+ "amiHardwareType",
245
+ "warmPool",
246
+ "persistentDataVolume",
247
+ "availabilityZones"
248
+ ];
249
+ /**
250
+ * The pre-6.0 config-derived identity key. SURVIVES ONLY as the legacy
251
+ * checksum behind manifest identity-alias hints (`legacyKey`/`legacyAnchor`)
252
+ * — it feeds no construct IDs, no physical names and no template properties.
253
+ * ASG sharing keys on the capacity SLOT.
254
+ */
255
+ export function getEc2ConfigKey(ec2Config) {
256
+ const instanceType = ec2Config.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE;
257
+ const amiHardwareType = resolvedAmiHardwareLabel(ec2Config);
258
+ const warmPoolKey = ec2Config.warmPool
259
+ ? `wp${ec2Config.warmPool.minSize ?? DEFAULT_WARM_POOL_MIN_SIZE}-${ec2Config.warmPool.reuseOnScaleIn ?? DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN}`
260
+ : "nowp";
261
+ const baseKey = `${instanceType}-${amiHardwareType}-${warmPoolKey}`;
262
+ const pdv = ec2Config.persistentDataVolume;
263
+ const extras = [];
264
+ if (pdv !== undefined) {
265
+ extras.push(`pdv-${pdv.deviceName}-${pdv.sizeGb}-${stableAzSegment(pdv.availabilityZone)}`);
266
+ }
267
+ // Serialise actual AZ names — `az${length}` would let services pinned to different AZs collide on `az1`.
268
+ // CDK Tokens (env-agnostic stacks) substitute a stable sentinel to keep the checksum deterministic.
269
+ if (ec2Config.availabilityZones !== undefined) {
270
+ const azKey = [...ec2Config.availabilityZones]
271
+ .map(stableAzSegment)
272
+ .sort()
273
+ .join(",");
274
+ extras.push(`az-${azKey}`);
275
+ }
276
+ return extras.length === 0 ? baseKey : `${baseKey}-${extras.join("-")}`;
277
+ }
278
+ /**
279
+ * The anchor a pre-6.0 synth derived from a config key — the exact `safeKey`
280
+ * strip the factory used (`key.replace(/[^a-zA-Z0-9]/g, "")`). Feeds the
281
+ * manifest alias hints and pin capture; never a live identity surface.
282
+ */
283
+ export function legacyAnchorFromKey(key) {
284
+ return key.replace(/[^a-zA-Z0-9]/g, "");
285
+ }
286
+ function stableAzSegment(az) {
287
+ return Token.isUnresolved(az) ? "synthAz" : az;
288
+ }
289
+ /**
290
+ * Cross-service capacity-slot compatibility: validates slot grammar, rejects
291
+ * anchor collisions across slots, and rejects a slot reuse that is
292
+ * incompatible with the slot's origin — config drift the shared ASG would
293
+ * silently discard, a contested identity, or a `persistentDataVolume`
294
+ * singleton being shared. Pure; called from BOTH validation layers —
295
+ * resources `validateEcsClusterProps` and patterns `validateEcsProps`
296
+ * (generator-standards § "Validate at the Lowest Layer the Field Belongs To")
297
+ * — with the same checks at the construction site
298
+ * (`getOrCreateAsgCapacityProvider`) as defence-in-depth.
299
+ *
300
+ * Anchor collisions are checked on the PIN-BLIND anchor (`identityAnchor` or
301
+ * slot derivation) — this layer is pure and cannot read the deploy-injected
302
+ * context pins. The factory re-checks with pins applied.
303
+ */
304
+ export function validateSharedEc2CapacityConfig(services) {
305
+ const origins = new Map();
306
+ const anchorsBySlot = new Map();
307
+ for (const service of services) {
308
+ if (service.capacityProvider !== "EC2")
309
+ continue;
310
+ const ec2Config = service.ec2Config ?? {};
311
+ const slot = resolveCapacitySlot(ec2Config);
312
+ assertValidCapacitySlot(slot);
313
+ const origin = origins.get(slot);
314
+ if (origin === undefined) {
315
+ const anchor = resolveCapacityAnchor(ec2Config);
316
+ assertAnchorUnique(slot, anchor, anchorsBySlot);
317
+ anchorsBySlot.set(slot, anchor);
318
+ origins.set(slot, { serviceName: service.name, ec2Config, anchor });
319
+ continue;
320
+ }
321
+ assertSharedAsgConfigMatches(service.name, ec2Config, slot, origin);
322
+ }
323
+ }
@@ -1,3 +1,4 @@
1
+ import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
1
2
  import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
2
3
  import { RetentionDays } from "aws-cdk-lib/aws-logs";
3
4
  export declare const DEFAULT_EC2_INSTANCE_TYPE = "t4g.micro";
@@ -8,6 +9,24 @@ export declare const DEFAULT_LOG_RETENTION = RetentionDays.TWO_WEEKS;
8
9
  export declare const DEFAULT_FARGATE_CPU = 256;
9
10
  export declare const DEFAULT_FARGATE_MEMORY_MIB = 512;
10
11
  export declare const DEFAULT_EC2_CONTAINER_MEMORY_MIB = 1024;
12
+ export declare const DEFAULT_EC2_MIN_CAPACITY = 2;
13
+ export declare const DEFAULT_EC2_MAX_CAPACITY = 3;
14
+ export declare const DEFAULT_EC2_INSTANCE_MONITORING = Monitoring.BASIC;
15
+ /**
16
+ * `updatePolicy` construction defaults, read at the same two must-agree sites
17
+ * as the capacity defaults above: `resolveUpdatePolicy` (ec2.ts) applies them
18
+ * at construction; the shared-ASG drift compare (`ecsCapacityConfig.ts`
19
+ * DEEP_ASG_FIELD_PROJECTIONS) resolves them so an explicit value equal to the
20
+ * default never reads as drift. A `persistentDataVolume` ASG cannot surge
21
+ * (its single-attach EBS volume can only follow one instance), so it
22
+ * terminates-then-launches (0/100); every other ASG surges-then-shrinks
23
+ * (100/200) so capacity never dips.
24
+ */
25
+ export declare const INSTANCE_REFRESH_PDV_MIN_HEALTHY = 0;
26
+ export declare const INSTANCE_REFRESH_PDV_MAX_HEALTHY = 100;
27
+ export declare const INSTANCE_REFRESH_DEFAULT_MIN_HEALTHY = 100;
28
+ export declare const INSTANCE_REFRESH_DEFAULT_MAX_HEALTHY = 200;
29
+ export declare const DEFAULT_ROLLING_UPDATE_PAUSE_SECONDS = 300;
11
30
  /**
12
31
  * The container hard memory limit for an EC2-capacity ECS service.
13
32
  *
@@ -1,3 +1,4 @@
1
+ import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
1
2
  import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
2
3
  import { RetentionDays } from "aws-cdk-lib/aws-logs";
3
4
  // Canonical source: @fjall/generator schemas/constants.ts — keep in sync
@@ -16,6 +17,29 @@ export const DEFAULT_LOG_RETENTION = RetentionDays.TWO_WEEKS;
16
17
  export const DEFAULT_FARGATE_CPU = 256;
17
18
  export const DEFAULT_FARGATE_MEMORY_MIB = 512;
18
19
  export const DEFAULT_EC2_CONTAINER_MEMORY_MIB = 1024;
20
+ // Read at two sites that must agree (code-quality § coupled values): the ASG
21
+ // construction (`getOrCreateAsgCapacityProvider`) and the shared-ASG drift
22
+ // compare (`ecsCapacityConfig.ts` SCALAR_ASG_FIELD_DEFAULTS) — the compare
23
+ // resolves the same defaults the construction applies, or an explicit value
24
+ // equal to the default would read as drift.
25
+ export const DEFAULT_EC2_MIN_CAPACITY = 2;
26
+ export const DEFAULT_EC2_MAX_CAPACITY = 3;
27
+ export const DEFAULT_EC2_INSTANCE_MONITORING = Monitoring.BASIC;
28
+ /**
29
+ * `updatePolicy` construction defaults, read at the same two must-agree sites
30
+ * as the capacity defaults above: `resolveUpdatePolicy` (ec2.ts) applies them
31
+ * at construction; the shared-ASG drift compare (`ecsCapacityConfig.ts`
32
+ * DEEP_ASG_FIELD_PROJECTIONS) resolves them so an explicit value equal to the
33
+ * default never reads as drift. A `persistentDataVolume` ASG cannot surge
34
+ * (its single-attach EBS volume can only follow one instance), so it
35
+ * terminates-then-launches (0/100); every other ASG surges-then-shrinks
36
+ * (100/200) so capacity never dips.
37
+ */
38
+ export const INSTANCE_REFRESH_PDV_MIN_HEALTHY = 0;
39
+ export const INSTANCE_REFRESH_PDV_MAX_HEALTHY = 100;
40
+ export const INSTANCE_REFRESH_DEFAULT_MIN_HEALTHY = 100;
41
+ export const INSTANCE_REFRESH_DEFAULT_MAX_HEALTHY = 200;
42
+ export const DEFAULT_ROLLING_UPDATE_PAUSE_SECONDS = 300;
19
43
  /**
20
44
  * The container hard memory limit for an EC2-capacity ECS service.
21
45
  *
@@ -49,7 +49,7 @@ export class EcsLifecycleHookMigration extends Construct {
49
49
  : "DISABLED"
50
50
  };
51
51
  this.lambda = new LambdaFunction(this, `${id}Fn`, {
52
- runtime: Runtime.NODEJS_22_X,
52
+ runtime: Runtime.NODEJS_24_X,
53
53
  handler: "index.handler",
54
54
  code: Code.fromInline(source),
55
55
  lambdaDescription: `${id} ECS deployment lifecycle hook for migration`,
@@ -3,7 +3,8 @@ import type { FargateTaskDefinition, Ec2TaskDefinition } from "aws-cdk-lib/aws-e
3
3
  import { type ISecurityGroup } from "aws-cdk-lib/aws-ec2";
4
4
  import { TargetTrackingScalingPolicy } from "aws-cdk-lib/aws-applicationautoscaling";
5
5
  import { type AutoScalingGroup } from "aws-cdk-lib/aws-autoscaling";
6
- import { type EcsServiceProps, type Ec2CapacityConfig, type QueueScalingResources } from "./ecsTypes.js";
6
+ import { type Ec2AsgOrigin } from "./ecsCapacityConfig.js";
7
+ import { type EcsServiceProps, type QueueScalingResources } from "./ecsTypes.js";
7
8
  import type { EcsConstructContext } from "./ecsContext.js";
8
9
  /**
9
10
  * Resolves the user's `circuitBreaker` config to the CDK
@@ -14,31 +15,34 @@ import type { EcsConstructContext } from "./ecsContext.js";
14
15
  export declare function resolveCircuitBreaker(config: false | {
15
16
  rollback?: boolean;
16
17
  } | undefined): DeploymentCircuitBreaker | undefined;
17
- /** Mutable state for ASG capacity provider deduplication. */
18
+ /** Mutable state for ASG capacity provider deduplication, keyed by SLOT. */
18
19
  export interface AsgCapacityState {
19
20
  providers: Map<string, AsgCapacityProvider>;
20
21
  /**
21
- * The service + ec2Config that first created each keyed ASG, so a later
22
- * service sharing the key can be checked against it
23
- * ({@link assertSharedAsgConfigMatches}).
22
+ * The service + ec2Config + anchor that first claimed each capacity slot,
23
+ * so a later service declaring the slot can be checked against it
24
+ * ({@link assertSharedAsgConfigMatches}) and anchor collisions across
25
+ * slots rejected ({@link assertAnchorUnique}).
24
26
  */
25
- asgOrigins: Map<string, {
26
- serviceName: string;
27
- ec2Config: Ec2CapacityConfig;
28
- }>;
27
+ asgOrigins: Map<string, Ec2AsgOrigin>;
29
28
  autoScalingGroup?: AutoScalingGroup;
30
29
  asgSecurityGroup?: ISecurityGroup;
31
30
  }
32
- /**
33
- * Generates a unique key for EC2 config so services with matching
34
- * configurations share an ASG.
35
- */
36
- export declare function getEc2ConfigKey(ec2Config: Ec2CapacityConfig): string;
31
+ export { getEc2ConfigKey, validateSharedEc2CapacityConfig, type Ec2AsgOrigin, type SharedEc2CapacityServiceInput } from "./ecsCapacityConfig.js";
37
32
  /**
38
33
  * Gets or creates an ASG capacity provider for an EC2-backed service.
39
- * Services with matching EC2 configs share the same ASG.
34
+ * Services declaring the same capacity SLOT share the same ASG.
35
+ *
36
+ * Every CloudFormation identity surface (the four construct-ID prefixes,
37
+ * the physical service name and everything `Ec2Instance` derives from it)
38
+ * comes from the slot's identity ANCHOR — `identityAnchor` pin, else the
39
+ * deploy-injected `fjall:capacityIdentity` context pin, else
40
+ * `toPascalCase(slot)` — never from the hardware config, so a config change
41
+ * updates the deployed ASG in place instead of renaming (replacing) it.
40
42
  *
41
- * Mutates `state` to track the provider and first ASG/security group.
43
+ * Mutates `state` to track the provider and first ASG/security group, and
44
+ * records the slot's identity alias on the manifest collector (rename
45
+ * detection and the pin writer consume it).
42
46
  */
43
47
  export declare function getOrCreateAsgCapacityProvider(ctx: EcsConstructContext, serviceProps: EcsServiceProps, state: AsgCapacityState): AsgCapacityProvider;
44
48
  /**