ClickHouse Cloud and Managed Postgres
This guide runs Loom on Kubernetes over two managed services: ClickHouse Cloud for
projections, vectors, history and analytics, and ClickHouse Managed Postgres as the memory
authority. It covers the services, the chart's postgres values, the checks after the
install, rotating the Postgres password and the CA, and the SQL surface user. The chart
itself is in Kubernetes, the authority in
Postgres memory authority.
The overlay deploy/helm/loom/values-clickhouse-cloud.yaml layers on the EKS one. It is for
a deployment on an empty ClickHouse: the authority is chosen at a deployment's first boot,
and on a ClickHouse that already holds memory the pods refuse to start, as no migration
moves memory from the ClickHouse authority to Postgres.
The services
- ClickHouse Cloud: a service with a database named
loomand a user for Loom, its IP access list allowing the cluster's NAT addresses (Kubernetes, On EKS). -
ClickHouse Managed Postgres: a service with two HA standbys, a database and a role that owns it, and the direct endpoint on port 5432 (Postgres, ClickHouse Managed Postgres). Grant the role
pg_read_all_stats, so Loom can see which standbys are attached:GRANT pg_read_all_stats TO loom;
Secrets
The overlay reads two Secrets beside loom-secrets: loom-postgres, key password, with
the role's password, and loom-postgres-ca, key ca.crt, with the CA bundle. Make them the
same way as loom-secrets, each with an ExternalSecret of its own.
ca.crt holds every root certificate the service's CA currently publishes, concatenated as
PEM; the driver loads the file as a bundle and trusts nothing else. Take the roots from the
CA's published list (the "CA Issuers" URL in the server certificate names the CA);
openssl s_client -starttls postgres -showcerts usually prints the chain without its
root. Convert a root fetched as DER first:
openssl x509 -inform DER -in root.der -out root.pem
Concatenate the root.pem files into ca.crt, the file the Secret holds. Check it against
the chain the server sends, trusting nothing but the bundle, as the driver does: leaf.pem
is the first certificate s_client -showcerts prints and chain.pem the rest
(-no-CAstore needs version 3 of openssl):
openssl verify -no-CApath -no-CAstore -CAfile ca.crt -untrusted chain.pem \
-verify_hostname <postgres host> leaf.pem
A certificate the bundle does not cover fails closed: a pod that boots with it does not
start, and its log shows SSLCertVerificationError; in a running pod, every new
connection fails. When the CA announces a change to its hierarchy, update the bundle under
a new Secret name (below).
Install
helm install loom deploy/helm/loom -n loom \
-f deploy/helm/loom/values-eks.yaml -f deploy/helm/loom/values-clickhouse-cloud.yaml \
--set publicBaseUrl=https://loom.example.com \
--set clickhouse.host=<service>.<region>.aws.clickhouse.cloud \
--set postgres.host=<postgres direct host>
With postgres.host set, the chart gives every pod that runs Loom (web, workers, the hook
Jobs and the analytics export):
LOOM_STORAGE_AUTHORITY=postgres,LOOM_PG_DURABILITYandLOOM_PG_POOL_MAX_SIZEfrompostgres.durabilityandpostgres.poolMaxSize;LOOM_PG_DSNwith no password,postgres://<user>@<host>:<port>/<database>?sslmode=verify-full&sslrootcert=/etc/loom/pg-ca/<key>, so the password never sits in the pod spec;PGPASSWORDfrompostgres.passwordSecret, which the driver reads when the DSN has none;postgres.caSecretmounted read-only at/etc/loom/pg-ca;- with
networkPolicy.enabled, egress topostgres.port.
The chart refuses to render a postgres block it cannot pass on as written, and any of
the names it renders set a second time in env, extraEnv, secrets.extra or
settingsFile; each refusal names the value to fix, and the block's comments in
deploy/helm/loom/values.yaml state the rules. Its values win over the same names in
existingSecret and envFrom.
Connections
Each Loom process holds up to postgres.poolMaxSize connections; a call that finds them
all busy waits up to LOOM_PG_STATEMENT_TIMEOUT_MS for one, then answers 503. A rollout
runs the old and the new pods side by side, so size the service for twice the steady count:
poolMaxSize × (webWorkers × web pods + worker processes) × 2
+ booting processes + running erasures + CLI runs ≤ max_connections − reserved
Web pods are autoscaling.maxReplicas with autoscaling on, else replicaCount; worker
processes are workers.replicaCount with workers.enabled, else one, which a web pod
runs. Each running erasure holds one more connection for its Postgres step. The overlay
sets webWorkers to 2, what the image picks for the chart's 2-CPU limit, and
poolMaxSize to 5: with three web pods and one workers pod that is 70 connections, which
fits a service whose max_connections leaves 100 for Loom. Raising webWorkers needs
resources.limits.cpu and memory (1 GiB per process) to match.
Replicas added past the chart's count by anything else are outside the sum.
After the install
- Loom refuses to start when the service's
synchronous_standby_namesis empty. It starts with fewer standbys attached than it names, logging the WARNINGPostgres durability: fewer synchronous standbys are attached than required. A commit then waits for them up toLOOM_PG_STATEMENT_TIMEOUT_MSplus one second, and answers 503POSTGRES_UNAVAILABLEwithoutcome_unknown(what the boot checks). GET /v1/healthreportsstorage_authority: postgresandpg_ok.GET /v1/health?include=postgresadds apostgresblock withstandby: synchronous; the block is missing wheneverpg_okis false, when the check'sSELECT 1fails for any reason or takes longer than 2 s.- A role without
pg_read_all_statsstill starts, with the boot WARNINGPostgres durability: cannot see pg_stat_replication; standbys are unverified, andstandbyreadsnull. The WARNING is the reliable sign;standbyis alsonullwhen its read fails or takes longer than 2 s.
Rotating the password and the CA
The password, in this order, back to back:
- Update the password in the secret store and confirm
loom-postgreshas synced. - Run
ALTER ROLE loom PASSWORD '…'on the service. - Run
kubectl rollout restarton the web and workers Deployments together.
Connections already open keep working. Between steps 2 and 3, a call whose pod must open a
new connection (the pool closes connections idle for 300 s) answers 500, and the server
log shows InvalidPasswordError (SQLSTATE 28P01); retrying does not help until the pod
restarts. A pod that starts between steps 1 and 2 (a scale-up, an eviction, the analytics
export or a hook Job) fails to boot until step 2, and boots on its next restart.
The CA: create the new bundle under a new Secret name and helm upgrade with
postgres.caSecret.name set to it. The pod spec changes, so the pods roll. Delete the old
Secret only once the rollout has finished and no revision you might roll back to names it.
The SQL surface user
An org reads its agents' memory as SQL through a ClickHouse user that
loom sql-access grant makes (SQL surface). Run it in a web
pod with a ClickHouse admin credential that may create users, passing the admin password
from a file, never typed at an echoing terminal:
web=$(kubectl get deploy -n loom -o name \
-l app.kubernetes.io/instance=loom,app.kubernetes.io/component=web)
kubectl exec -i -n loom "$web" -- sh -c \
'read -r LOOM_CH_PASSWORD; export LOOM_CH_PASSWORD LOOM_CH_USER=default;
exec loom sql-access grant --user acme_sql --org acme --namespaces support,research' < admin.pass
The command prints the SQL user's password once, to the terminal only: an exec stream is not
a container log. --namespaces '*' also covers namespaces made later; never give it on an
org that sign-ups share (access). --all instead of
--org … --namespaces … fits a deployment whose data belongs to one org. Revoke the user
before uninstalling: loom sql-access revoke --user acme_sql, run the same way.
Restoring a service
A restore of either service is followed by loom reconcile, run in a Job made from the
web pod's spec while the release is stopped (restoring one
store).
Related
Kubernetes · Postgres memory authority · Deployment · SQL surface · Query Loom from Postgres