@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
|
@@ -14,10 +14,15 @@ export 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 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,66 @@ export 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 const CLICKHOUSE_IMAGE = "docker.io/clickhouse/clickhouse-server:26.3.
|
|
46
|
+
export const CLICKHOUSE_IMAGE = "docker.io/clickhouse/clickhouse-server:26.3.17.56";
|
|
42
47
|
/** EBS volume configuration. */
|
|
43
48
|
export const CLICKHOUSE_EBS_VOLUME_SIZE_GB = 80;
|
|
44
49
|
export const CLICKHOUSE_EBS_IOPS = 3000;
|
|
45
50
|
export 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 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 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 const CLICKHOUSE_INSTANCE_MEMORY_GIB = {
|
|
68
|
+
"t4g.medium": 4,
|
|
69
|
+
"m7g.medium": 4,
|
|
70
|
+
"m8g.medium": 4,
|
|
71
|
+
"r7g.medium": 8,
|
|
72
|
+
"r8g.medium": 8,
|
|
73
|
+
"m7g.large": 8,
|
|
74
|
+
"m8g.large": 8,
|
|
75
|
+
"r7g.large": 16,
|
|
76
|
+
"r8g.large": 16,
|
|
77
|
+
"m7g.xlarge": 16,
|
|
78
|
+
"m8g.xlarge": 16,
|
|
79
|
+
"r7g.xlarge": 32,
|
|
80
|
+
"r8g.xlarge": 32
|
|
81
|
+
};
|
|
82
|
+
/** ECS container memory for the ClickHouse server task, derived from the
|
|
83
|
+
* instance type so a size change cannot leave the hand-set container cap
|
|
84
|
+
* behind (the pre-2026-07 shape: a hardcoded 3072 that silently starved —
|
|
85
|
+
* or over-committed — any non-4-GiB host). Unknown types throw at synth:
|
|
86
|
+
* a memoryLimitMiB above what the host advertises strands the task in
|
|
87
|
+
* PROVISIONING with no CloudFormation error. */
|
|
88
|
+
export function clickHouseTaskMemoryMiB(instanceType) {
|
|
89
|
+
const memoryGib = CLICKHOUSE_INSTANCE_MEMORY_GIB[instanceType];
|
|
90
|
+
if (memoryGib === undefined) {
|
|
91
|
+
throw new Error(`ClickHouseDatabase: unknown instance type '${instanceType}' — cannot ` +
|
|
92
|
+
"derive the container memory limit. Supported: " +
|
|
93
|
+
`${Object.keys(CLICKHOUSE_INSTANCE_MEMORY_GIB).join(", ")}. Add the ` +
|
|
94
|
+
"type's nominal memory to CLICKHOUSE_INSTANCE_MEMORY_GIB " +
|
|
95
|
+
"(clickhouseConstants.ts) to support it.");
|
|
96
|
+
}
|
|
97
|
+
// ≥32 GiB rows double the reserve: the kernel-invisible gap (firmware +
|
|
98
|
+
// page structs) approaches 1 GiB there, and a limit above what the ECS
|
|
99
|
+
// agent advertises (MemTotal − ECS_RESERVED_MEMORY) strands the task in
|
|
100
|
+
// PROVISIONING with no CloudFormation error.
|
|
101
|
+
const reserveMib = memoryGib >= 32
|
|
102
|
+
? CLICKHOUSE_HOST_RESERVE_MIB * 2
|
|
103
|
+
: CLICKHOUSE_HOST_RESERVE_MIB;
|
|
104
|
+
return memoryGib * 1024 - reserveMib;
|
|
105
|
+
}
|
|
52
106
|
/** ClickHouse ports. */
|
|
53
107
|
export const CLICKHOUSE_HTTP_PORT = 8123;
|
|
54
108
|
export const CLICKHOUSE_NATIVE_PORT = 9000;
|
|
@@ -58,22 +112,16 @@ export const CLICKHOUSE_PROMETHEUS_PORT = 9363;
|
|
|
58
112
|
* See aiDocs/patterns/clickhouse-tls-pattern.md § "The contract". */
|
|
59
113
|
export const CLICKHOUSE_HTTPS_PORT = 8443;
|
|
60
114
|
export const CLICKHOUSE_TCP_SECURE_PORT = 9440;
|
|
61
|
-
/** Mount path inside the
|
|
62
|
-
*
|
|
63
|
-
*
|
|
115
|
+
/** Mount path inside the ClickHouse container for the materialised TLS cert
|
|
116
|
+
* + key. The EC2 user-data bootstrap (`buildClickHouseUserData` tlsBootstrap)
|
|
117
|
+
* fetches the PEMs from Secrets Manager onto the EBS data volume at
|
|
118
|
+
* `<mount>/server-certs`, which is bind-mounted read-only into the container
|
|
119
|
+
* at this path. */
|
|
64
120
|
export 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. */
|
|
121
|
+
/** UID:GID the official ClickHouse image runs as. The user-data bootstrap
|
|
122
|
+
* `chown`s the materialised cert files to this UID so the server can read
|
|
123
|
+
* them. */
|
|
70
124
|
export 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 const ALPINE_INIT_CONTAINER_IMAGE = "public.ecr.aws/docker/library/alpine:3.20@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d";
|
|
77
125
|
/** EBS device name for the data volume (must match user data script). */
|
|
78
126
|
export const CLICKHOUSE_EBS_DEVICE_NAME = "/dev/xvdf";
|
|
79
127
|
/** EBS mount path on the EC2 host. */
|
|
@@ -129,6 +177,22 @@ export const CLICKHOUSE_SERVER_ROLE_TAG = {
|
|
|
129
177
|
key: "ClickHouseRole",
|
|
130
178
|
value: "server"
|
|
131
179
|
};
|
|
180
|
+
/** CloudWatch identity of the host metrics: namespace, metric names, and the
|
|
181
|
+
* dimension key. Three sites consume this and they MUST match: (a) the
|
|
182
|
+
* user-data put-metric-data timer (`buildClickHouseUserData`) that publishes
|
|
183
|
+
* the datapoints, (b) the host alarms (`createClickHouseAlarms`) that
|
|
184
|
+
* consume them, and (c) the instance-role PutMetricData namespace condition
|
|
185
|
+
* (`clickhouseDatabase.ts`). One-character drift re-blinds the alarms
|
|
186
|
+
* silently — and with disk-critical on TreatMissingData.BREACHING, a
|
|
187
|
+
* namespace typo pages as a phantom full disk. "CWAgent" matches what the
|
|
188
|
+
* CloudWatch Agent would publish, keeping dashboards portable if the timer
|
|
189
|
+
* is ever replaced by the real agent. */
|
|
190
|
+
export const CLICKHOUSE_HOST_METRICS = {
|
|
191
|
+
namespace: "CWAgent",
|
|
192
|
+
memoryMetric: "mem_used_percent",
|
|
193
|
+
diskMetric: "disk_used_percent",
|
|
194
|
+
asgDimension: "AutoScalingGroupName"
|
|
195
|
+
};
|
|
132
196
|
/** Shared secret generation options (all ClickHouse users share the same policy). */
|
|
133
197
|
export const CLICKHOUSE_SECRET_OPTIONS = {
|
|
134
198
|
excludePunctuation: true,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, clickHousePasswordSha256Snippet } from "./clickhouseConstants.js";
|
|
1
|
+
import { CLICKHOUSE_DATA_MOUNT_PATH, CLICKHOUSE_EBS_DEVICE_NAME, CLICKHOUSE_CONFIG_SUBDIR, CLICKHOUSE_USERS_SUBDIR, CLICKHOUSE_ECS_RESERVED_MEMORY_MIB, CLICKHOUSE_HOST_METRICS, CLICKHOUSE_HTTP_PORT, CLICKHOUSE_HTTPS_PORT, CLICKHOUSE_TCP_SECURE_PORT, CLICKHOUSE_TLS_CERT_MOUNT_PATH, CLICKHOUSE_PROMETHEUS_PORT, CLICKHOUSE_UID, clickHousePasswordSha256Snippet } from "./clickhouseConstants.js";
|
|
2
2
|
import { renderUsersXml } from "./clickhouseXmlRenderer.js";
|
|
3
3
|
export function generateServerConfigXml(options) {
|
|
4
4
|
const { backupBucketName, backupBucketRegion, coldTier } = options;
|
|
@@ -320,11 +320,76 @@ cat > "$MOUNT_POINT/server-certs/client.xml" <<'CLIENTXML'
|
|
|
320
320
|
</config>
|
|
321
321
|
CLIENTXML
|
|
322
322
|
|
|
323
|
-
chown -R
|
|
323
|
+
chown -R ${CLICKHOUSE_UID}:${CLICKHOUSE_UID} "$MOUNT_POINT/server-certs"
|
|
324
324
|
chmod 600 "$MOUNT_POINT/server-certs/server.key"
|
|
325
325
|
chmod 644 "$MOUNT_POINT/server-certs/client.xml"
|
|
326
326
|
`
|
|
327
327
|
: "";
|
|
328
|
+
// The CWAgent-namespace alarms (mem_used_percent / disk_used_percent) have
|
|
329
|
+
// no producer without this — no CloudWatch Agent is installed. The ASG name
|
|
330
|
+
// (the alarms' dimension) is unknowable at synth (LaunchTemplate → ASG is a
|
|
331
|
+
// CFN cycle), so it resolves at run time: IMDS instance tags first, with a
|
|
332
|
+
// describe-auto-scaling-instances fallback. AL2023 ECS AMIs ship no cron;
|
|
333
|
+
// systemd timer is the only scheduler present. The trailing `|| echo` keeps
|
|
334
|
+
// a publisher failure from aborting the whole database bootstrap.
|
|
335
|
+
//
|
|
336
|
+
// Hang-proofing: `Type=oneshot` defaults TimeoutStartSec to infinity and a
|
|
337
|
+
// timer never re-triggers a still-activating unit, so ONE wedged run (hung
|
|
338
|
+
// IMDS or AWS call) would silence the publisher forever — every curl carries
|
|
339
|
+
// --max-time and the unit a TimeoutStartSec under the 60s cadence. The disk
|
|
340
|
+
// metric publishes only when the data volume is actually mounted: `df` on
|
|
341
|
+
// an unmounted path reports the ROOT filesystem, a false-healthy number
|
|
342
|
+
// that would defeat the disk-critical alarm's missing-data=BREACHING flip.
|
|
343
|
+
const { namespace, memoryMetric, diskMetric, asgDimension } = CLICKHOUSE_HOST_METRICS;
|
|
344
|
+
const metricsPublisher = `
|
|
345
|
+
cat > /usr/local/bin/fjall-ch-metrics.sh << 'METRICSEOF'
|
|
346
|
+
#!/bin/bash
|
|
347
|
+
set -euo pipefail
|
|
348
|
+
IMDS_BASE="http://169.254.169.254/latest"
|
|
349
|
+
TOKEN=$(curl -sf --max-time 5 -X PUT "$IMDS_BASE/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 300")
|
|
350
|
+
imds() { curl -sf --max-time 5 -H "X-aws-ec2-metadata-token: $TOKEN" "$IMDS_BASE/meta-data/$1"; }
|
|
351
|
+
REGION=$(imds placement/region)
|
|
352
|
+
ASG=$(imds tags/instance/aws:autoscaling:groupName || true)
|
|
353
|
+
if [ -z "$ASG" ]; then
|
|
354
|
+
INSTANCE_ID=$(imds instance-id)
|
|
355
|
+
ASG=$(aws autoscaling describe-auto-scaling-instances --instance-ids "$INSTANCE_ID" --region "$REGION" --query "AutoScalingInstances[0].AutoScalingGroupName" --output text)
|
|
356
|
+
fi
|
|
357
|
+
if [ -z "$ASG" ] || [ "$ASG" = "None" ]; then
|
|
358
|
+
echo "fjall-ch-metrics: could not resolve ASG name" >&2
|
|
359
|
+
exit 1
|
|
360
|
+
fi
|
|
361
|
+
MEM=$(awk '/MemTotal/{t=$2} /MemAvailable/{a=$2} END{printf "%.1f", (t-a)*100/t}' /proc/meminfo)
|
|
362
|
+
METRICS=("MetricName=${memoryMetric},Dimensions=[{Name=${asgDimension},Value=$ASG}],Value=$MEM,Unit=Percent")
|
|
363
|
+
if mountpoint -q ${CLICKHOUSE_DATA_MOUNT_PATH}; then
|
|
364
|
+
DISK=$(df --output=pcent ${CLICKHOUSE_DATA_MOUNT_PATH} | tail -n 1 | tr -dc '0-9')
|
|
365
|
+
METRICS+=("MetricName=${diskMetric},Dimensions=[{Name=${asgDimension},Value=$ASG}],Value=$DISK,Unit=Percent")
|
|
366
|
+
else
|
|
367
|
+
echo "fjall-ch-metrics: ${CLICKHOUSE_DATA_MOUNT_PATH} not mounted, skipping disk metric" >&2
|
|
368
|
+
fi
|
|
369
|
+
aws cloudwatch put-metric-data --region "$REGION" --namespace ${namespace} --metric-data "\${METRICS[@]}"
|
|
370
|
+
METRICSEOF
|
|
371
|
+
chmod 755 /usr/local/bin/fjall-ch-metrics.sh
|
|
372
|
+
cat > /etc/systemd/system/fjall-ch-metrics.service << 'SVCEOF'
|
|
373
|
+
[Unit]
|
|
374
|
+
Description=Publish ClickHouse host metrics to CloudWatch
|
|
375
|
+
[Service]
|
|
376
|
+
Type=oneshot
|
|
377
|
+
TimeoutStartSec=50
|
|
378
|
+
ExecStart=/usr/local/bin/fjall-ch-metrics.sh
|
|
379
|
+
SVCEOF
|
|
380
|
+
cat > /etc/systemd/system/fjall-ch-metrics.timer << 'TIMEREOF'
|
|
381
|
+
[Unit]
|
|
382
|
+
Description=Run fjall-ch-metrics every minute
|
|
383
|
+
[Timer]
|
|
384
|
+
OnBootSec=60s
|
|
385
|
+
OnUnitActiveSec=60s
|
|
386
|
+
RandomizedDelaySec=5s
|
|
387
|
+
[Install]
|
|
388
|
+
WantedBy=timers.target
|
|
389
|
+
TIMEREOF
|
|
390
|
+
systemctl daemon-reload
|
|
391
|
+
systemctl enable --now fjall-ch-metrics.timer || echo "WARN: fjall-ch-metrics timer enable failed" >&2
|
|
392
|
+
`;
|
|
328
393
|
const script = `#!/bin/bash
|
|
329
394
|
set -euo pipefail
|
|
330
395
|
|
|
@@ -357,6 +422,14 @@ DOCKEROVERRIDE
|
|
|
357
422
|
systemctl daemon-reload
|
|
358
423
|
systemctl restart docker || true
|
|
359
424
|
|
|
425
|
+
# 3. Tell the ECS agent to withhold 256 MiB from advertised capacity so the
|
|
426
|
+
# scheduler accounts for kernel + agent + metrics-timer overhead instead of
|
|
427
|
+
# offering the container the host's full nominal memory. CDK appends its own
|
|
428
|
+
# 'echo ECS_CLUSTER=... >> /etc/ecs/ecs.config' after this script, so >> is
|
|
429
|
+
# required here (a > would be clobber-safe only by ordering luck).
|
|
430
|
+
mkdir -p /etc/ecs
|
|
431
|
+
echo "ECS_RESERVED_MEMORY=${CLICKHOUSE_ECS_RESERVED_MEMORY_MIB}" >> /etc/ecs/ecs.config
|
|
432
|
+
|
|
360
433
|
# Wait for the EBS volume to be attached (up to 60 seconds)
|
|
361
434
|
for i in $(seq 1 60); do
|
|
362
435
|
if [ -b "$DEVICE" ]; then
|
|
@@ -410,8 +483,8 @@ if [ "$USERS_CONFIG_RETRIEVED" != "1" ]; then
|
|
|
410
483
|
exit 1
|
|
411
484
|
fi
|
|
412
485
|
|
|
413
|
-
chown -R
|
|
414
|
-
${tlsBootstrap}`;
|
|
486
|
+
chown -R ${CLICKHOUSE_UID}:${CLICKHOUSE_UID} "$MOUNT_POINT"
|
|
487
|
+
${metricsPublisher}${tlsBootstrap}`;
|
|
415
488
|
return compactUserDataScript(script);
|
|
416
489
|
}
|
|
417
490
|
/**
|
|
@@ -152,7 +152,7 @@ export class RdsAurora extends Construct {
|
|
|
152
152
|
const readers = this.buildReaders(props, piEnabled, performanceInsightsKey, performanceInsightsRetention);
|
|
153
153
|
const engine = props.engine ||
|
|
154
154
|
DatabaseClusterEngine.auroraPostgres({
|
|
155
|
-
version: AuroraPostgresEngineVersion.of("
|
|
155
|
+
version: AuroraPostgresEngineVersion.of("18.4", "18")
|
|
156
156
|
});
|
|
157
157
|
const parameterGroup = new ParameterGroup(this, `${this.databaseNameValue}ParameterGroup`, {
|
|
158
158
|
engine,
|
|
@@ -110,7 +110,7 @@ export class RdsInstance extends Construct {
|
|
|
110
110
|
: undefined;
|
|
111
111
|
const engine = props.engine ||
|
|
112
112
|
DatabaseInstanceEngine.postgres({
|
|
113
|
-
version: PostgresEngineVersion.
|
|
113
|
+
version: PostgresEngineVersion.VER_17_9
|
|
114
114
|
});
|
|
115
115
|
const parameterGroup = new ParameterGroup(this, `${this.databaseNameValue}ParameterGroup`, {
|
|
116
116
|
engine,
|
|
@@ -6,11 +6,11 @@ import type { Construct } from "constructs";
|
|
|
6
6
|
export interface ClickHouseAlarmThresholds {
|
|
7
7
|
/** EC2 host CPU % over 5 min. Default 90. */
|
|
8
8
|
cpuThreshold?: number;
|
|
9
|
-
/** EC2 host memory % over 5 min (
|
|
9
|
+
/** EC2 host memory % over 5 min (CWAgent namespace). Default 80. */
|
|
10
10
|
memoryThreshold?: number;
|
|
11
|
-
/**
|
|
11
|
+
/** ClickHouse data volume disk % used. Default 70 (warn) — paired with critical at 85. */
|
|
12
12
|
diskWarnThreshold?: number;
|
|
13
|
-
/**
|
|
13
|
+
/** ClickHouse data volume disk % used. Default 85. */
|
|
14
14
|
diskCriticalThreshold?: number;
|
|
15
15
|
}
|
|
16
16
|
export interface ClickHouseAlarmsProps {
|
|
@@ -39,8 +39,12 @@ export interface ClickHouseAlarmsProps {
|
|
|
39
39
|
config?: ClickHouseAlarmThresholds;
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Single-node ClickHouse host-posture alarms. Covers host-level CPU
|
|
43
|
-
* memory and disk
|
|
42
|
+
* Single-node ClickHouse host-posture alarms. Covers host-level CPU (AWS/EC2)
|
|
43
|
+
* plus memory and disk in the `CWAgent` namespace — fed NOT by the CloudWatch
|
|
44
|
+
* Agent (never installed on these hosts) but by the lightweight put-metric-data
|
|
45
|
+
* timer the user-data installs (see `buildClickHouseUserData`), which publishes
|
|
46
|
+
* `mem_used_percent` / `disk_used_percent` under the same names and
|
|
47
|
+
* `AutoScalingGroupName` dimension the agent would use — plus the
|
|
44
48
|
* backup-failure log alarm when a backup-task log group is supplied:
|
|
45
49
|
*
|
|
46
50
|
* - **Backup failures** — `AccessDenied` or `S3Exception` from the backup
|
|
@@ -5,9 +5,14 @@ import { Metric } from "aws-cdk-lib/aws-cloudwatch";
|
|
|
5
5
|
import { FilterPattern, MetricFilter } from "aws-cdk-lib/aws-logs";
|
|
6
6
|
import { ALARM_DEFAULTS, registerAlarm, buildAlarmDescription } from "./alarmDefaults.js";
|
|
7
7
|
import { METRIC_NAMESPACE } from "./metricNamespaces.js";
|
|
8
|
+
import { CLICKHOUSE_HOST_METRICS } from "../database/clickhouseConstants.js";
|
|
8
9
|
/**
|
|
9
|
-
* Single-node ClickHouse host-posture alarms. Covers host-level CPU
|
|
10
|
-
* memory and disk
|
|
10
|
+
* Single-node ClickHouse host-posture alarms. Covers host-level CPU (AWS/EC2)
|
|
11
|
+
* plus memory and disk in the `CWAgent` namespace — fed NOT by the CloudWatch
|
|
12
|
+
* Agent (never installed on these hosts) but by the lightweight put-metric-data
|
|
13
|
+
* timer the user-data installs (see `buildClickHouseUserData`), which publishes
|
|
14
|
+
* `mem_used_percent` / `disk_used_percent` under the same names and
|
|
15
|
+
* `AutoScalingGroupName` dimension the agent would use — plus the
|
|
11
16
|
* backup-failure log alarm when a backup-task log group is supplied:
|
|
12
17
|
*
|
|
13
18
|
* - **Backup failures** — `AccessDenied` or `S3Exception` from the backup
|
|
@@ -42,9 +47,9 @@ export function createClickHouseAlarms(props) {
|
|
|
42
47
|
const memoryAlarm = new Alarm(scope, "ClickHouseMemoryAlarm", {
|
|
43
48
|
alarmDescription: buildAlarmDescription("ClickHouse host memory utilisation exceeds threshold (CWAgent)", undefined),
|
|
44
49
|
metric: new Metric({
|
|
45
|
-
namespace:
|
|
46
|
-
metricName:
|
|
47
|
-
dimensionsMap: {
|
|
50
|
+
namespace: CLICKHOUSE_HOST_METRICS.namespace,
|
|
51
|
+
metricName: CLICKHOUSE_HOST_METRICS.memoryMetric,
|
|
52
|
+
dimensionsMap: { [CLICKHOUSE_HOST_METRICS.asgDimension]: asgName },
|
|
48
53
|
period: ALARM_DEFAULTS.EVALUATION_PERIOD,
|
|
49
54
|
statistic: "Average"
|
|
50
55
|
}),
|
|
@@ -58,9 +63,9 @@ export function createClickHouseAlarms(props) {
|
|
|
58
63
|
const diskWarnAlarm = new Alarm(scope, "ClickHouseDiskWarnAlarm", {
|
|
59
64
|
alarmDescription: buildAlarmDescription("ClickHouse data volume above 70% used — plan growth response", undefined),
|
|
60
65
|
metric: new Metric({
|
|
61
|
-
namespace:
|
|
62
|
-
metricName:
|
|
63
|
-
dimensionsMap: {
|
|
66
|
+
namespace: CLICKHOUSE_HOST_METRICS.namespace,
|
|
67
|
+
metricName: CLICKHOUSE_HOST_METRICS.diskMetric,
|
|
68
|
+
dimensionsMap: { [CLICKHOUSE_HOST_METRICS.asgDimension]: asgName },
|
|
64
69
|
period: Duration.minutes(15),
|
|
65
70
|
statistic: "Average"
|
|
66
71
|
}),
|
|
@@ -74,9 +79,9 @@ export function createClickHouseAlarms(props) {
|
|
|
74
79
|
const diskCriticalAlarm = new Alarm(scope, "ClickHouseDiskCriticalAlarm", {
|
|
75
80
|
alarmDescription: buildAlarmDescription("ClickHouse data volume above 85% used — imminent insert failures", undefined),
|
|
76
81
|
metric: new Metric({
|
|
77
|
-
namespace:
|
|
78
|
-
metricName:
|
|
79
|
-
dimensionsMap: {
|
|
82
|
+
namespace: CLICKHOUSE_HOST_METRICS.namespace,
|
|
83
|
+
metricName: CLICKHOUSE_HOST_METRICS.diskMetric,
|
|
84
|
+
dimensionsMap: { [CLICKHOUSE_HOST_METRICS.asgDimension]: asgName },
|
|
80
85
|
period: Duration.minutes(5),
|
|
81
86
|
statistic: "Average"
|
|
82
87
|
}),
|
|
@@ -84,7 +89,11 @@ export function createClickHouseAlarms(props) {
|
|
|
84
89
|
evaluationPeriods: 2,
|
|
85
90
|
datapointsToAlarm: 2,
|
|
86
91
|
comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD,
|
|
87
|
-
|
|
92
|
+
// BREACHING, unlike the other host alarms: a dead metrics publisher looks
|
|
93
|
+
// identical to a full disk (no datapoints either way), and this alarm's
|
|
94
|
+
// whole job is to fire before inserts start failing. The 2×5min window
|
|
95
|
+
// absorbs the boot gap on instance replacement.
|
|
96
|
+
treatMissingData: TreatMissingData.BREACHING
|
|
88
97
|
});
|
|
89
98
|
registerAlarm(diskCriticalAlarm, snsAction, alarms);
|
|
90
99
|
if (backupTaskLogGroup !== undefined) {
|
|
@@ -1,21 +1,41 @@
|
|
|
1
1
|
import { type EngineCompat } from "@fjall/util/manifest/schemas";
|
|
2
|
+
/**
|
|
3
|
+
* Normalise an npm dependency RANGE to its minimum X.Y.Z floor: strip a leading
|
|
4
|
+
* range operator (`^`, `~`, `>=`, `>`, `=`, `v`) and any prerelease/build
|
|
5
|
+
* metadata, then reconstruct `${major}.${minor}.${patch}` (missing minor/patch
|
|
6
|
+
* default to 0). Returns undefined for anything without a valid integer major —
|
|
7
|
+
* so a non-semver range (a git URL, `*`) yields no floor and the caller omits
|
|
8
|
+
* the field rather than emitting garbage.
|
|
9
|
+
*/
|
|
10
|
+
export declare function rangeFloor(range: string | undefined): string | undefined;
|
|
2
11
|
/**
|
|
3
12
|
* The constructs' own package version — the assembly's `synthesisedBy`.
|
|
4
13
|
* `undefined` only when the package layout is broken enough that the upward
|
|
5
14
|
* walk finds no matching `package.json`.
|
|
6
15
|
*/
|
|
7
16
|
export declare const CONSTRUCTS_VERSION: string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* The minimum `aws-cdk` CLI floor these constructs were blessed against, derived
|
|
19
|
+
* from their own `peerDependencies["aws-cdk"]` — the FM1 `minimumAwsCdkCli` emit
|
|
20
|
+
* value. `undefined` when the peer is absent or non-semver (the field is then
|
|
21
|
+
* omitted from the stamped block).
|
|
22
|
+
*/
|
|
23
|
+
export declare const AWS_CDK_CLI_FLOOR: string | undefined;
|
|
8
24
|
/**
|
|
9
25
|
* Pure core of the emitter: derive the `engineCompat` block from a given
|
|
10
|
-
* version string (or undefined)
|
|
11
|
-
* `${major}.0.0` non-integer-major derivation live
|
|
12
|
-
* the derivation, delegated to by both
|
|
13
|
-
* `buildEngineCompat` — so the fail-safe and
|
|
14
|
-
* unit-testable directly, without mocking
|
|
15
|
-
* when the version is absent, empty, or has
|
|
16
|
-
* the caller stamps no block).
|
|
26
|
+
* version string (or undefined) and an optional aws-cdk CLI floor. Both the
|
|
27
|
+
* absent/empty guard and the `${major}.0.0` non-integer-major derivation live
|
|
28
|
+
* here — the single source of the derivation, delegated to by both
|
|
29
|
+
* `MINIMUM_ENGINE_VERSION` and `buildEngineCompat` — so the fail-safe and
|
|
30
|
+
* malformed-version paths are unit-testable directly, without mocking
|
|
31
|
+
* module-load state. Returns undefined when the version is absent, empty, or has
|
|
32
|
+
* no valid integer major (fail-safe: the caller stamps no block).
|
|
33
|
+
*
|
|
34
|
+
* `awsCdkCliFloor` is conditional-spread: an absent/empty floor omits
|
|
35
|
+
* `minimumAwsCdkCli` entirely (Zod-pitfall-2 safe — never `undefined`), so the
|
|
36
|
+
* FM1 check degrades to no-op rather than shipping a malformed constraint.
|
|
17
37
|
*/
|
|
18
|
-
export declare function buildEngineCompatFrom(version: string | undefined): EngineCompat | undefined;
|
|
38
|
+
export declare function buildEngineCompatFrom(version: string | undefined, awsCdkCliFloor?: string | undefined): EngineCompat | undefined;
|
|
19
39
|
/**
|
|
20
40
|
* The compatible deploy-engine floor for assemblies this constructs version
|
|
21
41
|
* synthesises: `${major}.0.0`, derived from `CONSTRUCTS_VERSION`. `undefined`
|
|
@@ -28,10 +48,12 @@ export declare const MINIMUM_ENGINE_VERSION: string | undefined;
|
|
|
28
48
|
* when the constructs' own version cannot be resolved (fail-safe: the caller
|
|
29
49
|
* stamps no block and the deploy degrades to a constraint-free proceed).
|
|
30
50
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
51
|
+
* Emits the two required fields plus `minimumAwsCdkCli` (Phase 2 FM1) whenever the
|
|
52
|
+
* aws-cdk peer floor resolves. The verdict classification of the emitted floors is
|
|
53
|
+
* proven in `@fjall/deploy-core`'s
|
|
54
|
+
* `orchestration/application/__tests__/engineCompat.test.ts` (a `4.0.0` engine
|
|
55
|
+
* floor refuses a 3.x engine; a `minimumAwsCdkCli` floor refuses an older bundled
|
|
56
|
+
* CLI); this module's tests prove the emitted shape matches those floors and reads
|
|
57
|
+
* back through the gate's reader.
|
|
36
58
|
*/
|
|
37
59
|
export declare function buildEngineCompat(): EngineCompat | undefined;
|
|
@@ -26,10 +26,21 @@
|
|
|
26
26
|
* hand-edit. A same-major-or-newer engine proceeds; an older-major engine
|
|
27
27
|
* hard-refuses.
|
|
28
28
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* `minimumAwsCdkCli` (Phase 2, FM1) is the floor for the deploy engine's bundled
|
|
30
|
+
* `aws-cdk` CLI: an engine whose CLI is older than the version these constructs
|
|
31
|
+
* were blessed against cannot reliably read the cloud assembly they synthesised.
|
|
32
|
+
* The aws-cdk CLI and aws-cdk-lib version axes DIVERGED in CDK v2 (CLI ~2.1134.x
|
|
33
|
+
* vs lib ~2.262.x), so this is a distinct check from `minimumEngineVersion`, on a
|
|
34
|
+
* different axis. It is derived from the constructs' OWN
|
|
35
|
+
* `peerDependencies["aws-cdk"]` floor — self-honest, and shipped with the package
|
|
36
|
+
* (unlike the blessed-pin JSON, which is not in `files`) — and conditional-spread
|
|
37
|
+
* so a package whose peer floor cannot be resolved simply omits it (fail-safe: the
|
|
38
|
+
* FM1 check degrades to no-op rather than emitting a garbage floor).
|
|
39
|
+
*
|
|
40
|
+
* The optional `maximumEngineMajor` bound is deliberately NOT emitted — it is ADR
|
|
41
|
+
* open question Q5 (engine-ahead cap), and capping a newer engine would break the
|
|
42
|
+
* normal fjall-upgrade-then-redeploy path. The classifier still honours one if a
|
|
43
|
+
* future assembly stamps it by hand; this emitter never does.
|
|
33
44
|
*
|
|
34
45
|
* Fail-safe: if the constructs' own version cannot be resolved (a broken
|
|
35
46
|
* package layout), `buildEngineCompat` returns undefined and the caller stamps
|
|
@@ -43,7 +54,41 @@ import { existsSync, readFileSync } from "fs";
|
|
|
43
54
|
import { getErrorMessage, maskSensitiveOutput } from "@fjall/util";
|
|
44
55
|
import { FjallLogger } from "./validationLogger.js";
|
|
45
56
|
const PACKAGE_NAME = "@fjall/components-infrastructure";
|
|
46
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Normalise an npm dependency RANGE to its minimum X.Y.Z floor: strip a leading
|
|
59
|
+
* range operator (`^`, `~`, `>=`, `>`, `=`, `v`) and any prerelease/build
|
|
60
|
+
* metadata, then reconstruct `${major}.${minor}.${patch}` (missing minor/patch
|
|
61
|
+
* default to 0). Returns undefined for anything without a valid integer major —
|
|
62
|
+
* so a non-semver range (a git URL, `*`) yields no floor and the caller omits
|
|
63
|
+
* the field rather than emitting garbage.
|
|
64
|
+
*/
|
|
65
|
+
export function rangeFloor(range) {
|
|
66
|
+
if (range === undefined || range.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
const firstToken = range.trim().split(/\s+/)[0] ?? "";
|
|
69
|
+
const core = firstToken.replace(/^[\^~>=v ]+/i, "").split(/[-+]/)[0] ?? "";
|
|
70
|
+
if (core.length === 0)
|
|
71
|
+
return undefined;
|
|
72
|
+
const parts = core.split(".");
|
|
73
|
+
const major = Number(parts[0]);
|
|
74
|
+
const minor = parts.length > 1 ? Number(parts[1]) : 0;
|
|
75
|
+
const patch = parts.length > 2 ? Number(parts[2]) : 0;
|
|
76
|
+
if (!Number.isInteger(major) || major < 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
if (!Number.isInteger(minor) || minor < 0)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (!Number.isInteger(patch) || patch < 0)
|
|
81
|
+
return undefined;
|
|
82
|
+
return `${major}.${minor}.${patch}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the constructs' own package.json (version + aws-cdk peer floor) by the
|
|
86
|
+
* same upward `name`-matching walk `DEPLOY_CORE_VERSION` uses — robust across the
|
|
87
|
+
* source (`lib/utils/`) and compiled (`dist/lib/utils/`) layouts, and skipping any
|
|
88
|
+
* unrelated `package.json` higher up (a consuming app's). Returns undefined only
|
|
89
|
+
* when the walk finds no matching `package.json` (broken layout).
|
|
90
|
+
*/
|
|
91
|
+
function resolveConstructsPackageMeta() {
|
|
47
92
|
try {
|
|
48
93
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
49
94
|
const { root } = parse(dir);
|
|
@@ -54,7 +99,13 @@ function resolveConstructsVersion() {
|
|
|
54
99
|
if (pkg.name === PACKAGE_NAME &&
|
|
55
100
|
typeof pkg.version === "string" &&
|
|
56
101
|
pkg.version.length > 0) {
|
|
57
|
-
|
|
102
|
+
const awsCdkPeer = pkg.peerDependencies?.["aws-cdk"];
|
|
103
|
+
return {
|
|
104
|
+
version: pkg.version,
|
|
105
|
+
awsCdkCliFloor: typeof awsCdkPeer === "string"
|
|
106
|
+
? rangeFloor(awsCdkPeer)
|
|
107
|
+
: undefined
|
|
108
|
+
};
|
|
58
109
|
}
|
|
59
110
|
}
|
|
60
111
|
if (dir === root)
|
|
@@ -63,28 +114,40 @@ function resolveConstructsVersion() {
|
|
|
63
114
|
}
|
|
64
115
|
}
|
|
65
116
|
catch (error) {
|
|
66
|
-
FjallLogger.warn(`Could not resolve ${PACKAGE_NAME}
|
|
117
|
+
FjallLogger.warn(`Could not resolve ${PACKAGE_NAME} metadata for engineCompat: ` +
|
|
67
118
|
maskSensitiveOutput(getErrorMessage(error)));
|
|
68
119
|
return undefined;
|
|
69
120
|
}
|
|
70
121
|
}
|
|
122
|
+
const CONSTRUCTS_PACKAGE_META = resolveConstructsPackageMeta();
|
|
71
123
|
/**
|
|
72
124
|
* The constructs' own package version — the assembly's `synthesisedBy`.
|
|
73
125
|
* `undefined` only when the package layout is broken enough that the upward
|
|
74
126
|
* walk finds no matching `package.json`.
|
|
75
127
|
*/
|
|
76
|
-
export const CONSTRUCTS_VERSION =
|
|
128
|
+
export const CONSTRUCTS_VERSION = CONSTRUCTS_PACKAGE_META?.version;
|
|
129
|
+
/**
|
|
130
|
+
* The minimum `aws-cdk` CLI floor these constructs were blessed against, derived
|
|
131
|
+
* from their own `peerDependencies["aws-cdk"]` — the FM1 `minimumAwsCdkCli` emit
|
|
132
|
+
* value. `undefined` when the peer is absent or non-semver (the field is then
|
|
133
|
+
* omitted from the stamped block).
|
|
134
|
+
*/
|
|
135
|
+
export const AWS_CDK_CLI_FLOOR = CONSTRUCTS_PACKAGE_META?.awsCdkCliFloor;
|
|
77
136
|
/**
|
|
78
137
|
* Pure core of the emitter: derive the `engineCompat` block from a given
|
|
79
|
-
* version string (or undefined)
|
|
80
|
-
* `${major}.0.0` non-integer-major derivation live
|
|
81
|
-
* the derivation, delegated to by both
|
|
82
|
-
* `buildEngineCompat` — so the fail-safe and
|
|
83
|
-
* unit-testable directly, without mocking
|
|
84
|
-
* when the version is absent, empty, or has
|
|
85
|
-
* the caller stamps no block).
|
|
138
|
+
* version string (or undefined) and an optional aws-cdk CLI floor. Both the
|
|
139
|
+
* absent/empty guard and the `${major}.0.0` non-integer-major derivation live
|
|
140
|
+
* here — the single source of the derivation, delegated to by both
|
|
141
|
+
* `MINIMUM_ENGINE_VERSION` and `buildEngineCompat` — so the fail-safe and
|
|
142
|
+
* malformed-version paths are unit-testable directly, without mocking
|
|
143
|
+
* module-load state. Returns undefined when the version is absent, empty, or has
|
|
144
|
+
* no valid integer major (fail-safe: the caller stamps no block).
|
|
145
|
+
*
|
|
146
|
+
* `awsCdkCliFloor` is conditional-spread: an absent/empty floor omits
|
|
147
|
+
* `minimumAwsCdkCli` entirely (Zod-pitfall-2 safe — never `undefined`), so the
|
|
148
|
+
* FM1 check degrades to no-op rather than shipping a malformed constraint.
|
|
86
149
|
*/
|
|
87
|
-
export function buildEngineCompatFrom(version) {
|
|
150
|
+
export function buildEngineCompatFrom(version, awsCdkCliFloor) {
|
|
88
151
|
if (version === undefined || version.length === 0)
|
|
89
152
|
return undefined;
|
|
90
153
|
const major = Number(version.split(".")[0]);
|
|
@@ -92,7 +155,9 @@ export function buildEngineCompatFrom(version) {
|
|
|
92
155
|
return undefined;
|
|
93
156
|
return {
|
|
94
157
|
synthesisedBy: version,
|
|
95
|
-
minimumEngineVersion: `${major}.0.0
|
|
158
|
+
minimumEngineVersion: `${major}.0.0`,
|
|
159
|
+
...(awsCdkCliFloor !== undefined &&
|
|
160
|
+
awsCdkCliFloor.length > 0 && { minimumAwsCdkCli: awsCdkCliFloor })
|
|
96
161
|
};
|
|
97
162
|
}
|
|
98
163
|
/**
|
|
@@ -107,12 +172,14 @@ export const MINIMUM_ENGINE_VERSION = buildEngineCompatFrom(CONSTRUCTS_VERSION)?
|
|
|
107
172
|
* when the constructs' own version cannot be resolved (fail-safe: the caller
|
|
108
173
|
* stamps no block and the deploy degrades to a constraint-free proceed).
|
|
109
174
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
175
|
+
* Emits the two required fields plus `minimumAwsCdkCli` (Phase 2 FM1) whenever the
|
|
176
|
+
* aws-cdk peer floor resolves. The verdict classification of the emitted floors is
|
|
177
|
+
* proven in `@fjall/deploy-core`'s
|
|
178
|
+
* `orchestration/application/__tests__/engineCompat.test.ts` (a `4.0.0` engine
|
|
179
|
+
* floor refuses a 3.x engine; a `minimumAwsCdkCli` floor refuses an older bundled
|
|
180
|
+
* CLI); this module's tests prove the emitted shape matches those floors and reads
|
|
181
|
+
* back through the gate's reader.
|
|
115
182
|
*/
|
|
116
183
|
export function buildEngineCompat() {
|
|
117
|
-
return buildEngineCompatFrom(CONSTRUCTS_VERSION);
|
|
184
|
+
return buildEngineCompatFrom(CONSTRUCTS_VERSION, AWS_CDK_CLI_FLOOR);
|
|
118
185
|
}
|