Repository navigation
deployment: SQL-only ClickHouse-hosted install for restricted and multi-node clusters (Altinity Cloud, operator) #692
Description
Goal
Make ClickHouse-hosted SQL Browser installs reliable, verifiable, and documented for operators who can run SQL against ClickHouse but cannot touch the server filesystem — Altinity Cloud, clickhouse-operator deployments, and other managed/restricted environments — on single nodes, single-shard multi-replica clusters, and multi-shard clusters.
docs/ASSET-DISTRIBUTION.md stays the canonical guide; DEPLOYMENT.md, README, and .wiki/ align to it. Add a separate hosted-install page only if the runbook becomes unwieldy.
Background
deploy/install.shbuildsdist/sql.htmlfrom source (needsnpm/node/curl), creates a temporaryFile('RawBLOB', '/var/lib/clickhouse/user_files/<name>')table (ON CLUSTERif--cluster), uploads throughINSERT INTO FUNCTION clusterAllReplicas(...), then drops the table. It refuses >1 shard.- Serving needs two independent things on every node: the bytes in
user_files/and an<http_handlers>config fragment. Shipping bytes through SQL does not enable/sqlby itself. install.shis never executed against a real ClickHouse in CI (onlyshellcheck), so its multi-node behavior is unproven.- Each GitHub release already publishes a prebuilt
sql.htmlasset. - On Altinity Cloud / operator clusters individual replicas are usually not reachable from outside (load balancer / Service). Fan-out must happen server-side; "connect to each node and upload" is not a general answer.
- clickhouse-operator
spec.configuration.fileslands in a ConfigMap (1 MiB cap): fine forhttp_handlers.xml, too small for the ~2 MB artifact.
Delivery — three PRs, this issue closes with the last
PR 1 — confirmed corrections (no mechanism change)
- Remove stale artifact sizes (
~65 KB×2 in ASSET-DISTRIBUTION,~500 KB → ~230 KBin DEPLOYMENT); explain raw vs HTTP-compressed size and how to measure. - Correct prerequisites: source install needs
npm,node22+,curl,clickhouse-client; mention the prebuilt release asset. - Fix applicability claims: "no separate web server" holds only for ClickHouse-native hosting; distributing
http_handlersis not guaranteed (providers may forbidconfig.d) → fallback is the container image / Helm / any static host. Option C: ConfigMap 1 MiB cap. Option B: replication is within a shard only. - Fix auth wording: Bearer id_token by default, access_token with
--audience, JWT-as-Basic-password withch_auth: "basic"(OSS + ch-jwt-verify); show--ch-auth basic/--audiencein the install example. - Fix
http${SECURE:+s}(printshttpswhenSECURE=0) and the missing HTTP port in the printed URL / redirect URI; regression test for both cases. - Fix
.wiki/Deployment-and-Security.mddescribing the standalone image as nginx (it is Caddy).
PR 2 — SQL-only upload, server-side fan-out, per-node verification
Target mechanism (each item is validated by the test harness below before it ships; record results):
- Single node / HTTP-only: upload with the
file()table function — no temporary table, path relative touser_files_path, no hard-coded/var/lib/clickhouse:works identically overINSERT INTO FUNCTION file('sql.html', 'RawBLOB') SETTINGS engine_file_truncate_on_insert = 1 FORMAT RawBLOB
clickhouse-client(stdin) and HTTP(S) (curl --data-binary @sql.html), so the native protocol is not required. - Replicated / multi-shard fan-out — decided (see the experiment comment: writes through
clusterAllReplicas/cluster, chainedfile()included, fail with Code 55 on 25.8/26.3/Antalya, and the current--clusterpath is broken on every multi-replica cluster). Use a server-side pull viaON CLUSTERDDL; every host on every shard writes its own copy:Sources: (a)CREATE TABLE default._asb_pull ON CLUSTER '<cluster>' ENGINE = File('RawBLOB', 'sql.html') AS SELECT * FROM url('<release-or-mirror-url>/sql.html', 'RawBLOB') SETTINGS engine_file_truncate_on_insert = 1; -- mandatory: re-runs append otherwise DROP TABLE default._asb_pull ON CLUSTER '<cluster>' SYNC;
url()to the GitHub release asset / internal mirror /s3()(needs node egress,URL/S3grant,remote_url_allow_hosts; verify redirect handling against real GitHub release URLs); (b) no egress: stage bytes on the initiator withfile(), pull withremote('<initiator>', file(...))(needsREMOTEgrant + credentials). Re-running repairs replaced/added nodes. Do not inline the 2 MB payload intoON CLUSTERDDL (Keeper znode size limit);sql-config.jsonas a literalAS SELECT '<json>'is small enough — test it. Remove theclusterAllReplicasINSERT path. - Per-node integrity check, run by the installer and documented as a standalone query:
Fail on any missing node or hash mismatch versus the local artifact; never rely on a load-balanced
SELECT hostName(), length(c), hex(SHA256(c)) FROM clusterAllReplicas('<cluster>', file('sql.html', 'RawBLOB', 'c String'))
/sqlsmoke test alone. Same forsql-config.json. - Installer:
--artifact <path|url>(prebuilt) vs source build; HTTP(S)-only mode; clean up temporary objects on failure without deleting good assets; clear errors for disabledfile()/url(), missing grants, unreachable replicas. - Test harness (docker-compose: Keeper + 2 shards × 2 replicas; OSS ClickHouse supported versions + one Altinity/Antalya build) exercising fresh install, re-install/upgrade, node wipe + repair, partial failure, least-privilege user; run in CI (path-filtered) like
docker-smoke.
PR 3 — restricted-environment runbook (written from PR 2's tested results)
- Preflight decision table: can you push
config.d(ACM cluster settings: yes; operator:spec.configuration.filesforhttp_handlers.xml)? can SQL writeuser_files(file()enabled, grants)? node egress to the artifact URL? native vs HTTP-only? single node / replicas / shards? → which procedure, or "ClickHouse-native hosting impossible → container image / Helm / static host". ClickHouse Cloud: File engine unavailable → standalone runtime. - Tested least-privilege grants per procedure, with version-specific privilege names (e.g.
FILE/URLsources, newerREAD/WRITE ONsource grants),CREATE/DROP,CLUSTER. No guessed grants. - End-to-end binary-safe examples:
clickhouse-client,curlover HTTPS, Altinity Cloud (ACM cluster setting for handlers + SQL upload), clickhouse-operator (CHIfilesfor handlers + SQL upload). - Single node / single-shard multi-replica / multi-shard procedures; scale-out and node replacement (re-run the pull, then verify); retry guidance.
- Enabling and verifying handlers: OAuth redirect URI,
/sql,/sql/config.json, sign-in,SELECT currentUser(). State thatsql-config.jsonis public and must not hold private credentials. - Option B (Replicated table +
predefined_query_handler) kept as a described alternative only (needs an anonymous read grant; replicates within a shard).
Definition of done
- Fresh install, upgrade, and node-replacement repair proven byte-exact on every node of a 2×2 cluster, on at least one OSS version and one Altinity build (versions recorded in the docs).
- Multi-shard: a tested procedure or an explicit, installer-enforced "unsupported" result — never a silent partial deployment.
- Least-privilege grants tested, or provider restrictions listed explicitly; no blanket claim that hosted providers allow SQL file writes.
- Regression tests for
--secureoutput, upload failure cleanup/retry, and the fan-out/verification logic. - No stale statements left in docs/README/wiki.
Out of scope
A new replicated-table serving backend, OAuth feature changes. Describe such alternatives; don't build them.
References
deploy/install.sh, deploy/http_handlers.xml, docs/ASSET-DISTRIBUTION.md, docs/DEPLOYMENT.md, docs/DEVELOPMENT.md, docs/CLICKHOUSE-OSS-OAUTH.md; ClickHouse docs: File engine, file(), url(), cluster/clusterAllReplicas.
Activity
[-]deployment: make ClickHouse-hosted installs reliable on restricted and multi-node clusters[/-][+]deployment: SQL-only ClickHouse-hosted install for restricted and multi-node clusters (Altinity Cloud, operator)[/+]deploy: register redirect URI with port; test real upload path (#692)
BorisTyshkevich commentedon Oct 26, 2026
Fan-out experiment results (Docker: Keeper + 2 shards × 2 replicas; ClickHouse 25.8 OSS, 26.3 OSS, Antalya 25.8.33)
Payload: 2,000,013 bytes incl. NUL/0xFF/CRLF/invalid UTF-8, checked by SHA256 per node. Identical behavior on all three versions.
INSERT INTO FUNCTION clusterAllReplicas('<c>', file(...))(chaining)Code 55 STORAGE_REQUIRES_PARAMETER: Method write is not supported by storage Distributed with more than one shard and no sharding key— clickhouse-client and HTTP alikecluster('<c>', file(...))/ with sharding keyFiletableON CLUSTER+INSERT INTO FUNCTION clusterAllReplicas(...))internal_replicationtrue or false; leaves the_asb_*table behind (set -eexits before DROP)INSERT INTO FUNCTION cluster('<1-shard>', db, tbl)internal_replication=true; all replicas only withfalseCREATE TABLE … ON CLUSTER … ENGINE=File('RawBLOB','sql.html') AS SELECT * FROM url(…,'RawBLOB') SETTINGS engine_file_truncate_on_insert=1+DROP … ON CLUSTER … SYNCremote('<n1>', file('stage.html','RawBLOB'))REMOTEgrant and explicit credentials for password usersSELECT hostName(), length(c), hex(SHA256(c)) FROM clusterAllReplicas('<c>', file('sql.html','RawBLOB','c String'))FILE_DOESNT_EXIST, names first bad node)File('RawBLOB','sql.html')user_files/; absolute path outside it → Code 291/sqlper nodeAccept-Encoding: gzip→Content-Encoding: gzipMinimal grants for the pull:
CLUSTER ON *.*,CREATE TABLE, DROP TABLE ON default.*,FILE ON *.*,URL ON *.*(INSERT not needed). Verification additionally needsREAD ON FILE+REMOTE.Not yet tested: pulling from the real GitHub release URL (redirects, egress),
sql-config.jsonvia a literalAS SELECT '<json>', HTTP-only end-to-end, ClickHouse Cloud.Consequence for the plan: PR 2 adopts the
ON CLUSTER+url()/remote()pull as the fan-out mechanism for both single-shard and multi-shard clusters; chainingclusterAllReplicas+file()is ruled out for writes (kept for verification). PR 1 (#693) now documents that--clusteris broken and the interim manual pull procedure.docs: --cluster is broken on multi-replica clusters; add tested manua…
deploy: fail fast on multi-replica --cluster; document config literal…
BorisTyshkevich commentedon Oct 26, 2026
Also for PR 2:
deploy/uninstall.sh --clusteronly empties the files on the connected node (CREATE/DROP run ON CLUSTER, but the truncating INSERT is local) — documented in #693, not fixed. The uninstall should use the same ON CLUSTER pattern as the install. Note also from the grants run: aDROP … ON CLUSTERafter a CREATE that failed on some hosts errors with UNKNOWN_TABLE on those hosts — useDROP TABLE IF EXISTSin the installer's cleanup.deploy: --cluster guard fails closed; idempotent multi-node pull proc…
Merge pull request #693 from Altinity/fix/692-deploy-corrections
BorisTyshkevich commentedon Oct 26, 2026
PR 2 design, from a second experiment (26.3 OSS, Antalya 25.8.33, partly 25.8 OSS)
Results that change the design:
CREATE … ON CLUSTER … AS SELECTtruncates the target file to 0 bytes. This happens with a dead replica, an interserver auth failure, or a missing source. Writing a network source straight into the livesql.htmlcan therefore cause an outage.url()does not follow redirects by default (TOO_MANY_REDIRECTS). GitHub release URLs needmax_http_get_redirects=10.clusterAllReplicas('<c>', file(stage)) SETTINGS engine_file_empty_if_not_exists=1. This needs the cluster's<secret>or per-replica credentials; with neither it fails withAUTHENTICATION_FAILED(516). The stage name must differ from the final name, otherwise the result is non-deterministic.TABLE_ALREADY_EXISTSon that node only. Pre-clean withDROP TABLE IF EXISTS … ON CLUSTER … SYNC, then retry; that path is tested.distributed_ddl_task_timeout. The queued statement replays later against whatever the source contains by then.wait_end_of_query=1: a failure can come back as HTTP 200 with an__exception__trailer.view()insideclusterAllReplicasreturns one row per node even when its file is missing; compare the row count againstsystem.clusters.Design: two phases.
_asb_next_*on every node, by staging (default) or with--pull-url._asb_next_*file over the live file. No network is involved in this step.New flags:
--artifact,--pull-url,--transport native|http,--http-port,--ddl-timeout,--verify-only.uninstall.shgets the sameON CLUSTERpattern.Tested minimal grants:
FILE,CLUSTER,REMOTE,CREATE TABLE,DROP TABLE.URLinstead ofREMOTE.REMOTE,FILE,SELECT ON system.clusters.deploy: integration suite, single-node FILE-only writes, bounded url …
deploy: review hardening for the SQL fan-out installer (#692)
deploy: review fixes for the SQL fan-out installer (#692)
deploy: node identity is serverUUID(), not hostName():tcpPort() (#692)
deploy: deterministic test barrier for the node-killed-mid-run scenar…
deploy: final review fixes for the SQL fan-out installer (#692)
Merge pull request #694 from Altinity/feat/692-sql-fanout-installer
docs: address review findings on the restricted-install runbook (#692)
Merge pull request #696 from Altinity/docs/692-restricted-runbook