| your Linux construction kit
Source
← Back to Overview

Kubernetes Cluster — self-assembling from sealed ISOs.

This is the real thing. A complete Kubernetes cluster that builds itself from ISOs. Three roles — master, etcd/control-plane, worker — each with its own sealed ISO containing a role-specific postinstall.sh. Boot the VMs. Walk away. Come back to a running cluster with Calico CNI, Helm, and WireGuard mesh.

The architecture

Three roles, three ISOs

Master
Salt master + WireGuard hub + kubectl + cluster-autobootstrap timer. Orchestrates everything. Does NOT run kubelet.
etcd (control-plane)
kubelet + kubeadm + kubectl + containerd. Runs kubeadm init (first node) or kubeadm join --control-plane (additional). Salt minion reports back to master.
Worker
kubelet + kubeadm + containerd. Joins cluster via kubeadm join. Salt minion for management. Runs your workloads.

How it self-assembles

The timeline

# Minute 0: Build ISOs (one per role)
./deploy.sh build   # builds master.iso, etcd-1.iso, worker-1.iso, worker-2.iso

# Minute 1: Deploy to Proxmox (or KVM, or bare metal)
# Each ISO: preseed install → poweroff → boot from disk → postinstall → poweroff

# Minute 15: All VMs powered off after postinstall
# Master has: Salt master, WireGuard hub, kubectl
# etcd nodes have: containerd, kubelet, Salt minion, WireGuard client
# Workers have: containerd, kubelet, Salt minion, WireGuard client

# Minute 16: All VMs started for production
# Master's wg-sync-salt.timer runs every 30s:
#   - Discovers Salt minions
#   - Reads WireGuard pubkeys from grains
#   - Pushes WireGuard configs to all nodes
#   - Generates Ansible inventory from Salt grains

# Minute 18: cluster-autobootstrap.timer runs every 60s:
#   - Finds first etcd node (control-plane)
#   - Runs kubeadm init on it
#   - Installs Calico CNI (Tigera operator)
#   - Joins remaining etcd nodes as control-planes
#   - Joins workers
#   - Pushes admin.conf to all control-planes
#   - Marks done when all nodes are Ready

# Minute 25: kubectl get nodes
# NAME      STATUS   ROLES           VERSION
# etcd-1    Ready    control-plane   v1.35.x
# worker-1  Ready    <none>          v1.35.x
# worker-2  Ready    <none>          v1.35.x
25 minutes from ISOs to running Kubernetes cluster. Zero SSH. Zero manual commands. The ISOs do everything.

The inventory

Define your cluster in environment variables

# Node spec: vmid name role lan_ip wg_cidr
MASTER_ID=999     MASTER_NAME=master    MASTER_LAN=10.100.10.20
ETCD_COUNT=1      ETCD_LAN_BASE=10.100.10.30
WORKER_COUNT=2    WORKER_LAN_BASE=10.100.10.40

# Results in:
# 999  master    master  10.100.10.20  10.78.0.1/16
# 1000 etcd-1    etcd    10.100.10.30  10.78.0.5/32
# 1010 worker-1  worker  10.100.10.40  10.78.0.11/32
# 1011 worker-2  worker  10.100.10.41  10.78.0.12/32

# Scale by changing counts:
ETCD_COUNT=3 WORKER_COUNT=5 ./deploy.sh build
# Builds 9 ISOs. Deploy. Walk away. Full HA cluster.

What each postinstall does

Master postinstall

APT sources → base packages → users + SSH hardening → WireGuard hub (star topology) → IP forwarding → Salt master + API → kubectl → Helm → nftables firewall → wg-sync-salt timer (30s) → cluster-autobootstrap timer (60s) → helm-autobootstrap timer → poweroff

etcd/worker postinstall

APT sources → base packages → users + SSH hardening → WireGuard key generation → Salt minion (points to master) → kernel tweaks (ip_forward, br_netfilter, overlay, swap off) → containerd (SystemdCgroup=true) → kubeadm + kubelet → nftables (k8s ports) → poweroff

The self-healing WireGuard mesh

wg-sync-salt: automatic peer management

Every 30 seconds, the master's wg-sync-salt timer:

  1. Queries Salt grains for every minion's WireGuard pubkey, CIDR, role, and LAN IP
  2. Regenerates the master's wg0.conf with all peers
  3. Pushes WireGuard client configs to each minion via Salt
  4. Runs wg syncconf to apply changes without restarting the interface
  5. Generates Ansible inventory from Salt grains (master/etcd/workers groups)

New node boots → Salt minion connects → grains include WireGuard pubkey → wg-sync-salt picks it up → pushes config → node joins the mesh. Automatic.

Clone and replicate

ZFS makes Kubernetes disposable

# Snapshot the entire cluster state
for node in master etcd-1 worker-1 worker-2; do
  ssh admin@${node} "sudo ksnap"
done

# Worker dies? Don't fix it. Clone from golden and rejoin.
zfs clone rpool/vms/worker-golden@base rpool/vms/worker-3
# Boot → postinstall → Salt minion → WireGuard → kubeadm join → Ready

# Upgrade the cluster? Snapshot first.
ssh admin@etcd-1 "sudo kbe create pre-k8s-upgrade"
# Run kubeadm upgrade. If it breaks:
ssh admin@etcd-1 "sudo kbe rollback pre-k8s-upgrade"
# 15 seconds. Cluster is back to pre-upgrade state.

# Replicate the entire cluster to DR site
for node in master etcd-1 worker-1 worker-2; do
  ssh admin@${node} "sudo zfs send -R rpool@backup | ssh dr-site zfs recv backup/${node}"
done
Kubernetes nodes are cattle. ZFS makes them infinitely clonable cattle with instant rollback. That's the combination nobody else has.

The commands that matter

slist
Formatted fleet inventory — minion ID, host, OS, LAN IP, WG IP, CPUs, RAM, role
sping
Ping all Salt minions — instant cluster health check
sknodes
kubectl get nodes -o wide via Salt on the control-plane
skpods
kubectl get pods -A -o wide via Salt
khelp
Full kubectl/helm/calico helper command reference
Every node is a bash script. Every step is auditable. The master postinstall is ~800 lines. The node postinstall is ~400 lines. No Ansible. No Terraform. No vendor SDK. The ISOs bootstrap themselves, discover each other via Salt, mesh via WireGuard, and converge into a Kubernetes cluster. All from ./deploy.sh build.