Skip to main content

Kubernetes

This is the overview for running Zaris on Kubernetes with the official Helm chart. It explains what the chart deploys, the general Helm commands to install / secure / expose it, and how to size the cluster — then hands off to a per-platform guide for the vendor-specific bits (cluster creation, node pools, LoadBalancers, ingress, DNS).

Kubernetes runs the nodes, so this is the attach deployment model: a Management Service observes the cluster and never forks a node. The chart runs the nodes as a StatefulSet, discovers peers from pod ordinals and headless DNS, and lets you scale out with a plain kubectl scale — no ConfigMap edit and no restart of existing pods. It also covers the connection-string client model, out-of-cluster clients, and TLS with multi-SAN certificates.

Pick your platform

If you already know Kubernetes, the general Helm commands below are all you need. For a step-by-step walkthrough on a specific platform — including cluster creation, a dedicated cache node pool, and a secured public console over HTTPS — start here:

PlatformGuide
Azure — AKSDeploy on AKS →
AWS — EKSDeploy on EKS →
Google Cloud — GKEDeploy on GKE →
Red Hat — OpenShiftDeploy on OpenShift →
Self-managed / bare-metal — On-PremDeploy On-Prem →

Each guide reuses the concepts on this page (sizing, security, ingress/cert-manager) and only adds what's specific to that environment.

Durability is by replication, not disk

Zaris nodes are in-memory. A node's data volume is an emptyDir; durability comes from the replication factor (RF), so a rescheduled pod rebuilds its share from replicas. No PersistentVolume is required.

How the node maps to Kubernetes

The node's identity and discovery model maps cleanly onto StatefulSet primitives — the chart wires all of this for you:

  • Stable identity → StatefulSet. Each pod gets a stable ordinal name (zaris-0, zaris-1, …), which the node adopts as its CLUSTRON_NODE_ID.
  • Peer discovery → headless Service + Kubernetes discovery. A headless Service (clusterIP: None) publishes stable per-pod DNS names. With node.discovery.kubernetes: true (the default) the live roster is synthesized from the pod ordinals and that DNS, so the cluster grows and shrinks with the StatefulSet.
  • Client access → Service. A ClusterIP Service fronts the node client port 7861 for in-cluster clients and load-balances the initial connection.
  • Health & rollout safety → HTTP probes. The node serves convergence-gated /livez and /readyz on port 7802. A pod is Ready only once its partition map and ownership are live, so a Ready pod is a routable pod. A PodDisruptionBudget bounds voluntary disruption.
  • Management → attach-mode manager. An optional Deployment runs one Management Service + Web Console that adopts the running nodes.

Prerequisites

  • A working Kubernetes cluster and kubectl configured against it (any conformant cluster — a managed cloud service or self-managed).
  • Helm v3.

The node and manager images are pulled from Docker Hub by the published chart, so there is nothing to build. For a specific cloud, the platform guides cover creating the cluster first.

One-command install

The chart is published as an OCI artifact on Docker Hub — install it by reference, no helm repo add needed:

helm install zaris oci://registry-1.docker.io/clustron/zaris \
-n zaris --create-namespace
kubectl -n zaris rollout status statefulset/zaris
kubectl -n zaris get pods

Expect zaris-0..3 and zaris-manager-* all 1/1 Ready. Because readiness only passes once the partition map and ownership are live, a Ready pod already owns and serves its share of the data.

The chart creates: the node StatefulSet (zaris), a headless Service (zaris-nodes) for peer DNS, a client Service (zaris-client), a PodDisruptionBudget, and — unless you disable it — the manager Deployment + Service (zaris-manager). By default the console is private (portForward) and the cluster is plaintext — the sections below add sizing, security, HTTPS, and TLS.

To pin a chart version, append --version 1.1.0. To see everything the chart exposes before installing, run helm show values oci://registry-1.docker.io/clustron/zaris.

Sizing: how many nodes and pods?

Three inputs decide the shape of the cluster — durability (replication factor), capacity (how much data + how much throughput), and blast radius (how much one pod's loss costs). Work them in order.

One pod = one node. The chart runs one Zaris node per pod, so "pods" and "nodes" are the same count (node.replicas). Each pod should map to a distinct Kubernetes worker for real fault tolerance — spread them with podAntiAffinity or topology spread (the platform guides show this per cloud).

Step 1 — pick the replication factor (durability). RF is how many copies of each partition exist, always on distinct pods:

replicationFactorToleratesUse for
1no pod loss (a pod down = its data gone until rescheduled + reloaded)dev / disposable caches
2 (default)one pod down with no data lossmost production caches
3two pods down simultaneouslystrict availability / larger clusters

Step 2 — pick the pod count. node.replicas must be a multiple of replicationFactor — that is the single rule that keeps every partition fully replicated. The partition count is derived as replicas ÷ replicationFactor; more pods means more (smaller) partitions, more parallelism, and a smaller blast radius per pod.

Replicas must be a multiple of RF

At RF 2, replicas: 3 is not durable — the arithmetic leaves one partition with a single copy, so losing the wrong pod loses data. Use 2, 4, 6, … for RF 2 and 3, 6, 9, … for RF 3. The chart's minimum durable cluster is replicas = 2 × RF.

Step 3 — size each pod (capacity). Nodes are in-memory, so the whole dataset — times RF — must fit in pod memory across the cluster:

memory per pod ≈ (dataset bytes × replicationFactor ÷ replicas) × 1.5

The × 1.5 is headroom for indexing, replication buffers, and eviction working under load. For CPU, a 4 vCPU pod is the sweet spot: on the fixed images a single 4-vCPU node serves on the order of ~400K ops/s at saturation, and the cluster scales roughly linearly as you add pods (~225K ops/s per added node in measured 1→2→3 node runs). Size for headroom, not saturation.

Worked examples:

ScenarioDatasetreplicationFactorreplicasPartitionsPer-pod resources
Dev / demo≤1 GB1221 vCPU / 2Gi
Small production~10 GB2422 vCPU / 8Gi
Medium production~50 GB2634 vCPU / 16Gi
High-availability~100 GB3624 vCPU / 32Gi

Set the per-pod resources under node.resources (see The nodes) and the counts under node.replicas / node.replicationFactor. You can scale out later with a hot kubectl scale — size for expected load up front, since scaling in is not yet drain-safe.

Dedicated cache nodes. To keep Zaris off your application's workers, put it on its own Kubernetes node pool (labeled + tainted) and pin the pods there with node.nodeSelector + node.tolerations. Each platform guide shows the exact node-pool commands — e.g. az aks nodepool add on AKS, eksctl create nodegroup on EKS.

Customizing with Helm

Everything below is set with --set key=value on helm install/upgrade, or in a -f my-values.yaml file (recommended for anything non-trivial). Full defaults + comments live in charts/zaris/values.yaml. A representative custom file:

# my-values.yaml
cluster:
id: orders-cache # cluster id AND the store name clients use
node:
replicas: 6 # keep a MULTIPLE of replicationFactor
replicationFactor: 2 # copies of each partition (on distinct pods)
image: { repository: clustron/zaris-node, tag: "1.1.0" }
resources: # per-node requests/limits
requests: { cpu: "2", memory: 4Gi }
limits: { cpu: "4", memory: 8Gi }
manager:
console:
expose: ingress # portForward (default) | ingress | loadBalancer
ingress:
host: zaris-console.example.com
tls: { enabled: true, clusterIssuer: letsencrypt-prod }
security:
enabled: true # REQUIRED to expose the console externally
existingSecret: zaris-admin # keys: admin-username / admin-password
helm install zaris oci://registry-1.docker.io/clustron/zaris -n zaris --create-namespace -f my-values.yaml

The store

There is one store per release, named by cluster.id — that value is both the clusterId and the store name your clients connect to. Its shape comes from the node tier:

SettingDefaultEffect
cluster.idzaris-k8sstore name / cluster id (the connection-string path)
node.replicationFactor2copies of each partition, kept on distinct pods
node.replication.modeAsyncAsync or Sync replication

The partition count is derived (replicas ÷ replicationFactor), and the store is auto-attached on deploy (manager.attach.enabled, default on) so it shows in the console with no manual step. To run more than one store, deploy the chart again into another release/namespace with a different cluster.id.

The nodes

SettingDefaultEffect
node.replicas4pod count — keep it a multiple of replicationFactor (RF 2 → 4, 6, …) so every partition is fully replicated
node.image.repository / tagclustron/zaris-node / 1.1.0node image
node.resources{}pod requests/limits (set these for production)
node.discovery.kubernetestruehot kubectl scale via ordinal + DNS discovery (no restart)
node.clientPort / healthPort7861 / 7802client / probe ports
node.nodeSelector · tolerations · affinity{}standard pod placement

Scale out live with kubectl scale statefulset zaris --replicas=<N> (then persist with helm upgrade ... --set node.replicas=<N>). See Scaling.

Authentication (control plane)

The console + management API are unauthenticated until you enable control-plane security. The chart refuses to expose the console externally (console.expose: ingress|loadBalancer) unless it's on — see Exposing the console.

SettingDefaultEffect
manager.security.enabledfalseturn on auth (sets ZARIS_SECURITY=true)
manager.security.adminUsername / adminPasswordadmin / adminfirst admin, seeded on boot — change before any real deploy
manager.security.existingSecret""use a Secret (keys admin-username/admin-password) instead of an inline password (preferred in production)

The manager seeds the admin before it becomes Ready, so the readiness-gated console URL is never published to an un-provisioned cluster. Data-plane (per-store) tokens for applications are managed from the console/PowerShell after sign-in — see the security guide.

TLS

Two independent layers:

  • Data-plane / cluster TLS (node↔node + client↔node): node.tls.enabled=true with a CA + per-node leaf Secret. See External clients — secure and TLS / CA models.

    SettingEffect
    node.tls.enabledtransport TLS (mounted-secret model)
    node.tls.certSecretSecret of per-node <nodeId>.pfx leaves
    node.tls.caPemcluster CA PEM (supply with --set-file node.tls.caPem=ca.pem)
  • Console TLS (browser↔console): terminated at the ingress with a publicly-trusted cert — cert-manager (manager.console.ingress.tls.clusterIssuer) or your own Secret (secretName). Never Zaris's internal CA. See Reaching the web console.

Connecting — the connection-string model

Clients connect with a connection string, identical on Kubernetes, Docker, Windows, or a cloud VM:

zaris://host1:7861,host2:7861/<storeName>?opt=val     # plaintext
zariss://host1:7863,host2:7863/<storeName>?opt=val # TLS (the scheme selects it)
  • The comma-separated hosts are the seed list — the client tries them in order (bootstrap failover), then learns the full topology from the cluster.
  • Options: ?token=env:VAR / file:/path / <literal> (prefer env: or file: to keep secrets out of the string); ?ca=/path/ca.pem for the cluster CA; ?tlsInsecure=true (dev only).

For in-cluster clients, use the client Service DNS: zaris-client.<namespace>.svc.cluster.local:7861.

The store name is the cluster id

A Zaris cluster hosts one store, and the store name is the cluster id. You set it in exactly one place on each side:

WhereWhat you set
Server (once)cluster.id in Helm values → clusterId in clustron.json. Default zaris-k8s.
Client (every connection)the path segment of the connection string: zaris://…/<clusterId>. Use the same value.

There is no separate clusterId field on the client — the connection-string path is the cluster id. The DI registration key and the PowerShell store handle are local labels only; they are never sent to the server. If the path doesn't match the server's cluster.id, the connect fails with an actionable error naming both values.

C# / ASP.NET

// Register one store (the key "zaris-k8s" here is a local label):
services.AddClustronZaris(
"zaris-k8s", "zaris://zaris-client.zaris.svc.cluster.local:7861/zaris-k8s");

// Or from the standard ConnectionStrings config section (appsettings.json):
// "ConnectionStrings": { "zaris-k8s": "zaris://zaris-client.zaris.svc.cluster.local:7861/zaris-k8s" }
services.AddClustronZarisFromConnectionStrings(configuration);

// Resolve + use:
var provider = sp.GetRequiredService<IZarisClientProvider>();
var client = await provider.GetAsync("zaris-k8s");
await client.PutAsync("k", "v");
var v = await client.GetAsync<string>("k");

You can smoke-test the data plane in-cluster with the ConnectionStringClient sample as a one-shot pod:

kubectl -n zaris run cs --restart=Never --image=clustron-zaris-connstring-sample:local \
-- "zaris://zaris-client.zaris.svc.cluster.local:7861/zaris-k8s" 100
kubectl -n zaris logs pod/cs # expect: verified 100/100 keys — RESULT: PASS

PowerShell

Connect-ZrStore -ConnectionString "zaris://zaris-client.zaris.svc.cluster.local:7861/zaris-k8s"

Scaling

With Kubernetes discovery on (the default), scaling out is a hot kubectl scale — new pods join and the partition map grows onto them, with no restart of existing pods:

kubectl -n zaris scale statefulset zaris --replicas=5
kubectl -n zaris rollout status statefulset/zaris

Validated: a 3→5 scale-out kept all data (200/200 keys) and grew the map. To make the new size durable across a helm upgrade, set it in values too:

helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values --set node.replicas=5
Scale in one step at a time

Removing a pod drops the partition copies it held. Scaling in is not yet drain-safe: taking away more than RF allows can remove every copy of some partition at once and lose that data. Scale in a single step at a time and let replication re-establish RF between steps. A lossless data-plane drain on scale-in is a planned enhancement — size the cluster for its expected load up front rather than relying on shrinking a live one.

High availability, rolling updates & cold start

  • PodDisruptionBudget (node.pdb, default maxUnavailable: 1) caps voluntary disruptions — node drains, upgrades, rolling restarts.
  • Safe rolling updates. The StatefulSet rolls pods sequentially, highest→lowest ordinal, each becoming Ready before the next. When a node begins shutting down it flips /readyz to 503 the instant shutdown starts, broadcasts a graceful leave, and holds 503 for the rest of the drain — so Kubernetes removes it from the client Service before it stops accepting work, and in-flight clients are steered to a still-ready peer. terminationGracePeriodSeconds (default 40) gives that graceful leave time to finish. The client Service keeps at least replicas − 1 Ready endpoints throughout, so there is no black-hole. A multi-pod TLS roll can take several minutes — that is normal, not a hang.
  • Cold start. A node that boots with a partial roster (StatefulSet DNS propagates incrementally; the ordinal-0 leader can briefly see only itself) converges to the real cluster via the live-peer reconciler. No operator action is needed.
  • Node failure recovery. Delete a pod (kubectl -n zaris delete pod zaris-2); the StatefulSet reschedules it, it rejoins, and replicas restore RF. With RF ≥ 2 the store keeps serving throughout.

External (out-of-cluster) clients — plaintext

By default, clients must run in-cluster because the cluster advertises in-cluster pod DNS. To let out-of-cluster clients connect, enable the external listener: each pod binds a second client listener that advertises externally-reachable addresses (derived from an ordinal template), and the chart creates one Service per pod (zaris-ext-<ordinal>).

helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values \
--set node.external.enabled=true \
--set node.external.clientPort=7863 \
--set node.external.service.type=LoadBalancer \
--set 'node.external.hostTemplate=zaris-{ordinal}.zaris-ext.example.com'
  • hostTemplate (with {ordinal} substituted) must resolve to the per-pod Services the chart creates. Use a DNS record per ordinal (LoadBalancer/Ingress) or the node IP (NodePort).
  • NodePort alternative: --set node.external.service.type=NodePort --set node.external.service.nodePortBase=31900 (pod NnodePortBase + N) and --set node.external.advertisePortBase=31900.
  • The external client port (7863) must differ from the internal client port (7861) and the health port (7802).

Then connect from outside the cluster:

zaris://zaris-0.zaris-ext.example.com:7863,zaris-1.zaris-ext.example.com:7863/zaris-k8s
Do not expose the data plane in the clear

Plaintext external access sends data over the network unencrypted; the node logs a loud [SECURITY] warning at startup. Set ZARIS_EXTERNAL_ALLOW_INSECURE=true only to acknowledge this for development, and use TLS (below) for anything real.

External clients — secure (TLS + multi-SAN)

TLS on the external listener needs each node's leaf certificate to carry its externally-advertised hostname in its SAN (so a TLS client dialing that name gets no name mismatch) as well as its internal DNS name (for node↔node gossip). The chart supports the mounted-secret model (Model B): pre-provision a cluster CA plus one leaf per node and mount them via a Secret.

1. Generate the CA + per-node multi-SAN leaves with tools/ZarisCertGen (built on the production CA primitives). Each leaf's SAN carries the internal headless DNS name and the external advertised host:

dotnet run --project tools/ZarisCertGen -- ./certs zaris-k8s-ca \
zaris-0=zaris-0.zaris-nodes.zaris.svc.cluster.local,zaris-0.zaris-ext.example.com \
zaris-1=zaris-1.zaris-nodes.zaris.svc.cluster.local,zaris-1.zaris-ext.example.com \
zaris-2=zaris-2.zaris-nodes.zaris.svc.cluster.local,zaris-2.zaris-ext.example.com
# → ./certs/ca.pem and ./certs/zaris-{0,1,2}.pfx

Verify a leaf carries both names:

openssl pkcs12 -in ./certs/zaris-0.pfx -clcerts -nokeys -passin pass: \
| openssl x509 -noout -subject -ext subjectAltName
# X509v3 Subject Alternative Name:
# DNS:zaris-0.zaris-nodes.zaris.svc.cluster.local, DNS:zaris-0.zaris-ext.example.com

2. Create the cert Secret (one Secret, each key <nodeId>.pfx):

kubectl -n zaris create secret generic zaris-node-certs \
--from-file=zaris-0.pfx=./certs/zaris-0.pfx \
--from-file=zaris-1.pfx=./certs/zaris-1.pfx \
--from-file=zaris-2.pfx=./certs/zaris-2.pfx

3. Deploy with TLS + external enabled:

helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values \
--set node.tls.enabled=true \
--set node.tls.certSecret=zaris-node-certs \
--set-file node.tls.caPem=./certs/ca.pem \
--set node.external.enabled=true \
--set node.external.clientPort=7863 \
--set node.external.service.type=LoadBalancer \
--set 'node.external.hostTemplate=zaris-{ordinal}.zaris-ext.example.com'

An initContainer copies each pod's own <nodeId>.pfx into place; the node then serves the internal data plane and the external listener over TLS, and node↔node gossip is mutual TLS.

4. Connect securely — give the client the cluster CA and use the zariss:// scheme:

zariss://zaris-0.zaris-ext.example.com:7863,zaris-1.zaris-ext.example.com:7863/zaris-k8s?ca=/path/ca.pem
Connect-ZrStore -ConnectionString "zariss://zaris-0.zaris-ext.example.com:7863/zaris-k8s?ca=C:\certs\ca.pem"
# or, with a token:
# zariss://.../zaris-k8s?token=env:ZARIS_TOKEN&ca=/path/ca.pem

Validated end-to-end: zariss://…/zaris-k8s?ca=… round-tripped 100/100 keys over TLS through the external listener, with the client validating each node against its advertised SAN.

TLS / CA models

ModelHow the leaf is obtainedWhen to use
B — mounted secret (used above)Pre-generate the CA + per-node leaves; mount them via a SecretAir-gapped / GitOps / deterministic PKI
A — enrollmentThe manager is the issuer; each node enrolls at boot (CSR-over-HTTP with a one-time token) and auto-renewsOnline clusters that want automatic issuance + rotation

Both models support the four CA trust modes — M0 auto, M1 bring-your-own CA, M2 intermediate, M3 pre-issued leaves. The mounted-secret model here is M1/M3-style (you own the CA). For the full picture, see CA and trust modes (the mounted path corresponds to Mode 3 — pre-issued leaves).

Ingress & cert-manager: prerequisites for external HTTPS

The console is private by default (portForward). To publish it at an https:// hostname your browser trusts, you need three things in the cluster before you set console.expose: ingress. This is standard Kubernetes plumbing — the chart consumes it, it does not install it.

1. An ingress controller — the reverse proxy that terminates HTTPS at the edge and routes to the console Service. A LoadBalancer is layer-4 (it forwards raw TCP, so http://<ip> only, no TLS termination and no hostname routing); an Ingress is layer-7 and is what gives you HTTPS at a name. Install one if your cluster doesn't have it:

  • Cloud-native: AKS Application Routing / AWS Load Balancer Controller / GKE ingress — the platform guides use these.
  • Portable: ingress-nginx works on any cluster, including self-managed / on-prem (with MetalLB to hand out the LoadBalancer IP).

2. A DNS name pointing at the ingress. A public cert is issued for a hostname, never a bare IP — so create a DNS A/CNAME record (e.g. zaris-console.example.com) that resolves to the ingress controller's external IP. That name goes in manager.console.ingress.host.

3. cert-manager (for automatic certificates). cert-manager watches your ingress and mints + auto-renews certificates from an issuer. The two common issuers:

  • Let's Encrypt (public CA, free) via a ClusterIssuer — the cert is trusted by every browser with no warning. Requires the DNS name to be publicly resolvable so the ACME challenge can validate it. Point the chart at it with manager.console.ingress.tls.clusterIssuer=letsencrypt-prod.
  • Your corporate/internal CA — for private or air-gapped clusters where Let's Encrypt can't reach the host. Issue the cert yourself (or from an internal issuer) and hand the chart a TLS Secret via manager.console.ingress.tls.secretName instead of a ClusterIssuer.
Console TLS ≠ cluster TLS

This section is about the browser → console certificate, which must be publicly trusted and is terminated at the ingress — never Zaris's internal CA. The separate node ↔ node / client ↔ node transport (data-plane TLS) uses the cluster's own CA. They're independent layers with independent certs.

A minimal ClusterIssuer for Let's Encrypt (HTTP-01), applied once per cluster:

# letsencrypt-clusterissuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef: { name: letsencrypt-prod }
solvers:
- http01:
ingress: { class: nginx } # match your ingress controller's class
# Install cert-manager (CRDs + controller), then apply the issuer:
helm install cert-manager cert-manager \
--repo https://charts.jetstack.io \
-n cert-manager --create-namespace --set crds.enabled=true
kubectl apply -f letsencrypt-clusterissuer.yaml

With those in place, exposing the console over HTTPS is the one command below. The platform guides wire the controller + DNS + issuer with the exact commands for each cloud.

Add a manager and Web Console

The chart deploys the attach-mode manager and console by default (manager.enabled: true). It auto-attaches the store on boot (manager.attach.enabled, default on): the manager self-registers the store — cluster id, replication factor, partition count, node roster — so it appears in the Console with no manual "register" step, and the orchestrator-run nodes are never forked or contacted.

Reach the console from a workstationmanager.console.expose chooses how:

exposeYou getNeeds
ingress (default)https://<your-host> at a hostname, TLS terminated at the ingressan ingress controller + DNS
loadBalancerthe console's own cloud LoadBalancer → http://<external-ip>a cloud LB (AKS/EKS/GKE)
portForwardhttp://localhost:8080 via one kubectl port-forwardnothing — works on any cluster

The management port (7801) is never exposed externally — the internal manager Service stays ClusterIP; only the console (HTTP) is published. Helm prints the exact URL in the install notes, and helm status <release> -n <ns> reprints it.

Exposing the console requires control-plane auth

The default is console.expose: portForwardprivate, works out of the box, no security needed (only you, via kubectl port-forward, can reach it). The external modes (ingress / loadBalancer) publish an admin console, so the chart refuses to render them unless manager.security.enabled=true with an admin credential — you cannot accidentally put an unauthenticated admin console on the internet.

helm install zaris oci://registry-1.docker.io/clustron/zaris -n zaris \
--set manager.console.expose=ingress \
--set manager.console.ingress.host=zaris-console.example.com \
--set manager.console.ingress.tls.enabled=true \
--set manager.console.ingress.tls.clusterIssuer=letsencrypt-prod \
--set manager.security.enabled=true \
--set manager.security.adminPassword='<a-strong-password>' # or --set manager.security.existingSecret=<secret>

The manager seeds the admin on boot — before it becomes Ready — so the readiness-gated URL is never published to an un-provisioned cluster (which would let anyone race you to claim ClusterAdmin). Sign in with admin / your password; read the generated one with:

kubectl -n zaris get secret zaris-manager-admin -o jsonpath='{.data.admin-password}' | base64 -d

In production, prefer manager.security.existingSecret (keys admin-username / admin-password) so the password isn't rendered from values. Change it from the console after first sign-in.

The URL goes live only once the store is attached and serving. With manager.console.gateOnStoreAttached (default on) the manager gates readiness on GET /health/ready — a store attached and at least one node reporting fresh metrics — so a user opening the URL never lands on an empty console. A 503 in the first few seconds after install is expected; node attach is never blocked by this gate.

HTTPS with no browser warning (ingress): TLS terminates at the ingress controller using a cert your users already trust — cert-manager, or a Secret you provide — not Zaris's internal CA:

# cert-manager mints + auto-renews a publicly-trusted cert (e.g. Let's Encrypt)
helm install zaris oci://registry-1.docker.io/clustron/zaris -n zaris \
--set manager.console.expose=ingress \
--set manager.console.ingress.host=zaris-console.example.com \
--set manager.console.ingress.tls.enabled=true \
--set manager.console.ingress.tls.clusterIssuer=letsencrypt-prod

Any cluster (e.g. kind, no ingress controller):

helm install zaris oci://registry-1.docker.io/clustron/zaris -n zaris --set manager.console.expose=portForward
kubectl -n zaris port-forward svc/zaris-manager-console 8080:7810 # → http://localhost:8080

To run without a manager (orchestrator-run nodes only), install with --set manager.enabled=false.

Chart values reference

KeyDefaultMeaning
cluster.idzaris-k8sclusterId and the store name clients must use
node.replicas4node pod count (keep a multiple of replicationFactor)
node.replicationFactor2copies per partition (must be < replicas)
node.discovery.kubernetestruehot kubectl scale via ordinal + DNS discovery
node.image.repository / tagclustron/zaris-node / 1.1.0node image
node.clientPort / healthPort7861 / 7802client / probe ports
node.terminationGracePeriodSeconds40time allowed for the graceful drain before SIGKILL
node.tls.enabledfalsetransport TLS (mounted-secret model)
node.tls.certSecret""Secret of per-node <nodeId>.pfx leaves
node.tls.caPem""cluster CA PEM (supply with --set-file)
node.external.enabledfalsesecond (external) client listener + per-pod Services
node.external.clientPort7863external listener port (≠ client / health)
node.external.hostTemplate""per-pod advertised host ({ordinal} substituted)
node.external.service.typeNodePortNodePort or LoadBalancer
node.pdb.maxUnavailable1voluntary-disruption cap
manager.enabledtruedeploy the attach-mode manager + console
manager.attach.enabledtruemanager self-registers (adopts) the store on boot — no manual register step
manager.security.enabledfalsecontrol-plane auth (console + mgmt API). Required to expose the console externally
manager.security.adminUsername / adminPasswordadmin / ""first admin, seeded on boot when security is on (password required)
manager.security.existingSecret""use an existing Secret (keys admin-username/admin-password) instead of adminPassword
manager.console.exposeportForwardhow the console is reached: portForward (private, default) / ingress / loadBalancer
manager.console.gateOnStoreAttachedtruedon't publish the console URL until a store is attached and serving
manager.console.ingress.host""hostname for the console URL (required for ingress)
manager.console.ingress.tls.clusterIssuer""cert-manager ClusterIssuer to auto-mint a trusted cert (no browser warning)
manager.console.loadBalancer.sourceRanges[]CIDR allowlist for the console LB (empty = open to the internet)
manager.service.typeClusterIPinternal manager Service (mgmt port never exposed externally)

The full set of defaults and comments lives in the chart's values.yaml.

Troubleshooting

SymptomCause / fix
store name '<x>' does not match the cluster's id at connectThe connection-string path must equal cluster.id — it is the cluster id. Use .../zaris-k8s. The DI key / store handle is a local label and is irrelevant here.
TLS client: "no cluster CA was provided"Pass the CA: ?ca=/path/ca.pem (or -TlsCaCert).
TLS client: "certificate wasn't trusted"The CA doesn't match the cluster CA, or the leaf SAN lacks the dialed host. Regenerate leaves with the advertised host in the SAN.
Upgrade seems stuck; pods roll slowlyRollingUpdate is sequential (highest→lowest, one at a time). A multi-pod TLS roll takes minutes — normal.
Boot log shows more nodes than replicasStale CoreDNS after a rapid redeploy into the same names; the live-peer reconciler converges. Harmless.
[SECURITY] External … TLS is OFFExpected when external is on without TLS. Enable TLS for production external access.

Useful checks:

kubectl -n zaris get pods -o wide
kubectl -n zaris logs zaris-0 | grep -E "readyz|partition map|TLS ENABLED|ExternalListener|SECURITY"
kubectl -n zaris get endpoints zaris-client # the Ready pods the client Service routes to

Uninstall

helm uninstall zaris -n zaris
kubectl delete namespace zaris # also removes the cert / CA Secrets

Next steps