Skip to main content
Katakate scales horizontally via multi-node K3s clusters, configured with an Ansible-style inventory. This guide explains the inventory format, the HA story, and how to mix backends across nodes.

Why multi-node?

  • Horizontal scale — more nodes, more sandboxes.
  • Replicated storage — Longhorn keeps multiple replicas of every volume across nodes (numberOfReplicas configurable). Data survives a node failure.
  • Workload locality — pods land on nodes whose backend matches the requested --backend.
  • High availability of the control plane — embedded etcd across 3+ K3s servers.

Topology at a glance

A typical 3-server + 1-agent cluster with mixed backends (including all-backend nodes running k7d) and Longhorn replication: What this shows:
  • Any server can serve the API NodePort — clients can hit any of node1..3 on :31007.
  • etcd quorum spans the three servers. One server can fail without losing the control plane.
  • Backend labels decide where a sandbox lands. --backend kfd is restricted to node1 and node4; --backend kql to node1, node2, node3; --backend k7d to node1 and node2.
  • k7d nodes run the node-local k7d daemon (systemd unit, owns every runtimeClassName: k7 microVM on that node) plus its XFS disk pool at /var/lib/k7d/disks.
  • The k7-agent DaemonSet runs on every node. The k7-api forwards k7d pause / resume / fork to the agent on the sandbox’s node (shared-token auth, ingress restricted to the k7-api pod by a CiliumNetworkPolicy), so k7d VM ops work through the API regardless of which node hosts the sandbox. It also serves per-node storage-pool stats for k7 nodes storage.
  • Longhorn replicas of each kata-qemu-longhorn PVC are spread across kql-capable nodes (longhorn_replicas=2 keeps two copies; dataLocality: best-effort keeps one local to the pod).
  • Devmapper thin-pools and k7d disk pools are per-node and not replicated — kfd sandboxes are ephemeral by design, and k7d fork/snapshot trees are host-local (a fork lands on the source’s node; cross-node fork is on the k7d roadmap).

Inventory format

k7 install -i inventory.ini reads a standard Ansible INI inventory. Each host declares which backend(s) it supports via k7_backends (comma-separated). A complete example (src/k7/deploy/inventory.ini.example):
For a 3-node HA cluster with all backends on every node, one command from the first master brings everything up:
Don’t pass --backend alongside -i: the per-host k7_backends in the inventory is authoritative (an explicit --backend overrides it).
On dual-NVMe boxes where the OS lives on one disk and the other is a raw spare, omit k7_devmapper_disk: NVMe enumeration (nvme0n1 vs nvme1n1) is not stable across reboots, so a hardcoded device can point at the OS disk after a reboot. The playbook auto-detects the empty non-root whole disk. Set it only when a node has several spare disks and you must pick a specific one.

Per-host variables

Group variables ([k7_cluster:vars])

Roles: servers vs agents

  • [k7_servers] — K3s control-plane nodes. The first host listed becomes the cluster-init server; subsequent servers join it (HA via embedded etcd).
  • [k7_agents] — K3s worker nodes. They join the first server with the K3s join token.
Servers can also schedule sandboxes (they aren’t tainted).

Single-master vs HA

  • 1 server: the simplest setup; no HA. Failure of the server takes the cluster down.
  • 2 servers: avoid — embedded etcd has no fault tolerance with even counts. Better: 1 server + 1 agent.
  • 3+ servers (odd numbers): full HA. Quorum tolerates (N-1)/2 server failures.

Mixed backends per node

A node can advertise both backends in k7_backends:
The playbook installs both Kata runtime classes (kata and kata-qemu), merges containerd configs, and labels the node with both:
When you k7 create --backend kql ..., the scheduler uses nodeSelector: { k7.katakate.org/backend-kata-qemu-longhorn: "true" } to land the pod on a compatible node. If no compatible node exists, k7 detects the FailedScheduling event and returns a clear error listing which backends are actually available.

Longhorn topology and replication

Longhorn is configured to be topology-aware:
  • volumeBindingMode: WaitForFirstConsumer — the PVC binds when the pod is scheduled, ensuring the volume is created on the node where the pod will run.
  • dataLocality: best-effort — Longhorn keeps one replica on the same node as the pod (reduces network reads).
  • replica-auto-balance: best-effort — replicas auto-spread across nodes.
Practical effect: writing to /mnt/state from inside the sandbox stays on the local node when possible, and the data has redundant replicas elsewhere in the cluster. If the pod’s node fails, the Deployment reschedules onto another node and Longhorn re-attaches the volume from a surviving replica.

Joining nodes incrementally

You don’t have to install everything in one shot. To add an agent to a running cluster:
To add a new server (HA):
For a fresh 3-server HA cluster, prefer the inventory-based install — it handles token distribution and join ordering automatically.

CNI choice across the cluster

Cilium (the default) is cluster-wide and applies its eBPF datapath uniformly across nodes. FQDN egress works regardless of which node the sandbox lands on. If you pass --cni flannel, every node uses Flannel and FQDN egress is unavailable cluster-wide.

Verification

After k7 install -i inventory.ini:
A clean cluster will show all nodes Ready, Cilium pods Running, the k7-api Deployment with 1/1 ready, and Longhorn replicas distributed across nodes.

Reference