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.