New to Kestra?
Use blueprints to kickstart your first workflows.
Classify ClickHouse replica health from system.replicas, alert on lag or readonly state, and optionally restart the replica.
A ClickHouse replica that falls behind on its replication queue degrades reads
silently - queries still return results, just increasingly stale ones. A replica
that loses its ClickHouse Keeper session degrades harder: it flips to readonly and
stops accepting writes outright, often without an obvious client-side error until
something tries to insert into it. This blueprint reads ClickHouse's own
system.replicas table to tell these two failure modes apart, classifies the
result into a three-state risk level, alerts on anything but a clean bill of
health, and can resync a lagging or readonly replica with ClickHouse's own
SYSTEM RESTART REPLICA statement, behind two independent gates.
periodic_replica_check (io.kestra.plugin.core.trigger.Schedule) runs every 15 minutes, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.check_replica_status (io.kestra.plugin.jdbc.clickhouse.Query, fetchType: FETCH_ONE) reads system.replicas filtered to target_table, populating row.is_readonly, row.is_session_expired, row.absolute_delay, row.queue_size, row.active_replicas, row.total_replicas, row.zookeeper_exception - or no row at all.evaluate_replica_presence (io.kestra.plugin.core.flow.If) branches on whether a row was returned.else (UNREACHABLE): alert_table_not_replicated reports that target_table has no row in system.replicas at all.then (row found): nested evaluate_replica_health (io.kestra.plugin.core.flow.If) branches on is_readonly == 0.else (UNREACHABLE): alert_replica_readonly reports the readonly/lost-session state, with zookeeper_exception if ClickHouse recorded one.then (writable): nested evaluate_replica_lag (io.kestra.plugin.core.flow.If) branches on absolute_delay > max_absolute_delay_seconds or active_replicas < total_replicas.else (HEALTHY): log_replica_healthy records a clean check.then (AT_RISK): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers remediation ("true", gated again by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When both gates allow it, restart_replica runs SYSTEM RESTART REPLICA {{ target_table }}. alert_replica_at_risk always fires to Slack regardless of which remediation path ran.log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.errors block alerts Slack separately if the flow itself fails outright - an unreachable cluster or bad credentials must not read as "no risk" or "healthy".SYSTEM RESTART REPLICA on the one table you named - it cannot touch any other table or any row of data.auto_remediate, dry_run) between "risk detected" and "replica actually restarted".ReplicatedMergeTree-family tables who need an early signal when a replica falls behind or loses its Keeper session.SELECT * FROM system.replicas by hand during an incident.Query driving both system-table introspection (system.replicas) and a SYSTEM administrative statement (RESTART REPLICA), not just table reads and inserts.system.replicas run by hand answers one moment in time; it does not decide a cadence, classify the result into a three-state risk level, gate a restart behind two independent confirmations, or notify anyone. Kestra supplies the schedule, the HEALTHY/AT_RISK/UNREACHABLE classification as a first-class branch, two independent authorization gates before any replica is restarted, and an execution history that shows exactly when a replica degraded and whether anything was done about it.
ReplicatedMergeTree-family engine and a working ClickHouse Keeper (or ZooKeeper) ensemble - system.replicas has no row at all for a plain MergeTree table.SELECT on system.replicas and, if auto_remediate will ever be "true", permission to run SYSTEM RESTART REPLICA.Local testing:
docker run -d --name clickhouse-replica-gate -p 8124:8123 -p 9001:9000 clickhouse/clickhouse-server:latest
docker network connect YOUR_KESTRA_NETWORK clickhouse-replica-gate (only needed if the Kestra Worker runs in a separate Docker network than this container; set clickhouse_url to jdbc:clickhouse://clickhouse-replica-gate:8123/default from inside that network, or jdbc:clickhouse://localhost:8124/default from the host). Host port 8124 is used here specifically so it never collides with Kestra's own UI on 8080. A single-node container has no real Keeper ensemble to replicate against, so a genuinely meaningful ReplicatedMergeTree test needs either ClickHouse Keeper configured on the same node or a second container - a true replica-lag scenario cannot be fully exercised on one bare container.
CLICKHOUSE_USERNAME / CLICKHOUSE_PASSWORD: credentials used by every io.kestra.plugin.jdbc.clickhouse.Query task in this flow.SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_replica_at_risk, alert_replica_readonly, alert_table_not_replicated, and the errors block.SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_CLICKHOUSE_PASSWORD=$(echo -n '<your-local-password>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.clickhouse_url (STRING, default jdbc:clickhouse://localhost:8124/default): JDBC URL.target_table (STRING, default default.events): fully qualified db.table audited and, if remediated, restarted.max_absolute_delay_seconds (INT, default 300): lag ceiling before a writable replica counts as AT_RISK.auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for remediation to even be considered.dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for SYSTEM RESTART REPLICA to actually run.outputs.check_replica_status.row.*: the full system.replicas row for target_table every run (is_readonly, is_session_expired, absolute_delay, queue_size, active_replicas, total_replicas, zookeeper_exception), or no row at all if the table is not Replicated.outputs.restart_replica: present only when remediation actually ran; SYSTEM RESTART REPLICA returns no result set, so this output carries no row data of its own, only its existence matters.ReplicatedMergeTree table (requires Keeper/ZooKeeper configured - see Prerequisites) under target_table.CLICKHOUSE_USERNAME, CLICKHOUSE_PASSWORD, and SLACK_WEBHOOK_URL secrets.log_replica_healthy fires on a freshly created, idle replica.absolute_delay past max_absolute_delay_seconds, then re-run and confirm alert_replica_at_risk fires.auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the restart without running it.auto_remediate: "true" and dry_run: "false" and confirm restart_replica runs, then verify with SELECT * FROM system.replicas that absolute_delay has recovered.periodic_replica_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.target_table values with io.kestra.plugin.core.flow.Loop to run the same gate across every Replicated table in a cluster in one flow.queue_size or future_parts alongside absolute_delay to catch a replica that is building a backlog before its lag clock actually starts moving.SYSTEM RESTORE REPLICA instead of SYSTEM RESTART REPLICA when local data exists but Keeper metadata was lost - ClickHouse's own documentation describes RESTORE REPLICA as the correct recovery for that specific case, which this flow does not attempt to distinguish from a routine session expiry.system.replicas has no row at all for a non-Replicated table. A plain MergeTree table pointed at by target_table makes evaluate_replica_presence report UNREACHABLE on every run, which is correct behavior, not a bug - this gate only has something to audit for ReplicatedMergeTree-family engines.is_readonly and lag are different failure modes, not degrees of the same one. A replica can have absolute_delay = 0 and still be is_readonly = 1 immediately after losing its Keeper session, before any lag has had time to accumulate - that is why evaluate_replica_health is checked before evaluate_replica_lag, not folded into the same condition.system.replicas introspection and the SYSTEM RESTART REPLICA statement go through the same generic io.kestra.plugin.jdbc.clickhouse.Query task used elsewhere in this repo's own ClickHouse flows (system.mutations, system.parts).SYSTEM RESTART REPLICA makes the table temporarily unavailable. Per ClickHouse's own documentation, this reinitializes the Keeper session and the table cannot serve queries normally while that happens - it is a real, if usually brief, availability cost, which is exactly why it sits behind both auto_remediate and dry_run.auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for SYSTEM RESTART REPLICA to run; setting only one leaves the other still blocking.Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.system.replicas column names are taken verbatim from ClickHouse's own system-tables documentation, and SYSTEM RESTART REPLICA syntax from ClickHouse's own SYSTEM statements documentation - but no Docker container or Kestra engine was run to execute this flow end to end, and a true multi-replica lag scenario was not exercised at all.