Workshop: High-Availability K3s Edge Cluster on NVMe SSDs
Deploying a single Kubernetes node at the edge creates a dangerous single point of failure (SPOF). If an operator trips over a power cable or an internal component overheats, your entire edge telemetry, control loop, and local API gateway drops offline.
In this enterprise hands-on workshop, we engineer a 3-Node High-Availability (HA) K3s Kubernetes Cluster across three Raspberry Pi 5 boards running from PCIe Gen 3 M.2 NVMe SSDs. We leverage K3s's embedded etcd engine to guarantee quorum, configure MetalLB for virtual IP (VIP) load balancing, and implement resilient pod scheduling capable of withstanding unexpected physical node deaths without service interruption.
1. High-Availability Cluster Topology & Quorum Math
A production Kubernetes control plane requires an odd number of voting members to reach consensus and prevent split-brain partitions.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
subgraph Network["Dual-Stack Edge LAN (Gigabit Switch)"]
VIP["Virtual IP (MetalLB VIP: 192.168.10.100)"]
end
subgraph Cluster["3-Node HA Embedded etcd Mesh (Port 2379/2380)"]
N1["rpi-node01 (192.168.10.51)<br/>Control Plane + etcd Member 1"]
N2["rpi-node02 (192.168.10.52)<br/>Control Plane + etcd Member 2"]
N3["rpi-node03 (192.168.10.53)<br/>Control Plane + etcd Member 3"]
N1 <===>|Raft Consensus| N2
N2 <===>|Raft Consensus| N3
N3 <===>|Raft Consensus| N1
end
VIP -.->|Active ARP Leader| N1
VIP -.->|Failover Candidate| N2
VIP -.->|Failover Candidate| N3
The Raft Quorum Requirement
To survive physical failures, etcd requires a strict majority of nodes to remain active:
Quorum Formula:
Quorum = (N ÷ 2) + 1
For a 3-node cluster: Quorum = (3 ÷ 2) + 1 = 2 nodes
Fault Tolerance: Can withstand 1 node failure without loss of cluster consensus.
If you deploy only 2 nodes, a failure of 1 node leaves only 1 active node (which is less than the required 2-node quorum), causing the entire cluster to lock in read-only panic. Three nodes is the absolute mathematical minimum for a High-Availability cluster.
2. Hardware Architecture & Pre-Flight Checklist
Hardware Bill of Materials
- Nodes: 3× Raspberry Pi 5 (4GB or 8GB RAM).
- Storage: 3× M.2 NVMe SSDs (e.g., Samsung 980, WD Black SN770) mounted on official Raspberry Pi M.2 HAT+ boards.
- Power: 3× 27W USB-PD power supplies or an 802.3at PoE+ Gigabit switch with PoE HATs.
- Networking: 1× 8-port Managed Gigabit Switch with Cat6 patch cables.
Host Pre-Flight Configuration (Execute on All 3 Nodes)
Log in to rpi-node01, rpi-node02, and rpi-node03 via SSH:
1. Assign Static IP Addresses
Ensure each node binds a predictable static IPv4 on your subnet:
- rpi-node01: 192.168.10.51
- rpi-node02: 192.168.10.52
- rpi-node03: 192.168.10.53
2. Enable cgroups Memory Accounting
In /boot/firmware/cmdline.txt, append cgroup_memory=1 cgroup_enable=memory and reboot.
3. Disable Swap Permanently
Kubernetes memory scheduling requires swap to be deactivated:
3. Initializing the Primary Control Plane Node (rpi-node01)
On the first node (rpi-node01), initialize the K3s cluster using the --cluster-init flag to instantiate the embedded etcd datastore:
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
--cluster-init \
--tls-san=192.168.10.50 \
--tls-san=k8s.edge.local \
--disable traefik \
--disable servicelb \
--write-kubeconfig-mode 644" sh -
Parameter Breakdown
--cluster-init: Instructs K3s to initialize an embedded distributed etcd cluster rather than using a single local SQLite database.--tls-san=192.168.10.50: Adds a Virtual IP (VIP) to the TLS certificate SANs, allowing you to connectkubectlto the cluster via a load-balanced VIP without SSL certificate validation errors.--disable traefik --disable servicelb: Frees up system ports and RAM for production ingress controllers and MetalLB.
Verify that Node 1 is healthy and running:
Extract the shared cluster join token:
Save this token: K10398f7e2d9...::server:7b2a9e...
4. Joining Nodes 2 and 3 into the Embedded etcd Mesh
To form an HA control plane, Node 2 and Node 3 must join as control-plane server peers, not simple worker agents.
Joining Node 2 (rpi-node02)
SSH into rpi-node02 and run:
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
--server https://192.168.10.51:6443 \
--token <YOUR_CLUSTER_TOKEN> \
--tls-san=192.168.10.50 \
--tls-san=k8s.edge.local \
--disable traefik \
--disable servicelb \
--write-kubeconfig-mode 644" sh -
Joining Node 3 (rpi-node03)
SSH into rpi-node03 and execute the exact same command:
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
--server https://192.168.10.51:6443 \
--token <YOUR_CLUSTER_TOKEN> \
--tls-san=192.168.10.50 \
--tls-san=k8s.edge.local \
--disable traefik \
--disable servicelb \
--write-kubeconfig-mode 644" sh -
Confirming High-Availability Quorum
Return to rpi-node01 and verify the cluster status:
Expected output:
NAME STATUS ROLES AGE VERSION
rpi-node01 Ready control-plane,etcd,master 8m12s v1.30.2+k3s1
rpi-node02 Ready control-plane,etcd,master 3m45s v1.30.2+k3s1
rpi-node03 Ready control-plane,etcd,master 90s v1.30.2+k3s1
Confirm that the etcd Raft cluster has 3 registered voting members:
5. Network Load Balancing: Deploying MetalLB (Layer 2 Mode)
Without a cloud provider's external load balancer, Kubernetes Service objects of type LoadBalancer remain permanently stuck in <pending>. MetalLB solves this on bare-metal networks by broadcasting Layer 2 ARP packets to bind real LAN IP addresses to cluster services.
Step 1: Install MetalLB via Official Manifests
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml
Wait until all MetalLB controller and speaker pods are running:
kubectl wait --namespace metallb-system \
--for=condition=ready pod \
--selector=app=metallb \
--timeout=120s
Step 2: Configure IP Address Pool and L2 Advertisement
Create metallb-config.yaml to allocate a pool of available IP addresses on your local subnet (e.g., 192.168.10.100 to 192.168.10.110):
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: edge-pool
namespace: metallb-system
spec:
addresses:
- 192.168.10.100-192.168.10.110
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: l2-advertisement
namespace: metallb-system
spec:
ipAddressPools:
- edge-pool
Apply the configuration:
6. Deploying High-Availability Workloads with Anti-Affinity
Deploy an Nginx web service configured with 3 replicas and Pod Anti-Affinity. Anti-affinity rules instruct the Kubernetes scheduler to spread replicas across different physical Raspberry Pi nodes so that losing any single node does not terminate the application.
Create ha-workload.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ha-edge-web
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: ha-web
template:
metadata:
labels:
app: ha-web
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- ha-web
topologyKey: "kubernetes.io/hostname"
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 64Mi
---
apiVersion: v1
kind: Service
metadata:
name: ha-web-service
namespace: default
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 80
selector:
app: ha-web
Apply the deployment:
Inspect service IP assignment:
Output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ha-web-service LoadBalancer 10.43.184.212 192.168.10.100 80:31245/TCP 45s
MetalLB has assigned Virtual IP 192.168.10.100. You can now open http://192.168.10.100 from any browser on your network!
7. Fault-Tolerance Testing: Simulating Sudden Physical Death
Now verify high-availability resilience under actual hardware failure conditions.
- Keep a terminal open on your workstation running:
- In a second terminal, send continuous HTTP requests to the MetalLB Virtual IP:
- Pull the power cable out of
rpi-node01(the initial master node).
What Happens:
- The etcd Raft consensus detects the loss of member 1 within 1,000 milliseconds.
- Nodes 2 and 3 maintain quorum (2 active nodes out of 3 = 66% majority).
- MetalLB speaker on Node 2 or 3 sends an unsolicited gratuitous ARP packet, taking ownership of VIP
192.168.10.100within 2 to 3 seconds. - The
curlcommand resumes serving traffic without HTTP 500 errors! - The Kubernetes controller manager evicts the dead pod and schedules a replacement on the remaining nodes.
Plug rpi-node01 back into power. Within 45 seconds, the node reboots, catches up with the etcd transaction log, and rejoins the cluster quorum seamlessly!
Masterclass Completed
Congratulations! You have mastered the entire Raspberry Pi Enterprise Curriculum: from low-level ARM silicon architecture, bus physics, and kernel recovery to automated cluster orchestration.