Zum Inhalt

Workshop: Hochverfügbarer K3s Edge-Cluster auf NVMe-SSDs

Der Betrieb eines einzelnen Kubernetes-Knotens an der Edge birgt ein enormes Ausfallrisiko (Single Point of Failure). Fällt die Stromversorgung aus oder überhitzt eine Komponente, sind Telemetrie-Gateways, Messdatenerfassung und Steuerungs-APIs schlagartig offline.

In diesem praxisnahen Workshop bauen wir einen echten 3-Knoten Hochverfügbarkeits-Cluster (HA) mit K3s auf drei Raspberry Pi 5 Platinen mit M.2 NVMe SSDs über PCIe Gen 3 auf. Wir nutzen das eingebettete etcd von K3s für absolute Konsistenz, richten MetalLB für virtuelle IP-Adressen (VIP) ein und sichern Workloads so ab, dass ein physischer Totalausfall eines Knotens ohne Unterbrechung abgefangen wird.


1. Topologie & Quorum-Mathematik

Eine hochverfügbare Kubernetes-Control-Plane verlangt stets eine ungerade Anzahl an abstimmungsberechtigten Knoten, um Split-Brain-Situationen im Netzwerk auszuschließen:

%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    subgraph Network["Lokales Gigabit-Netzwerk"]
        VIP["Virtuelle IP (MetalLB VIP)<br/>192.168.10.100"]
    end

    subgraph Cluster["3-Knoten HA etcd Quorum Mesh"]
        direction TB
        N1["rpi-node01 (.51)<br/>Control Plane + etcd 1"]
        N2["rpi-node02 (.52)<br/>Control Plane + etcd 2"]
        N3["rpi-node03 (.53)<br/>Control Plane + etcd 3"]

        N1 <-->|Raft Konsens| N2
        N2 <-->|Raft Konsens| N3
    end

    VIP -->|Aktiver ARP-Leader| N1
    N1 -.->|Quorum Failover| N2
    N2 -.->|Quorum Failover| N3

Die Raft-Quorum-Bedingung

Damit der Cluster bei Knotenausfällen handlungsfähig bleibt, benötigt etcd stets die absolute Mehrheit der Stimmen:

Quorum-Berechnung:
Quorum = (N ÷ 2) + 1
Für einen 3-Knoten-Cluster: Quorum = (3 ÷ 2) + 1 = 2 Knoten
Fehlertoleranz: Kann den Ausfall von 1 Knoten vollständig kompensieren.

Bei nur 2 Knoten führt der Ausfall eines Knotens dazu, dass nur noch 1 Knoten aktiv ist (weniger als das nötige Quorum von 2). Der gesamte Cluster schaltet dann zum Schutz vor Inkonsistenzen in den Nur-Lese-Zustand. Drei Knoten sind das mathematische Minimum für Hochverfügbarkeit.


2. Hardware-Architektur & Systemvorbereitung

Hardware-Komponenten

  1. Knoten: 3× Raspberry Pi 5 (mit 4 GB oder 8 GB RAM).
  2. Datenträger: 3× M.2 NVMe SSDs (z. B. Samsung 980 oder WD Black SN770) auf offiziellen Raspberry Pi M.2 HAT+ Boards.
  3. Stromversorgung: 3× 27 W USB-PD Netzteile oder PoE+ Switch mit entsprechenden HATs.
  4. Netzwerk: 1× Managed Gigabit Switch mit Cat6-Patchkabeln.

Vorbereitung auf allen 3 Knoten durchführen

Auf rpi-node01, rpi-node02 und rpi-node03 per SSH einloggen:

1. Feste IP-Adressen vergeben

  • rpi-node01: 192.168.10.51
  • rpi-node02: 192.168.10.52
  • rpi-node03: 192.168.10.53

2. Speicher-cgroups aktivieren

In /boot/firmware/cmdline.txt am Ende der Zeile cgroup_memory=1 cgroup_enable=memory anhängen und neu starten.

3. Swap deaktivieren

sudo dphys-swapfile swapoff
sudo dphys-swapfile uninstall
sudo systemctl disable dphys-swapfile

3. Ersten Master-Knoten initialisieren (rpi-node01)

Auf dem ersten Knoten (rpi-node01) wird der K3s-Cluster mit dem Flag --cluster-init initialisiert:

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-Erklärung

  • --cluster-init: Initialisiert ein verteiltes etcd-Cluster anstelle von SQLite.
  • --tls-san=192.168.10.50: Fügt eine virtuelle IP (VIP) zum TLS-Zertifikat hinzu, damit kubectl ohne Zertifikatsfehler über die VIP kommunizieren kann.
  • --disable traefik --disable servicelb: Gibt Ports und RAM für MetalLB und eigene Ingress-Controller frei.

Status prüfen:

kubectl get nodes -o wide

Sicherheitstoken für die weiteren Knoten auslesen:

sudo cat /var/lib/rancher/k3s/server/node-token

Token notieren: K10398f7e2d9...::server:7b2a9e...


4. Knoten 2 und 3 als vollwertige Master einbinden

Die Knoten 2 und 3 treten nicht als einfache Worker, sondern als gleichberechtigte Server-Knoten bei.

Knoten 2 beitreten lassen (rpi-node02)

Per SSH auf rpi-node02 einloggen:

curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
  --server https://192.168.10.51:6443 \
  --token <DEIN_CLUSTER_TOKEN> \
  --tls-san=192.168.10.50 \
  --tls-san=k8s.edge.local \
  --disable traefik \
  --disable servicelb \
  --write-kubeconfig-mode 644" sh -

Knoten 3 beitreten lassen (rpi-node03)

Per SSH auf rpi-node03 einloggen und denselben Befehl ausführen:

curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
  --server https://192.168.10.51:6443 \
  --token <DEIN_CLUSTER_TOKEN> \
  --tls-san=192.168.10.50 \
  --tls-san=k8s.edge.local \
  --disable traefik \
  --disable servicelb \
  --write-kubeconfig-mode 644" sh -

Hochverfügbarkeit überprüfen

Auf rpi-node01 zurückkehren und den Cluster-Status prüfen:

kubectl get nodes

Erwartetes Ergebnis:

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


5. Netzwerklastverteilung mit MetalLB (Layer 2)

Auf Bare-Metal-Platinen bleiben LoadBalancer-Dienste in Kubernetes ohne Clouddienst im Status <pending> hängen. MetalLB löst dies über Layer-2-ARP-Broadcasts im lokalen Netzwerk.

Schritt 1: MetalLB installieren

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.8/config/manifests/metallb-native.yaml

Warten, bis alle MetalLB-Pods laufen:

kubectl wait --namespace metallb-system \
  --for=condition=ready pod \
  --selector=app=metallb \
  --timeout=120s

Schritt 2: IP-Adressbereich konfigurieren

Datei metallb-config.yaml erstellen (IP-Bereich z. B. 192.168.10.100 bis 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

Anwenden:

kubectl apply -f metallb-config.yaml

6. Hochverfügbares Deployment mit Anti-Affinity

Wir stellen einen Nginx-Webserver mit 3 Replikaten und Pod-Anti-Affinity bereit. Anti-Affinity sorgt dafür, dass Kubernetes niemals zwei Pods auf denselben physischen Raspberry Pi legt:

Datei ha-workload.yaml erstellen:

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

Bereitstellen:

kubectl apply -f ha-workload.yaml

Dienst abfragen:

kubectl get svc ha-web-service

MetalLB weist die virtuelle IP 192.168.10.100 zu. Die Seite ist ab sofort netzwerkweit unter http://192.168.10.100 erreichbar!


7. Der Härtetest: Stromkabel unter Last ziehen

Jetzt testen wir die Hochverfügbarkeit unter realen Ausfallbedingungen:

  1. In einem Terminal die Pods überwachen:
    watch kubectl get pods -o wide
    
  2. In einem zweiten Terminal kontinuierlich Anfragen an die virtuelle IP senden:
    while true; do curl -s -I http://192.168.10.100 | grep HTTP; sleep 0.5; done
    
  3. Ziehe das Stromkabel von rpi-node01 (dem ersten Master-Knoten) ab!

Was passiert im System:

  • Der etcd-Raft-Konsens erkennt den Verlust von Knoten 1 innerhalb von 1 Sekunde.
  • Die Knoten 2 und 3 behalten das Quorum (2 von 3 Stimmen = 66 % Mehrheit).
  • MetalLB übernimmt die virtuelle IP 192.168.10.100 binnen 2 bis 3 Sekunden auf Knoten 2 oder 3 per Gratuitous-ARP.
  • Der curl-Befehl liefert ohne Unterbrechung weiterhin HTTP 200 Antworten!
  • Der Kubernetes Controller-Manager startet den ausgefallenen Pod automatisch auf einem der verbleibenden Knoten nach.

Steckst du das Stromkabel von rpi-node01 wieder ein, bootet die NVMe-SSD in rund 20 Sekunden, synchronisiert sich mit dem etcd-Transaktionsprotokoll und gliedert sich nahtlos wieder in das Cluster-Quorum ein.


Masterclass abgeschlossen

Herzlichen Glückwunsch! Du hast das gesamte Curriculum der Raspberry Pi Enterprise Masterclass gemeistert: von ARM-Silizium-Grundlagen und physikalischem Bus-Engineering bis zur ausfallsicheren Cluster-Orchestrierung.