@fjall/components-infrastructure 4.4.0 → 5.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/config/aws/oidcConnector.d.ts +6 -6
- package/dist/lib/patterns/aws/account.d.ts +11 -10
- package/dist/lib/patterns/aws/account.js +11 -10
- package/dist/lib/patterns/aws/clickhouseDatabase.d.ts +16 -3
- package/dist/lib/patterns/aws/clickhouseDatabase.js +23 -2
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +12 -2
- package/dist/lib/patterns/aws/database.js +5 -5
- package/dist/lib/resources/aws/compute/ecs.js +1 -0
- package/dist/lib/resources/aws/compute/ecsConstants.d.ts +14 -0
- package/dist/lib/resources/aws/compute/ecsConstants.js +23 -0
- package/dist/lib/resources/aws/compute/ecsServiceFactory.d.ts +9 -0
- package/dist/lib/resources/aws/compute/ecsServiceFactory.js +71 -1
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +2 -6
- package/dist/lib/resources/aws/compute/ecsValidation.js +9 -2
- package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +58 -25
- package/dist/lib/resources/aws/database/clickhouseConstants.js +89 -25
- package/dist/lib/resources/aws/database/clickhouseUserData.js +77 -4
- package/dist/lib/resources/aws/database/rdsAurora.js +1 -1
- package/dist/lib/resources/aws/database/rdsInstance.js +1 -1
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.d.ts +9 -5
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.js +21 -12
- package/dist/lib/utils/engineCompat.d.ts +35 -13
- package/dist/lib/utils/engineCompat.js +90 -23
- package/package.json +25 -23
|
@@ -24,12 +24,12 @@ export interface OidcConnectorProps {
|
|
|
24
24
|
* Account TIER of the connected account (organisation | platform | account) —
|
|
25
25
|
* the structural TIER axis, from the account stack's polymorphic
|
|
26
26
|
* `deployRoleAccountTier()`; NEVER the wire `environment` / stage axis (that is
|
|
27
|
-
* `accountStage`).
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* (
|
|
27
|
+
* `accountStage`). Since the §4.4b Step-1 expand, trust WIDTH no longer
|
|
28
|
+
* keys on this tier: every synth passes the account id, so the `FjallDeploy`
|
|
29
|
+
* machine subject accepts `[generic, acct]` (StringEquals array = OR of
|
|
30
|
+
* exact literals) for EVERY tier, letting per-account deploy issuance flip.
|
|
31
|
+
* The tier now gates only the `machineTrustMode: "account-scoped"` contract
|
|
32
|
+
* (governance-only until §4.4b's probe gate) and the boundary preset.
|
|
33
33
|
*/
|
|
34
34
|
accountTier?: AccountTier;
|
|
35
35
|
/**
|
|
@@ -61,16 +61,17 @@ export declare class Account extends Stack {
|
|
|
61
61
|
*/
|
|
62
62
|
protected receivesDeployRole(): boolean;
|
|
63
63
|
/**
|
|
64
|
-
* Account TIER passed to this account's `OidcConnector
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* "
|
|
71
|
-
* `
|
|
72
|
-
*
|
|
73
|
-
*
|
|
64
|
+
* Account TIER passed to this account's `OidcConnector`. A polymorphic
|
|
65
|
+
* METHOD, not a read of the `organisationType` field: the connector is
|
|
66
|
+
* instantiated in THIS base constructor, and subclass field initialisers run
|
|
67
|
+
* AFTER `super()`, so `this.organisationType` would read "account" for every
|
|
68
|
+
* subclass here (the same construction-order reason `receivesDeployRole()`
|
|
69
|
+
* is a method). Base ⇒ "account"; `Platform` ⇒ "platform" and `Organisation`
|
|
70
|
+
* ⇒ "organisation" override. Since the §4.4b Step-1 expand every tier
|
|
71
|
+
* renders the widened `[generic, acct]` machine subject — the tier now gates
|
|
72
|
+
* only the governance-only "account-scoped" contract mode (the builder
|
|
73
|
+
* throws for tier "account"); kept in lockstep with `organisationType` by
|
|
74
|
+
* the Platform-synth deploy-trust parity test.
|
|
74
75
|
*/
|
|
75
76
|
protected deployRoleAccountTier(): AccountTier;
|
|
76
77
|
/**
|
|
@@ -151,16 +151,17 @@ export class Account extends Stack {
|
|
|
151
151
|
return this.constructor === Account;
|
|
152
152
|
}
|
|
153
153
|
/**
|
|
154
|
-
* Account TIER passed to this account's `OidcConnector
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* "
|
|
161
|
-
* `
|
|
162
|
-
*
|
|
163
|
-
*
|
|
154
|
+
* Account TIER passed to this account's `OidcConnector`. A polymorphic
|
|
155
|
+
* METHOD, not a read of the `organisationType` field: the connector is
|
|
156
|
+
* instantiated in THIS base constructor, and subclass field initialisers run
|
|
157
|
+
* AFTER `super()`, so `this.organisationType` would read "account" for every
|
|
158
|
+
* subclass here (the same construction-order reason `receivesDeployRole()`
|
|
159
|
+
* is a method). Base ⇒ "account"; `Platform` ⇒ "platform" and `Organisation`
|
|
160
|
+
* ⇒ "organisation" override. Since the §4.4b Step-1 expand every tier
|
|
161
|
+
* renders the widened `[generic, acct]` machine subject — the tier now gates
|
|
162
|
+
* only the governance-only "account-scoped" contract mode (the builder
|
|
163
|
+
* throws for tier "account"); kept in lockstep with `organisationType` by
|
|
164
|
+
* the Platform-synth deploy-trust parity test.
|
|
164
165
|
*/
|
|
165
166
|
deployRoleAccountTier() {
|
|
166
167
|
return "account";
|
|
@@ -17,9 +17,22 @@ export interface ClickHouseDatabaseProps {
|
|
|
17
17
|
type: "ClickHouse";
|
|
18
18
|
/** VPC to deploy into. Falls back to `app.getVpc()` via the factory. */
|
|
19
19
|
vpc?: IVpc;
|
|
20
|
-
/** EC2 instance type. Default: `m7g.medium` (1 vCPU sustained, 4 GiB)
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
20
|
+
/** EC2 instance type. Default: `m7g.medium` (1 vCPU sustained, 4 GiB);
|
|
21
|
+
* recommended step-up `r8g.medium` (8 GiB — see
|
|
22
|
+
* `DEFAULT_CLICKHOUSE_INSTANCE_TYPE`). Must appear in
|
|
23
|
+
* `CLICKHOUSE_INSTANCE_MEMORY_GIB` — the container memory limit derives
|
|
24
|
+
* from it via `clickHouseTaskMemoryMiB()` and unknown types throw at
|
|
25
|
+
* synth. CH user-data is tuned for 1 vCPU; raise per-user `max_threads`
|
|
26
|
+
* and `max_concurrent_queries` if bumping to a >=2 vCPU host.
|
|
27
|
+
*
|
|
28
|
+
* WARNING — changing this on a DEPLOYED stack REPLACES the persistent
|
|
29
|
+
* data volume, not just the instance. The instance type is embedded in
|
|
30
|
+
* the compute construct's logical-ID key (`getEc2ConfigKey`), so the
|
|
31
|
+
* `AWS::EC2::Volume` beneath it gets a new logical ID: CloudFormation
|
|
32
|
+
* snapshots-then-deletes the old volume (`RemovalPolicy.SNAPSHOT`) and
|
|
33
|
+
* creates an EMPTY one, and the volume's `ownerLogicalId` re-attach tag
|
|
34
|
+
* chain resets. Plan a snapshot-restore migration for live data — this
|
|
35
|
+
* is NOT a config-only change on a stack with data. */
|
|
23
36
|
instanceType?: string;
|
|
24
37
|
/**
|
|
25
38
|
* ECS service desired task count. Defaults to 1 (single-node, single-task
|
|
@@ -22,7 +22,7 @@ import { resolveAlertsTopic } from "../../utils/resolveAlertsTopic.js";
|
|
|
22
22
|
import { createClickHouseAlarms } from "../../resources/aws/monitoring/index.js";
|
|
23
23
|
import { ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, ClickHouseDefaultProfiles, PROFILE_NAME_PATTERN } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
24
24
|
import { inferAmiHardwareType } from "../../resources/aws/compute/ecsConstants.js";
|
|
25
|
-
import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, CLICKHOUSE_EBS_VOLUME_SIZE_GB, CLICKHOUSE_EBS_IOPS, CLICKHOUSE_EBS_THROUGHPUT_MBPS,
|
|
25
|
+
import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, CLICKHOUSE_EBS_VOLUME_SIZE_GB, CLICKHOUSE_EBS_IOPS, CLICKHOUSE_EBS_THROUGHPUT_MBPS, clickHouseTaskMemoryMiB, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_NATIVE_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_SECRET_OPTIONS, CLICKHOUSE_SERVER_ROLE_TAG, CLICKHOUSE_HOST_METRICS, clickHouseUserSecretName, CLICKHOUSE_HEALTH_CHECK, CLICKHOUSE_STOP_TIMEOUT_SECONDS, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, userPasswordEnvName, OPTIMISE_FINAL_SCHEDULE, REPLACING_MERGE_TREE_TABLES, OPTIMISE_MV_TABLES, CLICKHOUSE_CLOUDMAP_SERVICE_NAME, CLICKHOUSE_SERVER_CONTAINER_NAME, OPTIMISE_TASK_MEMORY_MIB, OPTIMISE_TASK_CPU_UNITS, BACKUP_SCHEDULE, BACKUP_TASK_MEMORY_MIB, BACKUP_TASK_CPU_UNITS, BACKUP_RETENTION_DAYS } from "../../resources/aws/database/clickhouseConstants.js";
|
|
26
26
|
import { TlsCertGenerator } from "../../resources/aws/utilities/tlsCertGenerator.js";
|
|
27
27
|
import { EcsCompute } from "./computeEcs.js";
|
|
28
28
|
/**
|
|
@@ -375,6 +375,10 @@ export class ClickHouseDatabase extends Construct {
|
|
|
375
375
|
name: "ClickHouseService",
|
|
376
376
|
capacityProvider: "EC2",
|
|
377
377
|
desiredCount,
|
|
378
|
+
// Omitting `scaling` attaches default CPU target tracking. A scaled-out
|
|
379
|
+
// second task is an EMPTY second database behind the same Cloud Map name
|
|
380
|
+
// (plain MergeTree does not replicate) — desired-count scaling stays off.
|
|
381
|
+
scaling: false,
|
|
378
382
|
serviceDiscovery: {
|
|
379
383
|
name: CLICKHOUSE_CLOUDMAP_SERVICE_NAME,
|
|
380
384
|
// getaddrinfo clients require A; AWS rejects A under HOST/BRIDGE → AWS_VPC.
|
|
@@ -403,7 +407,7 @@ export class ClickHouseDatabase extends Construct {
|
|
|
403
407
|
desiredCapacity: 1,
|
|
404
408
|
minCapacity: 1,
|
|
405
409
|
maxCapacity: 1,
|
|
406
|
-
memoryLimitMiB:
|
|
410
|
+
memoryLimitMiB: clickHouseTaskMemoryMiB(instanceType),
|
|
407
411
|
tags: {
|
|
408
412
|
[CLICKHOUSE_SERVER_ROLE_TAG.key]: CLICKHOUSE_SERVER_ROLE_TAG.value
|
|
409
413
|
}
|
|
@@ -493,6 +497,23 @@ export class ClickHouseDatabase extends Construct {
|
|
|
493
497
|
tlsCaSecret.grantRead(instanceRole);
|
|
494
498
|
if (tlsServerSecret !== undefined)
|
|
495
499
|
tlsServerSecret.grantRead(instanceRole);
|
|
500
|
+
// For the user-data host-metrics timer (see buildClickHouseUserData).
|
|
501
|
+
// PutMetricData is not resource-scopable; the namespace condition bounds
|
|
502
|
+
// the grant to the CWAgent metrics the host alarms consume. The describe
|
|
503
|
+
// grant backs the IMDS-tag fallback path for ASG-name resolution.
|
|
504
|
+
instanceRole.addToPrincipalPolicy(new PolicyStatement({
|
|
505
|
+
actions: ["cloudwatch:PutMetricData"],
|
|
506
|
+
resources: ["*"],
|
|
507
|
+
conditions: {
|
|
508
|
+
StringEquals: {
|
|
509
|
+
"cloudwatch:namespace": CLICKHOUSE_HOST_METRICS.namespace
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}));
|
|
513
|
+
instanceRole.addToPrincipalPolicy(new PolicyStatement({
|
|
514
|
+
actions: ["autoscaling:DescribeAutoScalingInstances"],
|
|
515
|
+
resources: ["*"]
|
|
516
|
+
}));
|
|
496
517
|
// asgName is the CloudWatch dimension the host metrics key off, so without
|
|
497
518
|
// it the alarms cannot be built; no topic is the default, so skip silently
|
|
498
519
|
// rather than throw.
|
|
@@ -622,9 +622,19 @@ export interface EcsServiceConfig {
|
|
|
622
622
|
* ]
|
|
623
623
|
*/
|
|
624
624
|
routing?: EcsRoutingConfig | EcsRoutingConfig[];
|
|
625
|
-
/**
|
|
625
|
+
/**
|
|
626
|
+
* CPU units for this service's tasks (256-4096). Fargate only — EC2 task
|
|
627
|
+
* definitions carry no task-level CPU reservation (CPU is bounded by the
|
|
628
|
+
* instance type), so a value here is rejected at synth when `capacityProvider`
|
|
629
|
+
* is 'EC2' (see `validateEc2ServiceSizing`).
|
|
630
|
+
*/
|
|
626
631
|
cpu?: number;
|
|
627
|
-
/**
|
|
632
|
+
/**
|
|
633
|
+
* Memory in MiB for this service's tasks (512-30720). Fargate only — for EC2
|
|
634
|
+
* capacity the container memory comes from `ec2Config.memoryLimitMiB`; a
|
|
635
|
+
* service-level value here is rejected at synth unless it exactly mirrors
|
|
636
|
+
* that field (see `validateEc2ServiceSizing`).
|
|
637
|
+
*/
|
|
628
638
|
memoryLimitMiB?: number;
|
|
629
639
|
/** Desired number of tasks. Default: 2 */
|
|
630
640
|
desiredCount?: number;
|
|
@@ -16,15 +16,15 @@ import { Duration } from "aws-cdk-lib";
|
|
|
16
16
|
export { isAwsManagedKey, isCMKRequested, AWS_MANAGED, USE_CMK } from "../../utils/databaseTypes.js";
|
|
17
17
|
export const DATABASE_ENGINE_CONFIG = Object.freeze({
|
|
18
18
|
postgresql: {
|
|
19
|
-
aurora: { version: "
|
|
20
|
-
instance: { version: "17.
|
|
19
|
+
aurora: { version: "18.4", majorVersion: "18" },
|
|
20
|
+
instance: { version: "17.9", majorVersion: "17" },
|
|
21
21
|
defaultUsername: "postgres",
|
|
22
22
|
sslParameters: { "rds.force_ssl": "1" }
|
|
23
23
|
},
|
|
24
24
|
mysql: {
|
|
25
|
-
// majorVersion must be Aurora version (3.
|
|
26
|
-
aurora: { version: "8.0.mysql_aurora.3.
|
|
27
|
-
instance: { version: "8.4.
|
|
25
|
+
// majorVersion must be Aurora version (3.12.0), NOT MySQL version (8.0)
|
|
26
|
+
aurora: { version: "8.0.mysql_aurora.3.12.0", majorVersion: "3.12.0" },
|
|
27
|
+
instance: { version: "8.4.9", majorVersion: "8.4" },
|
|
28
28
|
defaultUsername: "admin",
|
|
29
29
|
sslParameters: { require_secure_transport: "ON" }
|
|
30
30
|
}
|
|
@@ -8,6 +8,20 @@ export declare const DEFAULT_LOG_RETENTION = RetentionDays.TWO_WEEKS;
|
|
|
8
8
|
export declare const DEFAULT_FARGATE_CPU = 256;
|
|
9
9
|
export declare const DEFAULT_FARGATE_MEMORY_MIB = 512;
|
|
10
10
|
export declare const DEFAULT_EC2_CONTAINER_MEMORY_MIB = 1024;
|
|
11
|
+
/**
|
|
12
|
+
* The container hard memory limit for an EC2-capacity ECS service.
|
|
13
|
+
*
|
|
14
|
+
* Takes `ec2Config` and nothing else — deliberately. The service-level
|
|
15
|
+
* `memoryLimitMiB` is Fargate-only, and a `?? serviceProps.memoryLimitMiB`
|
|
16
|
+
* fallback here is the silent-ignore footgun `validateEc2ServiceSizing`
|
|
17
|
+
* exists to reject. Narrowing the parameter to the one field that may feed
|
|
18
|
+
* the derivation makes that fallback unrepresentable rather than merely
|
|
19
|
+
* discouraged: re-adding it requires widening this signature, which is a
|
|
20
|
+
* visible act in review. Do not widen it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveEc2ContainerMemoryMiB(ec2Config: {
|
|
23
|
+
readonly memoryLimitMiB?: number;
|
|
24
|
+
} | undefined): number;
|
|
11
25
|
export declare const DEFAULT_ECS_FALLBACK_IMAGE = "amazon/amazon-ecs-sample";
|
|
12
26
|
export declare const DEFAULT_CUSTOM_RESOURCE_TIMEOUT_SECONDS = 300;
|
|
13
27
|
export declare const DEFAULT_HEALTH_CHECK_GRACE_SECONDS = 120;
|
|
@@ -16,6 +16,20 @@ export const DEFAULT_LOG_RETENTION = RetentionDays.TWO_WEEKS;
|
|
|
16
16
|
export const DEFAULT_FARGATE_CPU = 256;
|
|
17
17
|
export const DEFAULT_FARGATE_MEMORY_MIB = 512;
|
|
18
18
|
export const DEFAULT_EC2_CONTAINER_MEMORY_MIB = 1024;
|
|
19
|
+
/**
|
|
20
|
+
* The container hard memory limit for an EC2-capacity ECS service.
|
|
21
|
+
*
|
|
22
|
+
* Takes `ec2Config` and nothing else — deliberately. The service-level
|
|
23
|
+
* `memoryLimitMiB` is Fargate-only, and a `?? serviceProps.memoryLimitMiB`
|
|
24
|
+
* fallback here is the silent-ignore footgun `validateEc2ServiceSizing`
|
|
25
|
+
* exists to reject. Narrowing the parameter to the one field that may feed
|
|
26
|
+
* the derivation makes that fallback unrepresentable rather than merely
|
|
27
|
+
* discouraged: re-adding it requires widening this signature, which is a
|
|
28
|
+
* visible act in review. Do not widen it.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveEc2ContainerMemoryMiB(ec2Config) {
|
|
31
|
+
return ec2Config?.memoryLimitMiB ?? DEFAULT_EC2_CONTAINER_MEMORY_MIB;
|
|
32
|
+
}
|
|
19
33
|
// AWS sample image used when no ECR repository is provided. Consumed by both
|
|
20
34
|
// the resources/ image resolver and the patterns/ defaults block — keep them
|
|
21
35
|
// in lockstep via this single export.
|
|
@@ -46,10 +60,19 @@ export const ARM_INSTANCE_PREFIXES = [
|
|
|
46
60
|
"r6gd",
|
|
47
61
|
"r7g",
|
|
48
62
|
"r7gd",
|
|
63
|
+
"r8g",
|
|
64
|
+
"r8gd",
|
|
65
|
+
"c8g",
|
|
66
|
+
"c8gd",
|
|
67
|
+
"c8gn",
|
|
49
68
|
"m6g",
|
|
50
69
|
"m6gd",
|
|
51
70
|
"m7g",
|
|
52
71
|
"m7gd",
|
|
72
|
+
"m8g",
|
|
73
|
+
"m8gd",
|
|
74
|
+
"x8g",
|
|
75
|
+
"i8g",
|
|
53
76
|
"a1",
|
|
54
77
|
"x2gd",
|
|
55
78
|
"im4gn",
|
|
@@ -17,6 +17,15 @@ export declare function resolveCircuitBreaker(config: false | {
|
|
|
17
17
|
/** Mutable state for ASG capacity provider deduplication. */
|
|
18
18
|
export interface AsgCapacityState {
|
|
19
19
|
providers: Map<string, AsgCapacityProvider>;
|
|
20
|
+
/**
|
|
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}).
|
|
24
|
+
*/
|
|
25
|
+
asgOrigins: Map<string, {
|
|
26
|
+
serviceName: string;
|
|
27
|
+
ec2Config: Ec2CapacityConfig;
|
|
28
|
+
}>;
|
|
20
29
|
autoScalingGroup?: AutoScalingGroup;
|
|
21
30
|
asgSecurityGroup?: ISecurityGroup;
|
|
22
31
|
}
|
|
@@ -26,6 +26,67 @@ export function resolveCircuitBreaker(config) {
|
|
|
26
26
|
const rollback = config?.rollback ?? true;
|
|
27
27
|
return { enable: true, rollback };
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* `ec2Config` fields that shape the ASG but are NOT part of
|
|
31
|
+
* {@link getEc2ConfigKey}, split by how a difference can be detected without
|
|
32
|
+
* false positives. A second service whose key matches reuses the first
|
|
33
|
+
* service's ASG, so any of these it sets is silently discarded.
|
|
34
|
+
*
|
|
35
|
+
* Scalars compare by value. `tags` is plain data, so it compares deeply.
|
|
36
|
+
* `machineImage` / `userData` / `blockDevices` are CDK objects with no stable
|
|
37
|
+
* structural identity — two equivalent literals are different references, so
|
|
38
|
+
* only a defined-vs-undefined asymmetry is safe to report. They deliberately
|
|
39
|
+
* under-report: over-reporting is a hard synth failure on config that
|
|
40
|
+
* previously worked, which is the more damaging direction.
|
|
41
|
+
*/
|
|
42
|
+
const SCALAR_ASG_FIELDS = [
|
|
43
|
+
"minCapacity",
|
|
44
|
+
"maxCapacity",
|
|
45
|
+
"desiredCapacity",
|
|
46
|
+
"instanceMonitoring",
|
|
47
|
+
"associatePublicIpAddress"
|
|
48
|
+
];
|
|
49
|
+
const OPAQUE_ASG_FIELDS = [
|
|
50
|
+
"machineImage",
|
|
51
|
+
"userData",
|
|
52
|
+
"blockDevices"
|
|
53
|
+
];
|
|
54
|
+
function tagRecordsEqual(a, b) {
|
|
55
|
+
if (a === undefined || b === undefined)
|
|
56
|
+
return a === b;
|
|
57
|
+
const aKeys = Object.keys(a);
|
|
58
|
+
if (aKeys.length !== Object.keys(b).length)
|
|
59
|
+
return false;
|
|
60
|
+
return aKeys.every((key) => Object.is(a[key], b[key]));
|
|
61
|
+
}
|
|
62
|
+
function findDifferingAsgFields(ec2Config, origin) {
|
|
63
|
+
const differing = SCALAR_ASG_FIELDS.filter((field) => !Object.is(ec2Config[field], origin[field]));
|
|
64
|
+
if (!tagRecordsEqual(ec2Config.tags, origin.tags))
|
|
65
|
+
differing.push("tags");
|
|
66
|
+
for (const field of OPAQUE_ASG_FIELDS) {
|
|
67
|
+
if ((ec2Config[field] === undefined) !== (origin[field] === undefined)) {
|
|
68
|
+
differing.push(field);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return differing;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Rejects at synth when a service reusing a shared ASG asks for ASG settings
|
|
75
|
+
* that differ from the ones the ASG was built with — those settings would be
|
|
76
|
+
* silently ignored (same silent-ignore class as `validateEc2ServiceSizing`).
|
|
77
|
+
*/
|
|
78
|
+
function assertSharedAsgConfigMatches(serviceName, ec2Config, key, origin) {
|
|
79
|
+
const differing = findDifferingAsgFields(ec2Config, origin.ec2Config);
|
|
80
|
+
if (differing.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
throw new Error(`Service '${serviceName}': ec2Config ${differing.map((f) => `'${f}'`).join(", ")} ` +
|
|
83
|
+
`${differing.length === 1 ? "is" : "are"} ignored — it shares an auto scaling group with ` +
|
|
84
|
+
`service '${origin.serviceName}' (both resolve to EC2 capacity key '${key}'), and that ASG ` +
|
|
85
|
+
`is built from the first service's ec2Config. Either give the two services the same value ` +
|
|
86
|
+
`for ${differing.map((f) => `'${f}'`).join(", ")}, or give this service its own ASG by ` +
|
|
87
|
+
`changing a keyed field ('instanceType', 'amiHardwareType', 'warmPool', ` +
|
|
88
|
+
`'persistentDataVolume', 'availabilityZones').`);
|
|
89
|
+
}
|
|
29
90
|
/**
|
|
30
91
|
* Generates a unique key for EC2 config so services with matching
|
|
31
92
|
* configurations share an ASG.
|
|
@@ -71,8 +132,13 @@ export function getOrCreateAsgCapacityProvider(ctx, serviceProps, state) {
|
|
|
71
132
|
const key = getEc2ConfigKey(ec2Config);
|
|
72
133
|
const existing = state.providers.get(key);
|
|
73
134
|
if (existing) {
|
|
135
|
+
const origin = state.asgOrigins.get(key);
|
|
136
|
+
if (origin !== undefined) {
|
|
137
|
+
assertSharedAsgConfigMatches(serviceProps.name, ec2Config, key, origin);
|
|
138
|
+
}
|
|
74
139
|
return existing;
|
|
75
140
|
}
|
|
141
|
+
state.asgOrigins.set(key, { serviceName: serviceProps.name, ec2Config });
|
|
76
142
|
const safeKey = key.replace(/[^a-zA-Z0-9]/g, "");
|
|
77
143
|
const instanceType = ec2Config.instanceType ?? DEFAULT_EC2_INSTANCE_TYPE;
|
|
78
144
|
const amiHardwareType = ec2Config.amiHardwareType
|
|
@@ -160,7 +226,11 @@ export function getOrCreateAsgCapacityProvider(ctx, serviceProps, state) {
|
|
|
160
226
|
// MTP's ProtectedFromScaleIn flag does NOT clear on CP deletion, so
|
|
161
227
|
// CFN rollback wedges on a stranded protected instance. The drain
|
|
162
228
|
// path is Ec2GracefulTerminationHandler instead.
|
|
163
|
-
enableManagedTerminationProtection: false
|
|
229
|
+
enableManagedTerminationProtection: false,
|
|
230
|
+
// A min=max pinned ASG cannot scale; managed scaling would still mint a
|
|
231
|
+
// CapacityProviderReservation target-tracking pair that wedges in ALARM
|
|
232
|
+
// whenever a task cannot place (2026-07-24 ClickHouse phantom task).
|
|
233
|
+
enableManagedScaling: minCapacity !== maxCapacity
|
|
164
234
|
});
|
|
165
235
|
ctx.cluster.addAsgCapacityProvider(provider);
|
|
166
236
|
state.providers.set(key, provider);
|
|
@@ -5,7 +5,7 @@ import { StringParameter } from "aws-cdk-lib/aws-ssm";
|
|
|
5
5
|
import { buildParameterPath } from "@fjall/util";
|
|
6
6
|
import { resolveOrgId } from "../../../utils/cdkContext.js";
|
|
7
7
|
import { validateSsmPathComponent, validateSecretName } from "./ecsValidation.js";
|
|
8
|
-
import { DEFAULT_LOG_RETENTION, DEFAULT_FARGATE_CPU, DEFAULT_FARGATE_MEMORY_MIB,
|
|
8
|
+
import { DEFAULT_LOG_RETENTION, DEFAULT_FARGATE_CPU, DEFAULT_FARGATE_MEMORY_MIB, resolveEc2ContainerMemoryMiB } from "./ecsConstants.js";
|
|
9
9
|
import { LogGroup } from "../logging/logGroup.js";
|
|
10
10
|
import { getContainerImage } from "./ecsImages.js";
|
|
11
11
|
import { resolveRemoteConnections } from "./ecsRemoteConnections.js";
|
|
@@ -214,12 +214,8 @@ export function addContainersToTask(ctx, serviceName, serviceProps, taskDefiniti
|
|
|
214
214
|
stopTimeout: containerConfig.stopTimeout !== undefined
|
|
215
215
|
? Duration.seconds(containerConfig.stopTimeout)
|
|
216
216
|
: undefined,
|
|
217
|
-
// EC2 container memory is ec2Config-only by design — do NOT add a
|
|
218
|
-
// `?? serviceProps.memoryLimitMiB` fallback (that is the silent-ignore
|
|
219
|
-
// footgun `validateEc2ServiceSizing` exists to reject).
|
|
220
217
|
...(isServiceEc2(serviceProps) && {
|
|
221
|
-
memoryLimitMiB: serviceProps.ec2Config
|
|
222
|
-
DEFAULT_EC2_CONTAINER_MEMORY_MIB
|
|
218
|
+
memoryLimitMiB: resolveEc2ContainerMemoryMiB(serviceProps.ec2Config)
|
|
223
219
|
})
|
|
224
220
|
});
|
|
225
221
|
if (containerConfig.port !== undefined &&
|
|
@@ -156,12 +156,19 @@ export function validateEcsClusterProps(props) {
|
|
|
156
156
|
// backlog scaling — CPU/MEMORY target-tracking produces no datapoint at
|
|
157
157
|
// zero running tasks, so DesiredCount never rises (the 2026-06-04
|
|
158
158
|
// asset-discovery outage). Reject the un-wakeable shape at synth.
|
|
159
|
+
// scalingType undefined means scaling was EXPLICITLY disabled
|
|
160
|
+
// (resolveScalingConfig defaults undefined input to CPU): no policy exists
|
|
161
|
+
// to wake the service, so desiredCount 0 is a deliberately parked service
|
|
162
|
+
// (the ClickHouse bring-up escape hatch), not the trap.
|
|
159
163
|
if (service.desiredCount === 0 &&
|
|
164
|
+
service.scalingType !== undefined &&
|
|
160
165
|
service.scalingType !== ScalingType.QUEUE) {
|
|
161
166
|
throw new Error(`Service '${service.name}': desiredCount is 0 but scalingType is ` +
|
|
162
|
-
`${service.scalingType
|
|
167
|
+
`${service.scalingType} — a service that starts at zero can ` +
|
|
163
168
|
"only wake on queue backlog. Use ScalingType.QUEUE with queueScaling.queues " +
|
|
164
|
-
"so it scales up from 0,
|
|
169
|
+
"so it scales up from 0, raise desiredCount to keep at least one task running, " +
|
|
170
|
+
"or disable scaling to park the service deliberately (patterns layer: " +
|
|
171
|
+
"scaling: false; resources layer: omit scalingType). " +
|
|
165
172
|
"CPU/MEMORY target-tracking produces no datapoint at zero tasks and never scales up.");
|
|
166
173
|
}
|
|
167
174
|
if (service.scalingType === ScalingType.QUEUE) {
|
|
@@ -14,10 +14,15 @@ export declare const CLICKHOUSE_DATABASE_NAME = "analytics";
|
|
|
14
14
|
* max_concurrent_queries=8) — see clickhouseUserData.ts. Bumping to a larger
|
|
15
15
|
* instance MUST be paired with raising those thread/concurrency caps.
|
|
16
16
|
*
|
|
17
|
-
* Next size up
|
|
18
|
-
* `
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Next size up (2026-07-31 right-sizing, designs/2026-07-31-clickhouse-
|
|
18
|
+
* factory-fit-for-purpose.md): `r8g.medium` (1 vCPU Graviton4, 8 GiB,
|
|
19
|
+
* ~+30%/core over Graviton3) — CH's own doctrine floors production at
|
|
20
|
+
* 8 GiB, and the memory-bound single-tenant profile wants RAM before
|
|
21
|
+
* cores. `m7g.large` (2 vCPU, 8 GiB) only when CPU-bound. Set via
|
|
22
|
+
* `clickhouseInstanceType` CDK context or the `instanceType` prop —
|
|
23
|
+
* the container memory limit derives automatically via
|
|
24
|
+
* `clickHouseTaskMemoryMiB()`, but the thread/concurrency caps in
|
|
25
|
+
* clickhouseUserData.ts still need a lockstep re-tune. */
|
|
21
26
|
export declare const DEFAULT_CLICKHOUSE_INSTANCE_TYPE = "m7g.medium";
|
|
22
27
|
/** ClickHouse container image. Explicit `docker.io/` prefix is required so
|
|
23
28
|
* string-form consumers in `ecsImages.ts#getContainerImage()` route through
|
|
@@ -38,17 +43,35 @@ export declare const DEFAULT_CLICKHOUSE_INSTANCE_TYPE = "m7g.medium";
|
|
|
38
43
|
* Image size is ~250 MB larger but irrelevant on a single-instance EC2 ASG
|
|
39
44
|
* that pulls once per launch. Upstream CH CI runs its full perf + stress
|
|
40
45
|
* matrix on the Ubuntu build; Alpine is community-tier coverage. */
|
|
41
|
-
export declare const CLICKHOUSE_IMAGE = "docker.io/clickhouse/clickhouse-server:26.3.
|
|
46
|
+
export declare const CLICKHOUSE_IMAGE = "docker.io/clickhouse/clickhouse-server:26.3.17.56";
|
|
42
47
|
/** EBS volume configuration. */
|
|
43
48
|
export declare const CLICKHOUSE_EBS_VOLUME_SIZE_GB = 80;
|
|
44
49
|
export declare const CLICKHOUSE_EBS_IOPS = 3000;
|
|
45
50
|
export declare const CLICKHOUSE_EBS_THROUGHPUT_MBPS = 125;
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
51
|
+
/** Host memory reserved from the ClickHouse container: kernel + ECS agent +
|
|
52
|
+
* the host-metrics timer. The ECS agent advertises MemTotal minus
|
|
53
|
+
* CLICKHOUSE_ECS_RESERVED_MEMORY_MIB, and MemTotal itself runs below the
|
|
54
|
+
* AWS nominal figure (firmware carve-outs plus page-struct overhead that
|
|
55
|
+
* grows with RAM, ~205 MiB observed on the 4 GiB m7g.medium), so this
|
|
56
|
+
* reserve must absorb both or the task never places. 1 GiB is proven at
|
|
57
|
+
* 4–16 GiB; the ≥32 GiB rows double it (see clickHouseTaskMemoryMiB). */
|
|
58
|
+
export declare const CLICKHOUSE_HOST_RESERVE_MIB = 1024;
|
|
59
|
+
/** MiB the ECS agent withholds from advertised capacity — written to
|
|
60
|
+
* /etc/ecs/ecs.config as `ECS_RESERVED_MEMORY` by buildClickHouseUserData.
|
|
61
|
+
* Must stay well inside CLICKHOUSE_HOST_RESERVE_MIB: the container limit is
|
|
62
|
+
* derived from nominal memory, so the host reserve is what absorbs this
|
|
63
|
+
* withholding at task placement. */
|
|
64
|
+
export declare const CLICKHOUSE_ECS_RESERVED_MEMORY_MIB = 256;
|
|
65
|
+
/** Total memory (GiB) of the instance types the ClickHouse construct knows
|
|
66
|
+
* how to size a container for. Values are the AWS nominal figures. */
|
|
67
|
+
export declare const CLICKHOUSE_INSTANCE_MEMORY_GIB: Record<string, number>;
|
|
68
|
+
/** ECS container memory for the ClickHouse server task, derived from the
|
|
69
|
+
* instance type so a size change cannot leave the hand-set container cap
|
|
70
|
+
* behind (the pre-2026-07 shape: a hardcoded 3072 that silently starved —
|
|
71
|
+
* or over-committed — any non-4-GiB host). Unknown types throw at synth:
|
|
72
|
+
* a memoryLimitMiB above what the host advertises strands the task in
|
|
73
|
+
* PROVISIONING with no CloudFormation error. */
|
|
74
|
+
export declare function clickHouseTaskMemoryMiB(instanceType: string): number;
|
|
52
75
|
/** ClickHouse ports. */
|
|
53
76
|
export declare const CLICKHOUSE_HTTP_PORT = 8123;
|
|
54
77
|
export declare const CLICKHOUSE_NATIVE_PORT = 9000;
|
|
@@ -58,22 +81,16 @@ export declare const CLICKHOUSE_PROMETHEUS_PORT = 9363;
|
|
|
58
81
|
* See aiDocs/patterns/clickhouse-tls-pattern.md § "The contract". */
|
|
59
82
|
export declare const CLICKHOUSE_HTTPS_PORT = 8443;
|
|
60
83
|
export declare const CLICKHOUSE_TCP_SECURE_PORT = 9440;
|
|
61
|
-
/** Mount path inside the
|
|
62
|
-
*
|
|
63
|
-
*
|
|
84
|
+
/** Mount path inside the ClickHouse container for the materialised TLS cert
|
|
85
|
+
* + key. The EC2 user-data bootstrap (`buildClickHouseUserData` tlsBootstrap)
|
|
86
|
+
* fetches the PEMs from Secrets Manager onto the EBS data volume at
|
|
87
|
+
* `<mount>/server-certs`, which is bind-mounted read-only into the container
|
|
88
|
+
* at this path. */
|
|
64
89
|
export declare const CLICKHOUSE_TLS_CERT_MOUNT_PATH = "/etc/clickhouse-server/certs";
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
/** UID:GID the official ClickHouse image runs as. Init container `chown`s
|
|
69
|
-
* the materialised cert files to this UID so the server can read them. */
|
|
90
|
+
/** UID:GID the official ClickHouse image runs as. The user-data bootstrap
|
|
91
|
+
* `chown`s the materialised cert files to this UID so the server can read
|
|
92
|
+
* them. */
|
|
70
93
|
export declare const CLICKHOUSE_UID = 101;
|
|
71
|
-
/** Digest-pinned alpine image used by the TLS init container. The init
|
|
72
|
-
* container needs `jq` (extracts `cert`+`key` from the server-cert JSON
|
|
73
|
-
* secret); alpine ships it via `apk add` — but pinning the digest avoids
|
|
74
|
-
* fetching `:latest` on every deploy. Bump in lockstep with renovate
|
|
75
|
-
* alerts; CI verifies the digest resolves. */
|
|
76
|
-
export declare const ALPINE_INIT_CONTAINER_IMAGE = "public.ecr.aws/docker/library/alpine:3.20@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d";
|
|
77
94
|
/** EBS device name for the data volume (must match user data script). */
|
|
78
95
|
export declare const CLICKHOUSE_EBS_DEVICE_NAME = "/dev/xvdf";
|
|
79
96
|
/** EBS mount path on the EC2 host. */
|
|
@@ -123,6 +140,22 @@ export declare const CLICKHOUSE_SERVER_ROLE_TAG: {
|
|
|
123
140
|
readonly key: "ClickHouseRole";
|
|
124
141
|
readonly value: "server";
|
|
125
142
|
};
|
|
143
|
+
/** CloudWatch identity of the host metrics: namespace, metric names, and the
|
|
144
|
+
* dimension key. Three sites consume this and they MUST match: (a) the
|
|
145
|
+
* user-data put-metric-data timer (`buildClickHouseUserData`) that publishes
|
|
146
|
+
* the datapoints, (b) the host alarms (`createClickHouseAlarms`) that
|
|
147
|
+
* consume them, and (c) the instance-role PutMetricData namespace condition
|
|
148
|
+
* (`clickhouseDatabase.ts`). One-character drift re-blinds the alarms
|
|
149
|
+
* silently — and with disk-critical on TreatMissingData.BREACHING, a
|
|
150
|
+
* namespace typo pages as a phantom full disk. "CWAgent" matches what the
|
|
151
|
+
* CloudWatch Agent would publish, keeping dashboards portable if the timer
|
|
152
|
+
* is ever replaced by the real agent. */
|
|
153
|
+
export declare const CLICKHOUSE_HOST_METRICS: {
|
|
154
|
+
readonly namespace: "CWAgent";
|
|
155
|
+
readonly memoryMetric: "mem_used_percent";
|
|
156
|
+
readonly diskMetric: "disk_used_percent";
|
|
157
|
+
readonly asgDimension: "AutoScalingGroupName";
|
|
158
|
+
};
|
|
126
159
|
/** Shared secret generation options (all ClickHouse users share the same policy). */
|
|
127
160
|
export declare const CLICKHOUSE_SECRET_OPTIONS: {
|
|
128
161
|
readonly excludePunctuation: true;
|