Deploy on GKE
This guide takes you from an empty Google Cloud project to a running Zaris cluster on Google Kubernetes Engine (GKE) — creating the cluster with gcloud, deploying the Helm chart, and then exposing the Web Console privately, over public HTTP, and finally over trusted HTTPS. It also covers pinning Zaris to a dedicated cache node pool and enabling cluster (data-plane) TLS.
This is a cloud-specific walkthrough. For how the chart works — the StatefulSet model, the connection-string client, scaling, and every value — read the Kubernetes overview first. This page assumes those concepts and focuses on the GKE-specific steps.
Prerequisites
- A Google Cloud project with billing enabled and permission to create GKE clusters.
- The Google Cloud CLI (
gcloud) pluskubectl+helmv3 on your machine.- First time? Use Google Cloud Shell. It runs in the browser with
gcloud,kubectl, andhelmpreinstalled and already signed in — no local setup. The one exception:kubectl port-forward(used for the private console) needs a local terminal, because it forwards to your localhost.
- First time? Use Google Cloud Shell. It runs in the browser with
Enable the GKE (Kubernetes Engine) API, and set your project and a default zone so later commands can omit them:
gcloud services enable container.googleapis.com
gcloud config set project <your-project-id>
gcloud config set compute/zone us-central1-a
Create the GKE cluster
For a test, create a standard zonal cluster — a single zone keeps it simple and cheap, and Standard mode (unlike Autopilot) gives you the node-pool control the cache-tier section below relies on:
gcloud container clusters create zaris-gke \
--zone us-central1-a \
--num-nodes 3 \
--machine-type e2-standard-4
GKE also offers Autopilot, where Google manages nodes for you and you pay per-pod. It is a great default for general workloads, but this guide uses Standard because the dedicated cache node pool below needs direct control over node pools, labels, and taints — which Autopilot abstracts away.
Fetch cluster credentials into your kubeconfig and confirm the nodes are Ready:
gcloud container clusters get-credentials zaris-gke --zone us-central1-a
kubectl get nodes
You should see 3 nodes in Ready state.
(Optional) Dedicated cache node pool
For production it is a common pattern to keep the cache tier on its own VMs, separate from your application tier — so cache pods get predictable resources and app pods never crowd them (or vice-versa). GKE models this with node pools plus a taint/label pair.
Add a dedicated cache pool to the cluster, labelled and tainted so only Zaris lands on it:
gcloud container node-pools create cache \
--cluster zaris-gke \
--zone us-central1-a \
--num-nodes 3 \
--machine-type e2-standard-4 \
--node-labels workload=zaris \
--node-taints workload=zaris:NoSchedule
How the two settings work together:
- The taint
workload=zaris:NoSchedulerepels everything from the cache nodes — ordinary app pods (which carry no matching toleration) will never schedule there. - The label
workload=zarisis what Zaris'snodeSelectortargets — it keeps Zaris pinned to the cache pool. - Because Zaris also carries a toleration for that taint, it is the one workload allowed onto the cache nodes. Your application
Deployments need no change — they simply keep landing on the cluster's default pool.
Pin Zaris to the cache pool with a values file (used in the deploy step below):
# cache-pool.yaml — pin the whole chart (nodes + manager) to the cache pool
node:
nodeSelector:
workload: zaris
tolerations:
- key: workload
operator: Equal
value: zaris
effect: NoSchedule
manager:
nodeSelector:
workload: zaris
tolerations:
- key: workload
operator: Equal
value: zaris
effect: NoSchedule
Add -f cache-pool.yaml to the helm install in the next section.
With RF 2, node.replicas should be a multiple of the replication factor so every partition has both copies on distinct pods. On a 3-node cache pool you have three sensible choices:
- RF 3 (
--set node.replicationFactor=3 --set node.replicas=3) — every partition has a copy on all three nodes; survives 2 node losses. Simplest for exactly 3 nodes. - RF 2, replicas 3 — the chart warns that one partition ends up single-copy (3 ÷ 2 doesn't divide evenly); tolerable for non-critical data, but not fully durable.
- 4 cache nodes (
--num-nodes 4) with RF 2, replicas 4 — clean 2× replication with room to lose any one node.
Deploy Zaris (private first)
Start private and unauthenticated — the safe default. Create a namespace and install the chart (the store is auto-attached on deploy):
kubectl create namespace zaris
helm install zaris oci://registry-1.docker.io/clustron/zaris -n zaris \
--set node.replicas=4 \
--set node.replicationFactor=2
The chart image is public — pulling it needs no Docker login. Add -f cache-pool.yaml here if you set up a dedicated cache pool above.
Wait for the nodes to become Ready (a Ready pod already owns and serves its share of the data):
kubectl -n zaris rollout status statefulset/zaris
Open the console privately with a port-forward — this needs no security because only you, through your kubeconfig, can reach it:
kubectl -n zaris port-forward svc/zaris-manager-console 8080:7810
# → http://localhost:8080
kubectl port-forward tunnels to your localhost, so run it from a local terminal — not Cloud Shell. A 503 for the first few seconds is normal; the console URL is readiness-gated and goes live once a store is attached and serving.
Expose the console publicly (LoadBalancer, HTTP)
To reach the console without a port-forward, give it a public cloud LoadBalancer. On GKE a Service of type=LoadBalancer provisions a regional external network load balancer with a public IP automatically. External exposure always requires control-plane security — the chart refuses to render loadBalancer (or ingress) unless manager.security.enabled=true with an admin credential, so you can never accidentally put an unauthenticated admin console on the internet.
helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values \
--set manager.console.expose=loadBalancer \
--set manager.security.enabled=true \
--set manager.security.adminUsername=admin \
--set manager.security.adminPassword='<change-me>' \
--set 'manager.console.loadBalancer.sourceRanges={203.0.113.7/32}' # your IP — recommended
sourceRanges is a CIDR allowlist — restrict it to your IP (/32) rather than leaving the console open to the whole internet. The manager self-provisions the admin before the pod is Ready, so the URL is never published un-provisioned; still, change admin/admin — those defaults must not ship. In production prefer --set manager.security.existingSecret=<secret> (a Secret with keys admin-username/admin-password) over an inline password.
Watch for the public IP to be assigned:
kubectl -n zaris get svc zaris-manager-console -w
When EXTERNAL-IP changes from <pending> to an address, browse to http://<external-ip> and sign in.
A network LoadBalancer is a layer-4 (TCP) load balancer — it does not terminate TLS, so the console is served over plain HTTP. Fine for a quick internal demo; for anything real, use one of the HTTPS paths below.
Console over HTTPS
HTTPS requires two things a bare LoadBalancer can't give you: something that terminates TLS and a DNS hostname (a certificate is issued for a name, not an IP). GKE gives you two ways to get there — a portable ingress-nginx path, and a Google-native managed-certificate path.
(a) ingress-nginx + cert-manager + Let's Encrypt (recommended)
This path is portable across any Kubernetes cluster and is the best starting point. The flow is: install an ingress controller → point DNS at it → install cert-manager → issue a trusted cert automatically.
1. Install the ingress-nginx controller (creates its own external network LoadBalancer):
helm install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
-n ingress-nginx --create-namespace
2. Get the controller's external IP:
kubectl -n ingress-nginx get svc ingress-nginx-controller -w
3. Point DNS at it. Create a DNS A record mapping your hostname to that IP (in Cloud DNS, or whatever provider hosts your zone):
zaris-console.example.com → <ingress-external-ip>
4. Install cert-manager (mints and auto-renews the certificate):
helm install cert-manager cert-manager \
--repo https://charts.jetstack.io \
-n cert-manager --create-namespace \
--set crds.enabled=true
5. Apply a Let's Encrypt ClusterIssuer (HTTP-01 challenge, solved through the nginx ingress):
kubectl apply -f - <<'EOF'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: you@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
EOF
6. Deploy the console as an Ingress with TLS:
helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values \
--set manager.console.expose=ingress \
--set manager.console.ingress.className=nginx \
--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='<change-me>'
cert-manager sees the Ingress, satisfies the HTTP-01 challenge, and issues the certificate (a minute or two the first time). The result: https://zaris-console.example.com with a publicly-trusted cert and no browser warning. TLS terminates at the ingress using that cert — never Zaris's internal CA.
If you already hold a cert, skip cert-manager and set --set manager.console.ingress.tls.secretName=<your-tls-secret> instead of clusterIssuer. The Secret must be a standard kubernetes.io/tls Secret in the zaris namespace.
(b) Google-managed certificate + GKE Ingress
If you prefer Google's native path, GKE's built-in Ingress provisions a global external Application Load Balancer (GCLB) that terminates TLS using a Google-managed certificate — no cert-manager, no ingress controller to install.
-
Reserve a global static IP and create a DNS A record for your host pointing at it:
gcloud compute addresses create zaris-console-ip --global
gcloud compute addresses describe zaris-console-ip --global --format='value(address)' -
Create a
ManagedCertificatereferencing the same host:kubectl apply -f - <<'EOF'
apiVersion: networking.gke.io/v1
kind: ManagedCertificate
metadata:
name: zaris-console-cert
namespace: zaris
spec:
domains:
- zaris-console.example.com
EOF -
Deploy with
manager.console.expose=ingress, but note the className and annotations differ from path (a): GKE Ingress uses--set manager.console.ingress.className=gceand needs annotations wiring the static IP (kubernetes.io/ingress.global-static-ip-name) and the managed cert (networking.gke.io/managed-certificates). Set these viamanager.console.ingress.annotationsin a values file, and leavemanager.console.ingress.tls.enabledoff (GCLB terminates TLS from the ManagedCertificate, not from a cluster Secret). Google-managed certs can take 15–60 minutes to goActiveon first provisioning.
For a first deployment, path (a) is simpler and provisions faster; reach for (b) when you specifically want a Google-managed cert and the global load balancer.
Cluster TLS (node-to-node + client encryption)
The console TLS above secures the browser↔console hop. A separate, independent layer secures the data plane — node↔node replication and client↔node traffic — via node.tls.enabled with a cluster CA and one per-node leaf certificate mounted from a Secret.
Generating the CA and per-node leaves is covered in depth in the security docs — follow those to produce ca.pem and a Secret of <nodeId>.pfx leaves:
- CA and trust modes — the four trust models and which to pick.
- Certificate management — generating the CA + per-node multi-SAN leaves and creating the Secret.
Once you have the CA and the cert Secret, enable data-plane TLS:
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=ca.pem
node.tls.certSecret is a Secret whose keys are per-node <nodeId>.pfx leaves; node.tls.caPem is supplied from a local file with --set-file. Clients then connect with the zariss:// scheme and the cluster CA:
zariss://<host>:7861/zaris-k8s?ca=/path/ca.pem
Connect your app
There is one store per release, named by cluster.id (default zaris-k8s) — that value is both the cluster id and the store name your clients use. In-cluster applications connect through the client Service DNS:
# plaintext
zaris://zaris-client.zaris.svc.cluster.local:7861/zaris-k8s
# TLS (when cluster TLS is enabled)
zariss://zaris-client.zaris.svc.cluster.local:7861/zaris-k8s?ca=/path/ca.pem
Replace zaris in the DNS name with your namespace, and zaris-k8s with your cluster.id if you changed it. See the Kubernetes overview for the C# / ASP.NET and PowerShell client examples.
Scale
Scaling has two independent dimensions on GKE:
More VMs (capacity for pods) — resize the cluster or a specific node pool:
# resize the whole cluster's default pool
gcloud container clusters resize zaris-gke --zone us-central1-a --num-nodes 4
# or resize just the cache pool
gcloud container clusters resize zaris-gke --zone us-central1-a \
--node-pool cache --num-nodes 4
More Zaris pods (capacity + partitions) — scale the chart. Scaling out is lossless resharding — new pods join and the partition map grows onto them with no restart of existing pods:
helm upgrade zaris oci://registry-1.docker.io/clustron/zaris -n zaris --reuse-values \
--set node.replicas=6
Keep node.replicas a multiple of node.replicationFactor. Scale in one step at a time (it is not yet drain-safe) — see the scaling notes.
Tear down (stop billing)
The worker VMs, load balancers, and static IPs cost money even when nothing is using them, and GKE also charges a per-cluster management fee beyond the free tier. When you are finished, delete the cluster — it removes the cluster, all node pools, and the load balancers created in this guide in one step:
gcloud container clusters delete zaris-gke --zone us-central1-a
If you reserved a global static IP for the Google-managed cert path, release it too, and double-check nothing lingers:
gcloud compute addresses delete zaris-console-ip --global
gcloud container clusters list
This is irreversible — it deletes the GKE cluster, all node pools, and any data still in the cluster.