@fjall/components-infrastructure 16.0.1 → 17.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/clickhouseDatabase.d.ts +103 -8
- package/dist/lib/patterns/aws/clickhouseDatabase.js +51 -12
- package/dist/lib/patterns/aws/database.d.ts +1 -1
- package/dist/lib/patterns/aws/database.js +1 -1
- package/dist/lib/resources/aws/database/clickhouseBackupScript.d.ts +103 -0
- package/dist/lib/resources/aws/database/clickhouseBackupScript.js +123 -0
- package/dist/lib/resources/aws/database/clickhouseConstants.d.ts +132 -20
- package/dist/lib/resources/aws/database/clickhouseConstants.js +126 -15
- package/dist/lib/resources/aws/database/clickhouseSchemas.d.ts +33 -1
- package/dist/lib/resources/aws/database/clickhouseSchemas.js +31 -0
- package/dist/lib/resources/aws/database/clickhouseStorage.d.ts +58 -0
- package/dist/lib/resources/aws/database/clickhouseStorage.js +87 -0
- package/dist/lib/resources/aws/database/clickhouseTuning.d.ts +5 -6
- package/dist/lib/resources/aws/database/clickhouseTuning.js +39 -16
- package/dist/lib/resources/aws/database/clickhouseUserData.d.ts +11 -0
- package/dist/lib/resources/aws/database/clickhouseUserData.js +22 -6
- package/dist/lib/resources/aws/database/clickhouseXmlRenderer.js +22 -1
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.d.ts +54 -5
- package/dist/lib/resources/aws/monitoring/clickhouseAlarms.js +106 -10
- package/package.json +3 -3
|
@@ -29,15 +29,82 @@ export interface ClickHouseDatabaseProps {
|
|
|
29
29
|
* taking a new type live needs a launch-template refresh + instance
|
|
30
30
|
* refresh + ClickHouse restart, not a config reload.
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
32
|
+
* Changing this on a DEPLOYED stack keeps the data. The volume's logical
|
|
33
|
+
* ID and its `fjall:OwnerLogicalId` re-attach tag both derive from the
|
|
34
|
+
* capacity ANCHOR (`resolveCapacityAnchor`, `${Aws.STACK_NAME}/${anchor}/
|
|
35
|
+
* PersistentDataVolume`), not from the instance type, so neither moves
|
|
36
|
+
* when the type does and the re-attach chain holds. Config-keyed identity
|
|
37
|
+
* was the pre-6.0 derivation — `getEc2ConfigKey` survives only to emit
|
|
38
|
+
* the manifest's legacy alias.
|
|
39
|
+
*
|
|
40
|
+
* It is still not free: the type lives in the launch template and in
|
|
41
|
+
* user-data-baked server tuning, so it takes an instance refresh, and on
|
|
42
|
+
* this single-node cluster that refresh is a restart. The ASG terminates
|
|
43
|
+
* the instance (TERMINATING hook detaches the volume), launches a
|
|
44
|
+
* replacement, and the LAUNCHING hook re-attaches — ClickHouse is
|
|
45
|
+
* unavailable across that window. Size the change into a quiet period;
|
|
46
|
+
* do not plan a snapshot-restore migration for it. */
|
|
40
47
|
instanceType?: string;
|
|
48
|
+
/**
|
|
49
|
+
* EBS data-volume size in GiB. Default `CLICKHOUSE_EBS_VOLUME_SIZE_GB`
|
|
50
|
+
* (400). Integer, between `CLICKHOUSE_MIN_STORAGE_GB` and the gp3 ceiling;
|
|
51
|
+
* validated against `alarms.diskFreeCriticalGib` as a pair, because a
|
|
52
|
+
* volume smaller than five times that floor would deploy a critical alarm
|
|
53
|
+
* that can never leave ALARM.
|
|
54
|
+
*
|
|
55
|
+
* GROWTH ONLY, and the growth is not instantaneous. EBS `ModifyVolume`
|
|
56
|
+
* refuses to shrink a volume, so a decrease fails mid-update and rolls the
|
|
57
|
+
* stack back; it also permits one modification per volume per 6 hours, so
|
|
58
|
+
* a second bump inside that window fails until the cooldown elapses.
|
|
59
|
+
*
|
|
60
|
+
* Resizing the block device is half the job — the ext4 filesystem on it
|
|
61
|
+
* does not follow until `resize2fs` runs, which is a step in
|
|
62
|
+
* `buildClickHouseUserData` and therefore runs at BOOT. The declared size
|
|
63
|
+
* is embedded in that user data, so changing this prop versions the launch
|
|
64
|
+
* template and triggers the ASG instance refresh whose boot grows the
|
|
65
|
+
* filesystem — the increase applies in the same deploy, by the same
|
|
66
|
+
* mechanism an `instanceType` change rides. On this single-node cluster
|
|
67
|
+
* that relaunch is a restart (the TERMINATING hook detaches the volume,
|
|
68
|
+
* LAUNCHING re-attaches it): the data survives; the cluster is unavailable
|
|
69
|
+
* across the window. Size increases into a quiet period, exactly as with
|
|
70
|
+
* `instanceType`.
|
|
71
|
+
*/
|
|
72
|
+
storageGb?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Provisioned IOPS on the gp3 data volume. Default `CLICKHOUSE_EBS_IOPS`
|
|
75
|
+
* (3000, the baseline the volume price includes). Integer, between
|
|
76
|
+
* `CLICKHOUSE_MIN_IOPS` and `CLICKHOUSE_MAX_IOPS`, and at most
|
|
77
|
+
* `CLICKHOUSE_MAX_IOPS_PER_GB` × `storageGb` — EBS rejects more IOPS than
|
|
78
|
+
* the volume is large enough to carry. Validated with `throughputMbps` as
|
|
79
|
+
* a pair, because throughput needs IOPS to ride on (below).
|
|
80
|
+
*
|
|
81
|
+
* The same knob RDS exposes as provisioned IOPS on gp3 storage. Applied by
|
|
82
|
+
* EBS `ModifyVolume` in place: no launch-template change, no instance
|
|
83
|
+
* relaunch, no restart — the cluster stays up. It shares the
|
|
84
|
+
* one-modification-per-volume-per-6-hours cooldown with `storageGb` and
|
|
85
|
+
* `throughputMbps`; a change to any of the three inside that window fails
|
|
86
|
+
* until it elapses, so move them together.
|
|
87
|
+
*
|
|
88
|
+
* Worth raising only once the host can use it: a volume's provisioned
|
|
89
|
+
* figure is a ceiling the instance's own EBS bandwidth may sit under, so
|
|
90
|
+
* pair a large value with an `instanceType` whose EBS bandwidth reaches it.
|
|
91
|
+
*/
|
|
92
|
+
iops?: number;
|
|
93
|
+
/**
|
|
94
|
+
* Provisioned throughput on the gp3 data volume, in MiB/s. Default
|
|
95
|
+
* `CLICKHOUSE_EBS_THROUGHPUT_MBPS` (125, the baseline the volume price
|
|
96
|
+
* includes). Integer, between `CLICKHOUSE_MIN_THROUGHPUT_MBPS` and
|
|
97
|
+
* `CLICKHOUSE_MAX_THROUGHPUT_MBPS`, and needing
|
|
98
|
+
* `CLICKHOUSE_IOPS_PER_THROUGHPUT_MBPS` (4) IOPS per MiB/s — 1000 MiB/s
|
|
99
|
+
* needs `iops: 4000` or more, and the synth error names the figure.
|
|
100
|
+
*
|
|
101
|
+
* The same knob RDS exposes as storage throughput on gp3. Applied in place
|
|
102
|
+
* by `ModifyVolume` like `iops`, under the same shared 6-hour cooldown, and
|
|
103
|
+
* likewise bounded by the instance's EBS bandwidth. The restore-verify
|
|
104
|
+
* timeout is derived from the baseline rather than this value on purpose
|
|
105
|
+
* (see `clickHouseBackupVerifyTimeoutSeconds`).
|
|
106
|
+
*/
|
|
107
|
+
throughputMbps?: number;
|
|
41
108
|
/**
|
|
42
109
|
* ECS service desired task count. Defaults to 1 (single-node, single-task
|
|
43
110
|
* by design). Override to 0 via the `clickhouseDesiredCount` CDK context
|
|
@@ -57,6 +124,12 @@ export interface ClickHouseDatabaseProps {
|
|
|
57
124
|
* - omitted/undefined: cold tier ENABLED (S3 bucket provisioned).
|
|
58
125
|
* - `false`: cold tier DISABLED (single-tier hot storage on EBS only).
|
|
59
126
|
* - `{ mode: "s3" }`: cold tier ENABLED (explicit).
|
|
127
|
+
*
|
|
128
|
+
* Interacts with the backup restore-verify: the verify restores onto the
|
|
129
|
+
* EBS volume, so a cold-heavy database can outgrow the free-space gate
|
|
130
|
+
* long before the hot tier fills — from that day the verify skips (and its
|
|
131
|
+
* alarm pages daily) with no cure but a larger `storageGb`. When that
|
|
132
|
+
* trade is deliberate, `backupVerify: false` is the sanctioned off-switch.
|
|
60
133
|
*/
|
|
61
134
|
coldTier?: false | {
|
|
62
135
|
mode: "s3";
|
|
@@ -85,10 +158,32 @@ export interface ClickHouseDatabaseProps {
|
|
|
85
158
|
* schedules sparser than ~5.8 days must disable the heartbeat.
|
|
86
159
|
*/
|
|
87
160
|
backupSchedule?: string | false;
|
|
161
|
+
/**
|
|
162
|
+
* Restore-verify on each scheduled backup. Omitted (the default): after
|
|
163
|
+
* `BACKUP DATABASE … TO S3` returns, the task restores what it just wrote
|
|
164
|
+
* into a scratch database, compares the table set, and emits a verdict
|
|
165
|
+
* marker the verify alarms read — the only signal that the backups are
|
|
166
|
+
* actually restorable. `false` disables the verify (the backup itself
|
|
167
|
+
* still runs and the backup-failure alarm + success heartbeat remain);
|
|
168
|
+
* the verify alarms are withheld with it, so disabling never reads as a
|
|
169
|
+
* silent green. Reach for it when the restore cost is structurally
|
|
170
|
+
* unaffordable on-box — a cold-heavy dataset (see `coldTier`), or a
|
|
171
|
+
* volume deliberately sized tight — and accept that `BACKUP_CREATED`
|
|
172
|
+
* becomes the only evidence a backup exists.
|
|
173
|
+
*/
|
|
174
|
+
backupVerify?: false;
|
|
88
175
|
/**
|
|
89
176
|
* Backup-bucket lifecycle expiration in days for both current and noncurrent
|
|
90
177
|
* versions. Default: `BACKUP_RETENTION_DAYS` from `clickhouseConstants.ts`.
|
|
91
178
|
* Ignored when `backupBucket` is supplied (caller-owned lifecycle).
|
|
179
|
+
* Must exceed the `backupSchedule` interval — a retention shorter than the
|
|
180
|
+
* gap between runs expires each backup before its successor exists, which
|
|
181
|
+
* is a window with no backup at all rather than a short history.
|
|
182
|
+
*
|
|
183
|
+
* An integer between `CLICKHOUSE_MIN_BACKUP_RETENTION_DAYS` and
|
|
184
|
+
* `CLICKHOUSE_MAX_BACKUP_RETENTION_DAYS`. This is an S3 object lifetime and
|
|
185
|
+
* deliberately does NOT share the RDS 35-day ceiling, which bounds a
|
|
186
|
+
* different mechanism (`backupRetention` on the relational types).
|
|
92
187
|
*/
|
|
93
188
|
backupRetentionDays?: number;
|
|
94
189
|
/**
|
|
@@ -23,7 +23,10 @@ import { createClickHouseAlarms, validateClickHouseAlarmThresholds, BACKUP_HEART
|
|
|
23
23
|
import { ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, PROFILE_NAME_PATTERN } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
24
24
|
import { deriveClickHouseDefaultProfiles } from "../../resources/aws/database/clickhouseTuning.js";
|
|
25
25
|
import { inferAmiHardwareType } from "../../resources/aws/compute/ecsConstants.js";
|
|
26
|
-
import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE,
|
|
26
|
+
import { CLICKHOUSE_DATABASE_NAME, DEFAULT_CLICKHOUSE_INSTANCE_TYPE, CLICKHOUSE_IMAGE, 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, CLICKHOUSE_SERVICE_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";
|
|
27
|
+
import { CLICKHOUSE_MAX_BACKUP_RETENTION_DAYS, CLICKHOUSE_MIN_BACKUP_RETENTION_DAYS } from "@fjall/util/clickhouse";
|
|
28
|
+
import { resolveClickHouseStorage, resolveClickHouseVolumePerformance } from "../../resources/aws/database/clickhouseStorage.js";
|
|
29
|
+
import { buildClickHouseBackupScript } from "../../resources/aws/database/clickhouseBackupScript.js";
|
|
27
30
|
import { TlsCertGenerator } from "../../resources/aws/utilities/tlsCertGenerator.js";
|
|
28
31
|
import { EcsCompute } from "./computeEcs.js";
|
|
29
32
|
/**
|
|
@@ -263,6 +266,7 @@ export class ClickHouseDatabase extends Construct {
|
|
|
263
266
|
const desiredCount = resolveClickHouseDesiredCount(this.node.tryGetContext("clickhouseDesiredCount"), props.desiredCount);
|
|
264
267
|
const optimiseEnabled = props.optimiseSchedule !== false;
|
|
265
268
|
const backupEnabled = props.backupSchedule !== false;
|
|
269
|
+
const backupVerifyEnabled = props.backupVerify !== false;
|
|
266
270
|
const optimiseSchedule = typeof props.optimiseSchedule === "string"
|
|
267
271
|
? props.optimiseSchedule
|
|
268
272
|
: OPTIMISE_FINAL_SCHEDULE;
|
|
@@ -270,9 +274,15 @@ export class ClickHouseDatabase extends Construct {
|
|
|
270
274
|
? props.backupSchedule
|
|
271
275
|
: BACKUP_SCHEDULE;
|
|
272
276
|
const backupRetentionDays = props.backupRetentionDays ?? BACKUP_RETENTION_DAYS;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
277
|
+
// Checked after defaulting, so the default is held to the same bound as a
|
|
278
|
+
// caller's value. Integer-ness is part of the bound rather than a separate
|
|
279
|
+
// nicety: the value becomes `Duration.days()` on an S3 lifecycle rule,
|
|
280
|
+
// which rejects a fraction far downstream with no mention of the knob that
|
|
281
|
+
// supplied it.
|
|
282
|
+
if (!Number.isInteger(backupRetentionDays) ||
|
|
283
|
+
backupRetentionDays < CLICKHOUSE_MIN_BACKUP_RETENTION_DAYS ||
|
|
284
|
+
backupRetentionDays > CLICKHOUSE_MAX_BACKUP_RETENTION_DAYS) {
|
|
285
|
+
throw new Error(`ClickHouseDatabase: backupRetentionDays must be an integer between ${CLICKHOUSE_MIN_BACKUP_RETENTION_DAYS} and ${CLICKHOUSE_MAX_BACKUP_RETENTION_DAYS} days; got ${backupRetentionDays}.`);
|
|
276
286
|
}
|
|
277
287
|
const tlsOptions = props.tls ?? {
|
|
278
288
|
mode: "self-signed"
|
|
@@ -348,10 +358,29 @@ export class ClickHouseDatabase extends Construct {
|
|
|
348
358
|
retention: RetentionDays.TWO_WEEKS
|
|
349
359
|
})
|
|
350
360
|
: undefined;
|
|
361
|
+
// Resolved BEFORE the user data is built: the declared volume size is
|
|
362
|
+
// embedded into the script so a storageGb change versions the launch
|
|
363
|
+
// template (see `dataVolumeSizeGb` on BuildClickHouseUserDataOptions).
|
|
364
|
+
// The restore-verify gate is bounded by the same free-space floor the
|
|
365
|
+
// disk alarm pages on — the check that protects the data must not be
|
|
366
|
+
// able to fill the volume it is protecting. Volume size and floor are
|
|
367
|
+
// only checkable against each other, and the floor lives on the alarms
|
|
368
|
+
// knob.
|
|
369
|
+
const alarmThresholds = typeof props.alarms === "object" ? props.alarms : {};
|
|
370
|
+
validateClickHouseAlarmThresholds(alarmThresholds);
|
|
371
|
+
const { storageGb, diskFreeCriticalGib } = resolveClickHouseStorage(props.storageGb, alarmThresholds.diskFreeCriticalGib);
|
|
372
|
+
// IOPS and throughput are bounded by each other and by the size just
|
|
373
|
+
// resolved, so they resolve second, against the validated storageGb.
|
|
374
|
+
const { iops, throughputMbps } = resolveClickHouseVolumePerformance({
|
|
375
|
+
storageGb,
|
|
376
|
+
iops: props.iops,
|
|
377
|
+
throughputMbps: props.throughputMbps
|
|
378
|
+
});
|
|
351
379
|
const userData = UserData.custom(buildClickHouseUserData({
|
|
352
380
|
backupBucketName: backupBucket.bucketName,
|
|
353
381
|
backupBucketRegion: Stack.of(this).region,
|
|
354
382
|
instanceType,
|
|
383
|
+
dataVolumeSizeGb: storageGb,
|
|
355
384
|
...(coldTierBucket !== undefined && {
|
|
356
385
|
coldTier: {
|
|
357
386
|
bucketName: coldTierBucket.bucketName,
|
|
@@ -405,12 +434,15 @@ export class ClickHouseDatabase extends Construct {
|
|
|
405
434
|
// deploy-order window, or a dropped table lingering in the constants)
|
|
406
435
|
// would permanently skip every optimise after it. Failures are logged and
|
|
407
436
|
// the task still exits non-zero so they stay visible in the task history.
|
|
408
|
-
|
|
437
|
+
// Both scheduled tasks authenticate identically; the password arrives via
|
|
438
|
+
// CLICKHOUSE_PASSWORD rather than argv.
|
|
439
|
+
const clickHouseClient = `clickhouse-client --host ${clickHouseHost} --port ${nativePort} --user ${schemaAdmin.name}${sidecarTlsClientArgs}`;
|
|
409
440
|
const optimiseScript = [
|
|
410
441
|
"FAILED=0",
|
|
411
|
-
...optimiseStatements.map((stmt) => `${
|
|
442
|
+
...optimiseStatements.map((stmt) => `${clickHouseClient} --query "${stmt}" || { echo "optimise failed: ${stmt}"; FAILED=1; }`),
|
|
412
443
|
'exit "$FAILED"'
|
|
413
444
|
].join("; ");
|
|
445
|
+
// Resolved before the scheduled tasks because the backup task's
|
|
414
446
|
const scheduledTasks = [];
|
|
415
447
|
if (optimiseEnabled) {
|
|
416
448
|
scheduledTasks.push({
|
|
@@ -439,7 +471,14 @@ export class ClickHouseDatabase extends Construct {
|
|
|
439
471
|
"sh",
|
|
440
472
|
"-c",
|
|
441
473
|
// Password via CLICKHOUSE_PASSWORD env, not --password on argv (argv → /proc/<pid>/cmdline).
|
|
442
|
-
`${sidecarTlsPreamble}
|
|
474
|
+
`${sidecarTlsPreamble}${buildClickHouseBackupScript({
|
|
475
|
+
client: clickHouseClient,
|
|
476
|
+
databaseName: CLICKHOUSE_DATABASE_NAME,
|
|
477
|
+
backupDestUrl,
|
|
478
|
+
diskFreeCriticalGib,
|
|
479
|
+
storageGb,
|
|
480
|
+
verify: backupVerifyEnabled
|
|
481
|
+
})}`
|
|
443
482
|
],
|
|
444
483
|
secrets: {
|
|
445
484
|
CLICKHOUSE_PASSWORD: EcsSecret.fromSecretsManager(adminSecret.secret, "password"),
|
|
@@ -459,8 +498,6 @@ export class ClickHouseDatabase extends Construct {
|
|
|
459
498
|
const alarmsDisabled = props.alarms === false;
|
|
460
499
|
const applicationId = props.applicationId ??
|
|
461
500
|
(App.hasInstance() ? App.getInstance().getName() : undefined);
|
|
462
|
-
const alarmThresholds = typeof props.alarms === "object" ? props.alarms : {};
|
|
463
|
-
validateClickHouseAlarmThresholds(alarmThresholds);
|
|
464
501
|
const serviceLogAlarms = alarmsDisabled
|
|
465
502
|
? undefined
|
|
466
503
|
: buildClickHouseLogAlarmSpecs(alarmThresholds, coldTierEnabled);
|
|
@@ -520,11 +557,11 @@ export class ClickHouseDatabase extends Construct {
|
|
|
520
557
|
instanceMonitoring: Monitoring.BASIC,
|
|
521
558
|
availabilityZones: [dataAz],
|
|
522
559
|
persistentDataVolume: {
|
|
523
|
-
sizeGb:
|
|
560
|
+
sizeGb: storageGb,
|
|
524
561
|
deviceName: CLICKHOUSE_EBS_DEVICE_NAME,
|
|
525
562
|
availabilityZone: dataAz,
|
|
526
|
-
iops
|
|
527
|
-
throughputMbps
|
|
563
|
+
iops,
|
|
564
|
+
throughputMbps,
|
|
528
565
|
...(resolvedAlertsTopic !== undefined && {
|
|
529
566
|
alarmTopic: resolvedAlertsTopic
|
|
530
567
|
})
|
|
@@ -658,6 +695,8 @@ export class ClickHouseDatabase extends Construct {
|
|
|
658
695
|
asgName,
|
|
659
696
|
alarmTopic: resolvedAlertsTopic,
|
|
660
697
|
config: alarmThresholds,
|
|
698
|
+
diskFreeCriticalGib,
|
|
699
|
+
backupVerifyEnabled,
|
|
661
700
|
...(applicationId !== undefined && { applicationId }),
|
|
662
701
|
...(backupTaskLogGroup !== undefined && { backupTaskLogGroup })
|
|
663
702
|
});
|
|
@@ -332,7 +332,7 @@ export declare class RelationalDatabase extends Construct implements IRelational
|
|
|
332
332
|
grantIamConnect(grantee: IGrantable, dbUsername: string): Grant;
|
|
333
333
|
}
|
|
334
334
|
export { ClickHouseDatabase, type ClickHouseDatabaseProps, type ClickHouseAlarmThresholds };
|
|
335
|
-
export { ClickHouseDefaultProfiles, ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, type ClickHouseSchemaAdmin, type ManagedPasswordName, type ProfileSpec } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
335
|
+
export { ClickHouseDefaultProfiles, ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, SettingConstraintSchema, type ClickHouseSchemaAdmin, type FrozenProfileSpec, type ManagedPasswordName, type ProfileSpec, type SettingConstraint } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
336
336
|
export { renderUsersXml } from "../../resources/aws/database/clickhouseXmlRenderer.js";
|
|
337
337
|
export type { RenderUsersXmlOptions } from "../../resources/aws/database/clickhouseXmlRenderer.js";
|
|
338
338
|
export type { DynamoDBKeySchema, DynamoDBGlobalSecondaryIndex, DynamoDBTableProps } from "../../resources/aws/database/dynamodb.js";
|
|
@@ -655,6 +655,6 @@ export class RelationalDatabase extends Construct {
|
|
|
655
655
|
}
|
|
656
656
|
}
|
|
657
657
|
export { ClickHouseDatabase };
|
|
658
|
-
export { ClickHouseDefaultProfiles, ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
658
|
+
export { ClickHouseDefaultProfiles, ClickHouseSchemaAdminSchema, ManagedPasswordNameSchema, ProfileSpecSchema, SettingConstraintSchema } from "../../resources/aws/database/clickhouseSchemas.js";
|
|
659
659
|
export { renderUsersXml } from "../../resources/aws/database/clickhouseXmlRenderer.js";
|
|
660
660
|
export { isRelationalDatabase, isDynamoDBDatabase, isClickHouseDatabase, isAuroraDatabase, isInstanceDatabase, isGlobalAuroraDatabase } from "./interfaces/database.js";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backup task's shell body: take the backup, then prove it restores.
|
|
3
|
+
*
|
|
4
|
+
* `BACKUP DATABASE … TO S3(…)` returning `BACKUP_CREATED` means ClickHouse
|
|
5
|
+
* finished writing and the statement did not raise. It does NOT mean the
|
|
6
|
+
* bytes it wrote can be read back — and the gap between those two is the
|
|
7
|
+
* whole reason a backup exists. So the task restores what it just wrote,
|
|
8
|
+
* into a scratch database, and says so in its own log line.
|
|
9
|
+
*
|
|
10
|
+
* The restore is a FULL one, deliberately. `SETTINGS structure_only = 1`
|
|
11
|
+
* looks like the cheap version of this check and is not a version of it at
|
|
12
|
+
* all: on 26.3.17.56 a structure-only restore of a backup whose entire
|
|
13
|
+
* `data/` tree had been deleted returned `RESTORED` and exit 0, because it
|
|
14
|
+
* reads `metadata/` and never opens a part. A verification that passes on a
|
|
15
|
+
* backup with no data in it is worse than no verification — it converts an
|
|
16
|
+
* unrestorable backup into a green signal. The full restore fails that same
|
|
17
|
+
* case loudly (`STD_EXCEPTION`, missing `checksums.txt`), which is the
|
|
18
|
+
* behaviour the alarm is built on.
|
|
19
|
+
*/
|
|
20
|
+
export interface ClickHouseBackupScriptParams {
|
|
21
|
+
/** Fully-formed `clickhouse-client …` invocation, minus `--query`. */
|
|
22
|
+
readonly client: string;
|
|
23
|
+
/** Database the backup covers, and the restore compares against. */
|
|
24
|
+
readonly databaseName: string;
|
|
25
|
+
/** `https://<bucket>.s3.<region>.amazonaws.com/backup/` — trailing slash. */
|
|
26
|
+
readonly backupDestUrl: string;
|
|
27
|
+
/** Critical free-space floor in GiB; the verify gate keeps twice this
|
|
28
|
+
* clear so the check can never be the thing that pages. */
|
|
29
|
+
readonly diskFreeCriticalGib: number;
|
|
30
|
+
/** Declared data-volume size in GiB; bounds the verify restore's timeout
|
|
31
|
+
* via `clickHouseBackupVerifyTimeoutSeconds`. */
|
|
32
|
+
readonly storageGb: number;
|
|
33
|
+
/** When false, the script takes the backup and stops — no restore verify.
|
|
34
|
+
* The caller (`ClickHouseDatabase`) also withholds the verify alarms, so
|
|
35
|
+
* the two halves of the contract disable together. */
|
|
36
|
+
readonly verify: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Builds the backup + restore-verify script.
|
|
40
|
+
*
|
|
41
|
+
* Statement order carries decisions that are not obvious from reading it
|
|
42
|
+
* forward:
|
|
43
|
+
*
|
|
44
|
+
* 1. **The scratch database is dropped FIRST, not only last.** A restore
|
|
45
|
+
* onto an existing database of that name succeeds silently — verified on
|
|
46
|
+
* 26.3.17.56 — so a run that died between RESTORE and DROP would leave
|
|
47
|
+
* tables behind that satisfy the next run's table-set comparison no
|
|
48
|
+
* matter what the new backup contains. Dropping on the way in also makes
|
|
49
|
+
* the free-space reading below honest, since a leftover scratch copy is
|
|
50
|
+
* occupying the disk being measured.
|
|
51
|
+
*
|
|
52
|
+
* 2. **The live row count is sampled BEFORE the backup, and it arms the
|
|
53
|
+
* zero-rows check.** A restore of a genuinely empty database restores
|
|
54
|
+
* zero rows and is CORRECT; failing it would page FAILED on every fresh
|
|
55
|
+
* stack from night one. So `restored_rows=0` is a failure only when the
|
|
56
|
+
* source had rows at backup time. A failed source sample reads as
|
|
57
|
+
* `unknown`, which arms the check — an unreadable source is not evidence
|
|
58
|
+
* the database was empty (fail closed, per the read-failure posture).
|
|
59
|
+
*
|
|
60
|
+
* 3. **The restore is gated on free space, and the gate reads the same
|
|
61
|
+
* quantity the critical disk alarm guards.** A full restore writes a
|
|
62
|
+
* second copy of the database onto the volume it is verifying. That is
|
|
63
|
+
* affordable at today's size and stops being affordable at some larger
|
|
64
|
+
* one, and the failure mode if it is left ungated is the worst kind: the
|
|
65
|
+
* backup check fills the disk that the backups exist to protect. When
|
|
66
|
+
* the copy would not leave `diskFreeCriticalGib × margin` clear, the run
|
|
67
|
+
* skips the restore and SAYS SO with its own marker. Silent degradation
|
|
68
|
+
* is not available here — a skipped verify that logged nothing would be
|
|
69
|
+
* indistinguishable from a passing one. The same discipline covers the
|
|
70
|
+
* probe itself: a failed free-space read emits the SKIPPED marker (the
|
|
71
|
+
* verify did not run, which is exactly what that marker means) and exits
|
|
72
|
+
* non-zero, rather than being the one silent path out of the script.
|
|
73
|
+
*
|
|
74
|
+
* 4. **The restore is bounded by `timeout`, sized from the volume.** A hung
|
|
75
|
+
* restore is the one backup failure with no signal: `BACKUP_CREATED` is
|
|
76
|
+
* already emitted so the heartbeat is satisfied, and the verify alarms
|
|
77
|
+
* count markers rather than their absence. The bound converts that
|
|
78
|
+
* silence into exit 124, which takes the same branch as a raised
|
|
79
|
+
* restore. It scales with `storageGb` because the largest restore the
|
|
80
|
+
* free-space gate admits scales with the volume — a flat bound would
|
|
81
|
+
* misreport every healthy verify as FAILED past the size where the
|
|
82
|
+
* restore outgrows it.
|
|
83
|
+
*
|
|
84
|
+
* 5. **The comparison is on the table SET, never on row counts — and it
|
|
85
|
+
* ignores materialised-view inner tables.** The database keeps ingesting
|
|
86
|
+
* during and after the backup, so the restored copy is a point-in-time
|
|
87
|
+
* from somewhere inside the backup window and can never be expected to
|
|
88
|
+
* match live counts. Rows are checked only against zero (armed per 2).
|
|
89
|
+
* MV inner tables (`.inner_id.<uuid>` on Atomic databases) are excluded
|
|
90
|
+
* from both sides of the set comparison: `RESTORE … AS <scratch>` on the
|
|
91
|
+
* same server assigns NEW UUIDs, so the live database's inner names can
|
|
92
|
+
* never appear in the scratch database and a raw name comparison reports
|
|
93
|
+
* every implicit MV as missing — a FAILED page against a good backup.
|
|
94
|
+
* The MVs themselves (and their data, via the restored inner tables)
|
|
95
|
+
* remain fully compared.
|
|
96
|
+
*
|
|
97
|
+
* Every statement carries an explicit `|| …` failure branch rather than
|
|
98
|
+
* relying on `set -e`. The TLS preamble this script is concatenated onto
|
|
99
|
+
* sets `set -eu`, but ONLY when TLS is active — so a script depending on
|
|
100
|
+
* `set -e` would abort on the first failure with TLS on and blunder past it
|
|
101
|
+
* with TLS off. Explicit branches behave identically either way.
|
|
102
|
+
*/
|
|
103
|
+
export declare function buildClickHouseBackupScript(params: ClickHouseBackupScriptParams): string;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The backup task's shell body: take the backup, then prove it restores.
|
|
3
|
+
*
|
|
4
|
+
* `BACKUP DATABASE … TO S3(…)` returning `BACKUP_CREATED` means ClickHouse
|
|
5
|
+
* finished writing and the statement did not raise. It does NOT mean the
|
|
6
|
+
* bytes it wrote can be read back — and the gap between those two is the
|
|
7
|
+
* whole reason a backup exists. So the task restores what it just wrote,
|
|
8
|
+
* into a scratch database, and says so in its own log line.
|
|
9
|
+
*
|
|
10
|
+
* The restore is a FULL one, deliberately. `SETTINGS structure_only = 1`
|
|
11
|
+
* looks like the cheap version of this check and is not a version of it at
|
|
12
|
+
* all: on 26.3.17.56 a structure-only restore of a backup whose entire
|
|
13
|
+
* `data/` tree had been deleted returned `RESTORED` and exit 0, because it
|
|
14
|
+
* reads `metadata/` and never opens a part. A verification that passes on a
|
|
15
|
+
* backup with no data in it is worse than no verification — it converts an
|
|
16
|
+
* unrestorable backup into a green signal. The full restore fails that same
|
|
17
|
+
* case loudly (`STD_EXCEPTION`, missing `checksums.txt`), which is the
|
|
18
|
+
* behaviour the alarm is built on.
|
|
19
|
+
*/
|
|
20
|
+
import { CLICKHOUSE_BACKUP_SCRATCH_DATABASE, CLICKHOUSE_BACKUP_VERIFY_FREE_SPACE_MARGIN, CLICKHOUSE_BACKUP_VERIFY_FAILED_MARKER, CLICKHOUSE_BACKUP_VERIFY_OK_MARKER, CLICKHOUSE_BACKUP_VERIFY_SKIPPED_MARKER, clickHouseBackupVerifyTimeoutSeconds } from "./clickhouseConstants.js";
|
|
21
|
+
const BYTES_PER_GIB = 1024 ** 3;
|
|
22
|
+
/**
|
|
23
|
+
* Builds the backup + restore-verify script.
|
|
24
|
+
*
|
|
25
|
+
* Statement order carries decisions that are not obvious from reading it
|
|
26
|
+
* forward:
|
|
27
|
+
*
|
|
28
|
+
* 1. **The scratch database is dropped FIRST, not only last.** A restore
|
|
29
|
+
* onto an existing database of that name succeeds silently — verified on
|
|
30
|
+
* 26.3.17.56 — so a run that died between RESTORE and DROP would leave
|
|
31
|
+
* tables behind that satisfy the next run's table-set comparison no
|
|
32
|
+
* matter what the new backup contains. Dropping on the way in also makes
|
|
33
|
+
* the free-space reading below honest, since a leftover scratch copy is
|
|
34
|
+
* occupying the disk being measured.
|
|
35
|
+
*
|
|
36
|
+
* 2. **The live row count is sampled BEFORE the backup, and it arms the
|
|
37
|
+
* zero-rows check.** A restore of a genuinely empty database restores
|
|
38
|
+
* zero rows and is CORRECT; failing it would page FAILED on every fresh
|
|
39
|
+
* stack from night one. So `restored_rows=0` is a failure only when the
|
|
40
|
+
* source had rows at backup time. A failed source sample reads as
|
|
41
|
+
* `unknown`, which arms the check — an unreadable source is not evidence
|
|
42
|
+
* the database was empty (fail closed, per the read-failure posture).
|
|
43
|
+
*
|
|
44
|
+
* 3. **The restore is gated on free space, and the gate reads the same
|
|
45
|
+
* quantity the critical disk alarm guards.** A full restore writes a
|
|
46
|
+
* second copy of the database onto the volume it is verifying. That is
|
|
47
|
+
* affordable at today's size and stops being affordable at some larger
|
|
48
|
+
* one, and the failure mode if it is left ungated is the worst kind: the
|
|
49
|
+
* backup check fills the disk that the backups exist to protect. When
|
|
50
|
+
* the copy would not leave `diskFreeCriticalGib × margin` clear, the run
|
|
51
|
+
* skips the restore and SAYS SO with its own marker. Silent degradation
|
|
52
|
+
* is not available here — a skipped verify that logged nothing would be
|
|
53
|
+
* indistinguishable from a passing one. The same discipline covers the
|
|
54
|
+
* probe itself: a failed free-space read emits the SKIPPED marker (the
|
|
55
|
+
* verify did not run, which is exactly what that marker means) and exits
|
|
56
|
+
* non-zero, rather than being the one silent path out of the script.
|
|
57
|
+
*
|
|
58
|
+
* 4. **The restore is bounded by `timeout`, sized from the volume.** A hung
|
|
59
|
+
* restore is the one backup failure with no signal: `BACKUP_CREATED` is
|
|
60
|
+
* already emitted so the heartbeat is satisfied, and the verify alarms
|
|
61
|
+
* count markers rather than their absence. The bound converts that
|
|
62
|
+
* silence into exit 124, which takes the same branch as a raised
|
|
63
|
+
* restore. It scales with `storageGb` because the largest restore the
|
|
64
|
+
* free-space gate admits scales with the volume — a flat bound would
|
|
65
|
+
* misreport every healthy verify as FAILED past the size where the
|
|
66
|
+
* restore outgrows it.
|
|
67
|
+
*
|
|
68
|
+
* 5. **The comparison is on the table SET, never on row counts — and it
|
|
69
|
+
* ignores materialised-view inner tables.** The database keeps ingesting
|
|
70
|
+
* during and after the backup, so the restored copy is a point-in-time
|
|
71
|
+
* from somewhere inside the backup window and can never be expected to
|
|
72
|
+
* match live counts. Rows are checked only against zero (armed per 2).
|
|
73
|
+
* MV inner tables (`.inner_id.<uuid>` on Atomic databases) are excluded
|
|
74
|
+
* from both sides of the set comparison: `RESTORE … AS <scratch>` on the
|
|
75
|
+
* same server assigns NEW UUIDs, so the live database's inner names can
|
|
76
|
+
* never appear in the scratch database and a raw name comparison reports
|
|
77
|
+
* every implicit MV as missing — a FAILED page against a good backup.
|
|
78
|
+
* The MVs themselves (and their data, via the restored inner tables)
|
|
79
|
+
* remain fully compared.
|
|
80
|
+
*
|
|
81
|
+
* Every statement carries an explicit `|| …` failure branch rather than
|
|
82
|
+
* relying on `set -e`. The TLS preamble this script is concatenated onto
|
|
83
|
+
* sets `set -eu`, but ONLY when TLS is active — so a script depending on
|
|
84
|
+
* `set -e` would abort on the first failure with TLS on and blunder past it
|
|
85
|
+
* with TLS off. Explicit branches behave identically either way.
|
|
86
|
+
*/
|
|
87
|
+
export function buildClickHouseBackupScript(params) {
|
|
88
|
+
const { client, databaseName, backupDestUrl, diskFreeCriticalGib, storageGb, verify } = params;
|
|
89
|
+
const backupStatements = [
|
|
90
|
+
"STAMP=$(date +%Y%m%d-%H%M%S)",
|
|
91
|
+
`DEST="${backupDestUrl}backup-$STAMP/"`,
|
|
92
|
+
`${client} --query "BACKUP DATABASE ${databaseName} TO S3('$DEST')" || exit 1`
|
|
93
|
+
];
|
|
94
|
+
if (!verify) {
|
|
95
|
+
return backupStatements.join("; ");
|
|
96
|
+
}
|
|
97
|
+
const scratch = CLICKHOUSE_BACKUP_SCRATCH_DATABASE;
|
|
98
|
+
const requiredFreeBytes = Math.round(diskFreeCriticalGib *
|
|
99
|
+
BYTES_PER_GIB *
|
|
100
|
+
CLICKHOUSE_BACKUP_VERIFY_FREE_SPACE_MARGIN);
|
|
101
|
+
const verifyTimeoutSeconds = clickHouseBackupVerifyTimeoutSeconds(storageGb);
|
|
102
|
+
const dropScratch = `${client} --query "DROP DATABASE IF EXISTS ${scratch} SYNC"`;
|
|
103
|
+
const sourceRowsQuery = `SELECT sum(rows) FROM system.parts WHERE database = '${databaseName}' AND active`;
|
|
104
|
+
const fitVerdictQuery = `SELECT if((SELECT free_space FROM system.disks WHERE name = 'default')` +
|
|
105
|
+
` - (SELECT sum(bytes_on_disk) FROM system.parts WHERE database = '${databaseName}' AND active)` +
|
|
106
|
+
` >= ${requiredFreeBytes}, 'FITS', 'SKIP')`;
|
|
107
|
+
const missingTablesQuery = `SELECT count() FROM (SELECT name FROM system.tables WHERE database = '${databaseName}' AND name NOT LIKE '.inner%'` +
|
|
108
|
+
` EXCEPT SELECT name FROM system.tables WHERE database = '${scratch}' AND name NOT LIKE '.inner%')`;
|
|
109
|
+
const restoredRowsQuery = `SELECT sum(rows) FROM system.parts WHERE database = '${scratch}' AND active`;
|
|
110
|
+
return [
|
|
111
|
+
`${dropScratch} || exit 1`,
|
|
112
|
+
`SRCROWS=$(${client} --query "${sourceRowsQuery}") || SRCROWS=unknown`,
|
|
113
|
+
...backupStatements,
|
|
114
|
+
`FIT=$(${client} --query "${fitVerdictQuery}") || { echo "${CLICKHOUSE_BACKUP_VERIFY_SKIPPED_MARKER} $DEST free-space probe failed — verify did not run"; exit 1; }`,
|
|
115
|
+
`if [ "$FIT" != "FITS" ]; then echo "${CLICKHOUSE_BACKUP_VERIFY_SKIPPED_MARKER} $DEST restore would not leave ${requiredFreeBytes} bytes free"; exit 0; fi`,
|
|
116
|
+
`timeout ${verifyTimeoutSeconds} ${client} --query "RESTORE DATABASE ${databaseName} AS ${scratch} FROM S3('$DEST')" || { echo "${CLICKHOUSE_BACKUP_VERIFY_FAILED_MARKER} $DEST restore raised or exceeded ${verifyTimeoutSeconds}s"; ${dropScratch} || true; exit 1; }`,
|
|
117
|
+
`MISSING=$(${client} --query "${missingTablesQuery}") || MISSING=unknown`,
|
|
118
|
+
`ROWS=$(${client} --query "${restoredRowsQuery}") || ROWS=0`,
|
|
119
|
+
`${dropScratch} || true`,
|
|
120
|
+
`if [ "$MISSING" != "0" ] || { [ "$ROWS" = "0" ] && [ "$SRCROWS" != "0" ]; }; then echo "${CLICKHOUSE_BACKUP_VERIFY_FAILED_MARKER} $DEST missing_tables=$MISSING restored_rows=$ROWS source_rows=$SRCROWS"; exit 1; fi`,
|
|
121
|
+
`echo "${CLICKHOUSE_BACKUP_VERIFY_OK_MARKER} $DEST restored_rows=$ROWS source_rows=$SRCROWS"`
|
|
122
|
+
].join("; ");
|
|
123
|
+
}
|