@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.
- package/dist/lib/patterns/aws/clickhouseDatabase.js +4 -0
- package/dist/lib/resources/aws/compute/ec2.d.ts +31 -14
- package/dist/lib/resources/aws/compute/ec2.js +34 -10
- package/dist/lib/resources/aws/compute/ecsCapacityConfig.d.ts +56 -16
- package/dist/lib/resources/aws/compute/ecsCapacityConfig.js +226 -64
- package/dist/lib/resources/aws/compute/ecsConstants.d.ts +15 -0
- package/dist/lib/resources/aws/compute/ecsConstants.js +15 -0
- package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +16 -6
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +68 -19
- package/dist/lib/resources/aws/compute/ecsTypes.d.ts +33 -4
- package/dist/lib/resources/aws/compute/persistentDataVolume.d.ts +15 -5
- package/dist/lib/resources/aws/compute/persistentDataVolume.js +9 -6
- package/dist/lib/utils/capacityIdentityContext.d.ts +20 -0
- package/dist/lib/utils/capacityIdentityContext.js +43 -0
- package/dist/lib/utils/manifestWriter.d.ts +19 -2
- package/dist/lib/utils/manifestWriter.js +35 -0
- package/package.json +4 -5
- package/dist/lib/config/aws/identityCenterMembership.d.ts +0 -11
- package/dist/lib/config/aws/identityCenterMembership.js +0 -61
- package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets +0 -30
- package/dist/lib/layers/layers/secrets-resolver/bin/resolve-secrets.mjs +0 -212
- package/dist/lib/patterns/aws/buildkite/alarms.d.ts +0 -25
- package/dist/lib/patterns/aws/buildkite/alarms.js +0 -78
- package/dist/lib/patterns/aws/domainDelegation.d.ts +0 -8
- package/dist/lib/patterns/aws/domainDelegation.js +0 -45
- package/dist/lib/patterns/aws/domainFactory.d.ts +0 -23
- package/dist/lib/patterns/aws/domainFactory.js +0 -61
- package/dist/lib/utils/addSuffixToEmail.d.ts +0 -1
- 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
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
* - `
|
|
17
|
-
* `
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
*
|
|
109
|
-
* `
|
|
110
|
-
*
|
|
111
|
-
*
|
|
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) →
|
|
26
|
-
* `
|
|
27
|
-
* `addAutoScalingGroup` can conditionally spread the
|
|
28
|
-
* `updatePolicy: undefined` directly emits an empty
|
|
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: "
|
|
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 ??
|
|
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
|
|
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:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* as
|
|
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,
|
|
37
|
+
export declare function assertSharedAsgConfigMatches(serviceName: string, ec2Config: Ec2CapacityConfig, slot: string, origin: Ec2AsgOrigin): void;
|
|
16
38
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
35
|
-
*
|
|
36
|
-
* — config drift the shared ASG would
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* `
|
|
40
|
-
*
|
|
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;
|