Why multi-node?
- Horizontal scale — more nodes, more sandboxes.
- Replicated storage — Longhorn keeps multiple replicas of every volume across nodes (
numberOfReplicasconfigurable). 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..3on:31007. - etcd quorum spans the three servers. One server can fail without losing the control plane.
- Backend labels decide where a sandbox lands.
--backend kfdis restricted tonode1andnode4;--backend kqltonode1,node2,node3;--backend k7dtonode1andnode2. - k7d nodes run the node-local
k7ddaemon (systemd unit, owns everyruntimeClassName: k7microVM on that node) plus its XFS disk pool at/var/lib/k7d/disks. - The
k7-agentDaemonSet runs on every node. The k7-api forwards k7dpause/resume/forkto 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 fork7 nodes storage. - Longhorn replicas of each
kata-qemu-longhornPVC are spread acrosskql-capable nodes (longhorn_replicas=2keeps two copies;dataLocality: best-effortkeeps one local to the pod). - Devmapper thin-pools and k7d disk pools are per-node and not replicated —
kfdsandboxes 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):
--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.
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)/2server failures.
Mixed backends per node
A node can advertise both backends ink7_backends:
kata and kata-qemu), merges containerd configs, and labels the node with both:
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.
/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: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
Afterk7 install -i inventory.ini:
Ready, Cilium pods Running, the k7-api Deployment with 1/1 ready, and Longhorn replicas distributed across nodes.
Reference
- Example inventory:
src/k7/deploy/inventory.ini.example - Ansible playbook:
src/k7/deploy/k7-install-node.yaml
