@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.
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +1 -1
- package/dist/lib/resources/aws/compute/ecsCapacityConfig.js +9 -5
- package/dist/lib/resources/aws/compute/ecsConstants.d.ts +1 -0
- package/dist/lib/resources/aws/compute/ecsConstants.js +9 -1
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +2 -2
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +1 -1
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +9 -10
- package/dist/lib/resources/aws/compute/ecsValidation.js +23 -31
- package/package.json +3 -3
|
@@ -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.
|
|
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 {
|
|
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 ??
|
|
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 ??
|
|
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 ??
|
|
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 {
|
|
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 ??
|
|
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.
|
|
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
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* - demand >= nominal
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
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,
|
|
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
|
-
*
|
|
311
|
+
* Two verdicts:
|
|
312
312
|
*
|
|
313
|
-
* - demand >= nominal
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
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
|
|
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
|
|
362
|
-
|
|
361
|
+
const instanceTypeLabel = instanceTypeIsDefaulted
|
|
362
|
+
? `${instanceType} (the ECS EC2 default)`
|
|
363
|
+
: instanceType;
|
|
363
364
|
if (demand >= nominal) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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 ${
|
|
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": "
|
|
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": "^
|
|
84
|
-
"@fjall/util": "^
|
|
83
|
+
"@fjall/generator": "^12.0.0",
|
|
84
|
+
"@fjall/util": "^12.0.0",
|
|
85
85
|
"constructs": "^10.7.2"
|
|
86
86
|
},
|
|
87
87
|
"overrides": {
|