Deploy on Kubernetes

Run Ursula on Kubernetes with the Helm chart.

The Ursula Helm chart starts a static-membership Raft cluster with stable StatefulSet pod identities, durable per-pod Raft logs, a headless peer Service, a client/admin Service, a quorum-protecting PodDisruptionBudget, and a Helm test powered by ursulactl. The default chart also adds a stateless gateway for single-URL external access and soft multi-pod spread hints.

The chart is for fresh static-membership clusters. It does not perform online Raft voter expansion, voter removal, leader handoff during Kubernetes rolling updates, or post-bootstrap membership flag mutation. Those operations belong to the future Ursula operator workflow.

Install

The chart and server images are published to GHCR on every release. One command starts a three-voter cluster from the published artifacts:

helm install ursula oci://ghcr.io/tonbo-io/charts/ursula --version 0.2.0

The chart defaults to the ghcr.io/tonbo-io/ursula image pinned to the chart's appVersion, so no image overrides are needed. The default install starts three voters and 64 Raft groups. To use your own registry mirror, set global.image.repository, global.image.tag, and optionally global.imagePullSecrets.

Install from a local build

To run unreleased code, build the image from the repository and install the chart from the working tree instead:

docker build -t ursula:dev .

For kind:

kind load docker-image ursula:dev

Then point the chart at the local image:

helm install ursula charts/ursula \
  --set global.image.repository=ursula \
  --set global.image.tag=dev \
  --set global.image.pullPolicy=Never

Verify readiness

helm test ursula

The test mounts the chart-generated cluster-manifest.json and runs ursulactl wait-ready. It succeeds only when every configured node reports the expected Raft group count and every group has a leader.

Finish bootstrap

Fresh clusters start with raft.initMembershipPerGroup=true so Ursula can initialize per-group Raft membership automatically. After helm test passes, update your values and run:

helm upgrade ursula oci://ghcr.io/tonbo-io/charts/ursula --version 0.2.0 \
  --reuse-values \
  --set raft.initMembershipPerGroup=false

Use the same chart source you installed from (charts/ursula for a local build). If you do not use --reuse-values, repeat your image overrides or use a values file so the upgrade does not roll pods back to the chart default image.

Keep this value false for normal restarts and upgrades. A future Kubernetes operator will own this transition automatically.

Static membership

server.replicaCount controls the initial voter set for a fresh cluster. The chart supports 1, 3, and 5; production clusters should use 3 or 5.

Changing server.replicaCount on an initialized cluster is not safe Raft voter reconfiguration. Safe scaling requires an operator workflow that adds learners, waits for catch-up, promotes voters, and removes old voters.

Cold storage

Cold storage is disabled by default. Production multi-node clusters should use S3 or an S3-compatible object store:

s3:
  bucket: my-ursula-bucket
  region: us-east-1
  prefix: ursula-prod

coldStorage:
  enabled: true

Prefer workload identity through ServiceAccount annotations instead of static S3 credentials:

serviceAccount:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/ursula-s3

For MinIO or another S3-compatible backend, set s3.endpoint.

Operational endpoints

Ursula exposes a single admin endpoint on the client plane:

  • GET /__ursula/metrics: per-node JSON snapshot with Raft group state, hot bytes, cold backpressure counters, and per-core mailbox depth.

There are no dedicated /__ursula/healthz or /__ursula/readyz probes yet. Server HTTP probes are disabled in the current chart. Gateway probes use TCP socket checks, and cluster-level readiness should come from helm test or ursulactl wait-ready.

The mutating operator surface (raft operations, maintenance drain, cold-flush trigger) lives on the admin plane bound to pod loopback (127.0.0.1:4438); reach it with kubectl port-forward <pod> 4438:4438.

On pod termination the server handles SIGTERM by draining its listeners: it stops accepting, finishes in-flight requests, and exits 0, forcing exit after 20 seconds if a request hangs. Keep terminationGracePeriodSeconds above that (the Kubernetes default of 30s is fine). For zero-disruption rollouts, still drain leaderships first with ursulactl restart.

Access locally

kubectl port-forward svc/ursula 4437:4437
curl http://127.0.0.1:4437/__ursula/metrics

Upgrade limits

Until the operator exists, Kubernetes StatefulSet rolling updates do not transfer leaders, coordinate applied-index catch-up, or mutate Raft membership. Use ursulactl restart manually for drain-aware rolling restarts when you need operationally safe restarts on an initialized cluster.