@fjall/components-infrastructure 11.0.0 → 12.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.
@@ -825,7 +825,7 @@ export interface EcsServiceConfig {
825
825
  * {
826
826
  * services: [
827
827
  * { name: "api", capacityProvider: "FARGATE" },
828
- * { name: "worker", capacityProvider: "EC2", ec2Config: { instanceType: "t4g.micro" } }
828
+ * { name: "worker", capacityProvider: "EC2", ec2Config: { instanceType: "t4g.small" } }
829
829
  * ]
830
830
  * }
831
831
  */
@@ -2,7 +2,7 @@ import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
2
2
  import { Token } from "aws-cdk-lib";
3
3
  import { CAPACITY_ANCHOR_MAX_LENGTH, CAPACITY_ANCHOR_PATTERN, CAPACITY_SLOT_MAX_LENGTH, CAPACITY_SLOT_PATTERN, DEFAULT_CAPACITY_SLOT } from "@fjall/util";
4
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";
5
+ import { DEFAULT_ECS_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
6
  /**
7
7
  * Every `Ec2CapacityConfig` field, classified. The `Record<keyof …, …>`
8
8
  * satisfies-shape is a compile-time exhaustiveness guard in compiled `src/`
@@ -40,8 +40,7 @@ const OPAQUE_ASG_FIELDS = fieldsWithRole("property:opaque");
40
40
  /** The `"ARM" | "STANDARD"` label the config key and drift compare share. */
41
41
  function resolvedAmiHardwareLabel(config) {
42
42
  return (config.amiHardwareType ??
43
- (inferAmiHardwareType(config.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE) ===
44
- AmiHardwareType.ARM
43
+ (inferAmiHardwareType(config.instanceType ?? DEFAULT_ECS_EC2_INSTANCE_TYPE) === AmiHardwareType.ARM
45
44
  ? "ARM"
46
45
  : "STANDARD"));
47
46
  }
@@ -54,7 +53,7 @@ function resolvedAmiHardwareLabel(config) {
54
53
  * default (`desiredCapacity`, `associatePublicIpAddress`) compare raw.
55
54
  */
56
55
  const SCALAR_ASG_FIELD_RESOLVERS = {
57
- instanceType: (c) => c.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE,
56
+ instanceType: (c) => c.instanceType ?? DEFAULT_ECS_EC2_INSTANCE_TYPE,
58
57
  amiHardwareType: resolvedAmiHardwareLabel,
59
58
  minCapacity: (c) => c.minCapacity ?? DEFAULT_EC2_MIN_CAPACITY,
60
59
  maxCapacity: (c) => c.maxCapacity ?? DEFAULT_EC2_MAX_CAPACITY,
@@ -246,6 +245,11 @@ export const LEGACY_KEY_FIELDS = [
246
245
  "persistentDataVolume",
247
246
  "availabilityZones"
248
247
  ];
248
+ // The ECS EC2 instance-type default as it stood when the pre-6.0 legacy key
249
+ // format was live. FROZEN: the legacy checksum must keep matching what
250
+ // deployed pre-6.0 stacks derived, so it must not follow
251
+ // DEFAULT_ECS_EC2_INSTANCE_TYPE.
252
+ const LEGACY_KEY_DEFAULT_INSTANCE_TYPE = "t4g.micro";
249
253
  /**
250
254
  * The pre-6.0 config-derived identity key. SURVIVES ONLY as the legacy
251
255
  * checksum behind manifest identity-alias hints (`legacyKey`/`legacyAnchor`)
@@ -253,7 +257,7 @@ export const LEGACY_KEY_FIELDS = [
253
257
  * ASG sharing keys on the capacity SLOT.
254
258
  */
255
259
  export function getEc2ConfigKey(ec2Config) {
256
- const instanceType = ec2Config.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE;
260
+ const instanceType = ec2Config.instanceType ?? LEGACY_KEY_DEFAULT_INSTANCE_TYPE;
257
261
  const amiHardwareType = resolvedAmiHardwareLabel(ec2Config);
258
262
  const warmPoolKey = ec2Config.warmPool
259
263
  ? `wp${ec2Config.warmPool.minSize ?? DEFAULT_WARM_POOL_MIN_SIZE}-${ec2Config.warmPool.reuseOnScaleIn ?? DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN}`
@@ -2,6 +2,7 @@ import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
2
2
  import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
3
3
  import { RetentionDays } from "aws-cdk-lib/aws-logs";
4
4
  export declare const DEFAULT_EC2_INSTANCE_TYPE = "t4g.micro";
5
+ export declare const DEFAULT_ECS_EC2_INSTANCE_TYPE = "t4g.small";
5
6
  export declare const DEFAULT_WARM_POOL_MIN_SIZE = 1;
6
7
  export declare const DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN = false;
7
8
  export declare const DEFAULT_LOG_RETENTION_DAYS = 14;
@@ -1,8 +1,16 @@
1
1
  import { Monitoring } from "aws-cdk-lib/aws-autoscaling";
2
2
  import { AmiHardwareType } from "aws-cdk-lib/aws-ecs";
3
3
  import { RetentionDays } from "aws-cdk-lib/aws-logs";
4
- // Canonical source: @fjall/generator schemas/constants.ts — keep in sync
4
+ // Canonical source: @fjall/generator schemas/constants.ts — keep in sync.
5
+ // Standalone EC2 instances (the compute:ec2 pattern) only; ECS EC2 capacity
6
+ // defaults to DEFAULT_ECS_EC2_INSTANCE_TYPE below.
5
7
  export const DEFAULT_EC2_INSTANCE_TYPE = "t4g.micro";
8
+ // Smallest type whose usable memory (nominal 2048 minus the agent/OS reserve)
9
+ // fits the default container hard limit (1024) plus the schema-gate sidecar
10
+ // (512). A t4g.micro registers under 1024 with ECS, so a micro default made
11
+ // every defaults-only EC2 service structurally unplaceable — the same class
12
+ // as the 2026-08-12 deploy-worker wedge, present from first deploy.
13
+ export const DEFAULT_ECS_EC2_INSTANCE_TYPE = "t4g.small";
6
14
  export const DEFAULT_WARM_POOL_MIN_SIZE = 1;
7
15
  // reuseOnScaleIn: true returns scaled-in instances to the warm pool Stopped,
8
16
  // which can strand ECS tasks DRAINING indefinitely (the 2026-06-04 outage).
@@ -11,7 +11,7 @@ import { Ec2Instance } from "./ec2.js";
11
11
  import { vpcHasNatGateways } from "../../../utils/vpcUtils.js";
12
12
  import { toPascalCase } from "../../../utils/capitaliseString.js";
13
13
  import { stackScopedExportName } from "../../../utils/exportNaming.js";
14
- import { DEFAULT_EC2_INSTANCE_TYPE, DEFAULT_EC2_MIN_CAPACITY, DEFAULT_EC2_MAX_CAPACITY, DEFAULT_EC2_INSTANCE_MONITORING, DEFAULT_WARM_POOL_MIN_SIZE, DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN, DEFAULT_HEALTH_CHECK_GRACE_SECONDS, DEFAULT_MIN_HEALTHY_PERCENT, DEFAULT_MAX_HEALTHY_PERCENT, DEFAULT_DESIRED_COUNT, inferAmiHardwareType } from "./ecsConstants.js";
14
+ import { DEFAULT_ECS_EC2_INSTANCE_TYPE, DEFAULT_EC2_MIN_CAPACITY, DEFAULT_EC2_MAX_CAPACITY, DEFAULT_EC2_INSTANCE_MONITORING, DEFAULT_WARM_POOL_MIN_SIZE, DEFAULT_WARM_POOL_REUSE_ON_SCALE_IN, DEFAULT_HEALTH_CHECK_GRACE_SECONDS, DEFAULT_MIN_HEALTHY_PERCENT, DEFAULT_MAX_HEALTHY_PERCENT, DEFAULT_DESIRED_COUNT, inferAmiHardwareType } from "./ecsConstants.js";
15
15
  import { assertAnchorUnique, assertSharedAsgConfigMatches, assertValidCapacitySlot, getEc2ConfigKey, legacyAnchorFromKey, LEGACY_KEY_FIELDS, resolveCapacityAnchor, resolveCapacitySlot } from "./ecsCapacityConfig.js";
16
16
  import { lookupCapacityIdentityPin } from "../../../utils/capacityIdentityContext.js";
17
17
  import { getCurrentCollector } from "../../../utils/manifestWriter.js";
@@ -114,7 +114,7 @@ export function getOrCreateAsgCapacityProvider(ctx, serviceProps, state) {
114
114
  legacyKeyFields: [...LEGACY_KEY_FIELDS]
115
115
  });
116
116
  }
117
- const instanceType = ec2Config.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE;
117
+ const instanceType = ec2Config.instanceType ?? DEFAULT_ECS_EC2_INSTANCE_TYPE;
118
118
  const amiHardwareType = ec2Config.amiHardwareType
119
119
  ? ec2Config.amiHardwareType === "STANDARD"
120
120
  ? AmiHardwareType.STANDARD
@@ -114,7 +114,7 @@ export interface Ec2CapacityConfig {
114
114
  * should rely on the slot derivation.
115
115
  */
116
116
  identityAnchor?: string;
117
- /** EC2 instance type. Default: "t4g.micro" */
117
+ /** EC2 instance type. Default: "t4g.small" */
118
118
  instanceType?: string;
119
119
  /** AMI hardware type. Default: "ARM" (Graviton - better cost/performance) */
120
120
  amiHardwareType?: "ARM" | "STANDARD";
@@ -121,16 +121,15 @@ interface Ec2MemoryFitInput {
121
121
  * of a t4g.medium's 3835 registered), which this guard converts into a synth
122
122
  * error.
123
123
  *
124
- * Three verdicts:
125
- *
126
- * - demand >= nominal AND the sizing is explicit (`ec2Config.instanceType` or
127
- * `ec2Config.memoryLimitMiB` set) THROW. Zero false positives by
128
- * construction: registered < nominal always, so nothing that deploys today
129
- * can trip it.
130
- * - demand >= nominal on an all-defaults `ec2Config` WARNING only. The
131
- * default instanceType/memory combo (t4g.micro + 1024) is itself
132
- * oversubscribed, so throwing would reject every bare `ec2Config: {}`
133
- * consumer; fixing the defaults is a separate decision.
124
+ * Two verdicts:
125
+ *
126
+ * - demand >= nominal THROW. Zero false positives by construction:
127
+ * registered < nominal always, so nothing that deploys today can trip it.
128
+ * Applies equally to the defaulted instance type
129
+ * ({@link DEFAULT_ECS_EC2_INSTANCE_TYPE}) — the defaults combo (t4g.small +
130
+ * 1024, or + the gate's 512) fits with headroom, so an oversubscribed bare
131
+ * `ec2Config: {}` means the caller added containers without sizing and the
132
+ * task is just as unplaceable as an explicitly-sized one.
134
133
  * - demand > nominal − {@link estimatedEc2MemoryReserveMiB} → WARNING. The
135
134
  * demand fits nominal but sits inside the estimated agent/OS reserve band,
136
135
  * where placement depends on the AMI's exact registered memory.
@@ -4,7 +4,7 @@ import { evaluateBakeGuard } from "@fjall/util/docker";
4
4
  import { SCHEMA_GATE_CONTAINER_NAME } from "@fjall/util/migration";
5
5
  import { toKebab, SSM_COMPONENT_PATTERN, SSM_COMPONENT_ERROR, SECRET_NAME_PATTERN, SECRET_NAME_ERROR } from "@fjall/util";
6
6
  import { ScalingType } from "./ecsTypes.js";
7
- import { DEFAULT_EC2_CONTAINER_MEMORY_MIB, DEFAULT_EC2_INSTANCE_TYPE, SCHEMA_GATE_CONTAINER_MEMORY_MIB, estimatedEc2MemoryReserveMiB, nominalEc2InstanceMemoryMiB, resolveEc2ContainerMemoryMiB } from "./ecsConstants.js";
7
+ import { DEFAULT_EC2_CONTAINER_MEMORY_MIB, DEFAULT_ECS_EC2_INSTANCE_TYPE, SCHEMA_GATE_CONTAINER_MEMORY_MIB, estimatedEc2MemoryReserveMiB, nominalEc2InstanceMemoryMiB, resolveEc2ContainerMemoryMiB } from "./ecsConstants.js";
8
8
  import { validateSharedEc2CapacityConfig } from "./ecsCapacityConfig.js";
9
9
  /**
10
10
  * Validates ECS cluster props before construction.
@@ -308,16 +308,15 @@ export function validateEc2ServiceSizing(service) {
308
308
  * of a t4g.medium's 3835 registered), which this guard converts into a synth
309
309
  * error.
310
310
  *
311
- * Three verdicts:
311
+ * Two verdicts:
312
312
  *
313
- * - demand >= nominal AND the sizing is explicit (`ec2Config.instanceType` or
314
- * `ec2Config.memoryLimitMiB` set) THROW. Zero false positives by
315
- * construction: registered < nominal always, so nothing that deploys today
316
- * can trip it.
317
- * - demand >= nominal on an all-defaults `ec2Config` WARNING only. The
318
- * default instanceType/memory combo (t4g.micro + 1024) is itself
319
- * oversubscribed, so throwing would reject every bare `ec2Config: {}`
320
- * consumer; fixing the defaults is a separate decision.
313
+ * - demand >= nominal THROW. Zero false positives by construction:
314
+ * registered < nominal always, so nothing that deploys today can trip it.
315
+ * Applies equally to the defaulted instance type
316
+ * ({@link DEFAULT_ECS_EC2_INSTANCE_TYPE}) — the defaults combo (t4g.small +
317
+ * 1024, or + the gate's 512) fits with headroom, so an oversubscribed bare
318
+ * `ec2Config: {}` means the caller added containers without sizing and the
319
+ * task is just as unplaceable as an explicitly-sized one.
321
320
  * - demand > nominal − {@link estimatedEc2MemoryReserveMiB} → WARNING. The
322
321
  * demand fits nominal but sits inside the estimated agent/OS reserve band,
323
322
  * where placement depends on the AMI's exact registered memory.
@@ -340,7 +339,8 @@ export function validateEc2ServiceSizing(service) {
340
339
  export function validateEc2TaskMemoryFit(service) {
341
340
  if (service.capacityProvider !== "EC2")
342
341
  return [];
343
- const instanceType = service.ec2Config?.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE;
342
+ const instanceTypeIsDefaulted = service.ec2Config?.instanceType === undefined;
343
+ const instanceType = service.ec2Config?.instanceType ?? DEFAULT_ECS_EC2_INSTANCE_TYPE;
344
344
  const nominal = nominalEc2InstanceMemoryMiB(instanceType);
345
345
  if (nominal === undefined)
346
346
  return [];
@@ -358,31 +358,23 @@ export function validateEc2TaskMemoryFit(service) {
358
358
  accounting.push(`${container.name ?? "(default)"}=${memory}`);
359
359
  }
360
360
  const reserve = estimatedEc2MemoryReserveMiB(nominal);
361
- const explicitlySized = service.ec2Config?.instanceType !== undefined ||
362
- service.ec2Config?.memoryLimitMiB !== undefined;
361
+ const instanceTypeLabel = instanceTypeIsDefaulted
362
+ ? `${instanceType} (the ECS EC2 default)`
363
+ : instanceType;
363
364
  if (demand >= nominal) {
364
- if (explicitlySized) {
365
- throw new Error(`Service '${service.name}': container hard memory limits total ${demand} MiB ` +
366
- `(${accounting.join(" + ")}), which cannot fit a ${instanceType} ECS ` +
367
- `registers less than the nominal ${nominal} MiB per instance (the ECS agent ` +
368
- `and OS reserve roughly ${reserve} MiB), so no task of this shape can ever ` +
369
- `be placed, on any instance count. The stack deploys cleanly while ` +
370
- `desiredCount is 0, then wedges at the first scale-up. Lower ` +
371
- `ec2Config.memoryLimitMiB (every app container takes that hard limit), ` +
372
- `choose a larger ec2Config.instanceType, or run fewer containers in the task.`);
373
- }
374
- return [
375
- `Service '${service.name}': container hard memory limits total ${demand} MiB ` +
376
- `(${accounting.join(" + ")}), which cannot fit the default ${instanceType} ` +
377
- `(nominal ${nominal} MiB, less after the ECS agent/OS reserve) — no task of ` +
378
- `this shape can be placed at runtime. Set ec2Config.instanceType to a ` +
379
- `larger type or lower ec2Config.memoryLimitMiB.`
380
- ];
365
+ throw new Error(`Service '${service.name}': container hard memory limits total ${demand} MiB ` +
366
+ `(${accounting.join(" + ")}), which cannot fit a ${instanceTypeLabel} ECS ` +
367
+ `registers less than the nominal ${nominal} MiB per instance (the ECS agent ` +
368
+ `and OS reserve roughly ${reserve} MiB), so no task of this shape can ever ` +
369
+ `be placed, on any instance count. The stack deploys cleanly while ` +
370
+ `desiredCount is 0, then wedges at the first scale-up. Lower ` +
371
+ `ec2Config.memoryLimitMiB (every app container takes that hard limit), ` +
372
+ `choose a larger ec2Config.instanceType, or run fewer containers in the task.`);
381
373
  }
382
374
  if (demand > nominal - reserve) {
383
375
  return [
384
376
  `Service '${service.name}': container hard memory limits total ${demand} MiB ` +
385
- `of a ${instanceType}'s nominal ${nominal} MiB — inside the ECS agent/OS ` +
377
+ `of a ${instanceTypeLabel}'s nominal ${nominal} MiB — inside the ECS agent/OS ` +
386
378
  `reserve band (estimated usable ~${nominal - reserve} MiB). Placement can ` +
387
379
  `fail depending on the AMI's exact registered memory. Leave ~${reserve} MiB ` +
388
380
  `headroom: lower ec2Config.memoryLimitMiB or choose a larger ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/components-infrastructure",
3
- "version": "11.0.0",
3
+ "version": "12.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": "^11.0.0",
84
- "@fjall/util": "^11.0.0",
83
+ "@fjall/generator": "^12.0.0",
84
+ "@fjall/util": "^12.0.0",
85
85
  "constructs": "^10.7.2"
86
86
  },
87
87
  "overrides": {