Deployment

Kubernetes-Cluster mit k3s einrichten

Schritt-für-Schritt-Anleitung für einen hochverfügbaren Kubernetes-Cluster mit drei Servern. WireGuard-Mesh, k3s, Longhorn und erste Workloads.

AutorMarvin Strauch
Veröffentlicht am18. Mai 2026
Min. Lesezeit~46 min
Wörter8.000
Schwierigkeit Experte
StackKubernetes · k3s · WireGuard · Longhorn · Cluster · Hochverfügbarkeit

Diese Anleitung zeigt, wie Sie einen hochverfügbaren Kubernetes-Cluster mit drei Servern einrichten. Sie verbinden drei dataforest Seeds über ein verschlüsseltes WireGuard-Mesh-Netzwerk, installieren k3s als leichtgewichtige Kubernetes-Distribution, richten Longhorn für replizierte Speichervolumes ein und deployen eine erste Anwendung. Am Ende verfügen Sie über einen produktionsfähigen Cluster, der Ausfälle einzelner Server automatisch kompensiert, rolling Updates ohne Downtime durchführt und Daten über alle Nodes repliziert. Planen Sie 60 bis 90 Minuten für die gesamte Einrichtung ein.

Warum ein Kubernetes-Cluster?

Ein einzelner Server genügt für viele Anwendungen. Sobald jedoch Hochverfügbarkeit, unterbrechungsfreie Updates oder horizontale Skalierung gefordert sind, stößt ein Einzelserver an seine Grenzen. Fällt er aus, sind alle darauf betriebenen Dienste gleichzeitig offline. Updates erfordern Wartungsfenster. Lastspitzen lassen sich nur durch vertikales Skalieren (mehr CPU, mehr RAM) abfangen, was schnell an physische Grenzen stößt.

Kubernetes löst diese Probleme, indem es mehrere Server zu einem Cluster verbindet. Anwendungen werden als Container auf die verfügbaren Nodes verteilt. Fällt ein Node aus, verschiebt Kubernetes die betroffenen Workloads automatisch auf die verbleibenden Nodes. Rolling Updates tauschen Container schrittweise aus, sodass zu jedem Zeitpunkt mindestens eine gesunde Instanz läuft. Horizontale Skalierung fügt weitere Replikate hinzu, statt einen einzelnen Server aufzurüsten.

Für Anwendungen, die keine Hochverfügbarkeit benötigen und bei denen gelegentliche kurze Ausfälle akzeptabel sind, ist ein Kubernetes-Cluster nicht zwingend erforderlich. Eine Deployment-Plattform auf einem einzelnen Server bietet eine deutlich einfachere Lösung. Einen Überblick über diese Optionen finden Sie auf der Seite Self-Hosted Deployment.

Architektur-Überblick

Der Cluster besteht aus drei Schichten, die aufeinander aufbauen.

WireGuard-Mesh bildet die Netzwerkebene. Alle drei Server sind über verschlüsselte Punkt-zu-Punkt-Tunnel miteinander verbunden. Jede Kommunikation zwischen den Nodes läuft über dieses private Netzwerk. Externe Angreifer können den Cluster-internen Traffic weder mitlesen noch manipulieren. Mehr Hintergrund zu WireGuard finden Sie im VPN-Guide.

k3s ist eine zertifizierte, leichtgewichtige Kubernetes-Distribution von Rancher (SUSE). Sie verpackt den gesamten Kubernetes-Stack in ein einzelnes Binary und benötigt deutlich weniger Ressourcen als ein Standard-Kubernetes-Cluster.

Kubernetes unterscheidet zwischen der Control Plane (die Steuerungsebene) und den Workloads (Ihre Anwendungen). Die Control Plane besteht aus dem API Server (nimmt kubectl-Befehle entgegen), dem Scheduler (entscheidet, auf welchem Node ein Pod läuft), dem Controller Manager (sorgt dafür, dass der gewünschte Zustand eingehalten wird) und etcd (eine verteilte Datenbank, die den gesamten Cluster-Zustand speichert: Deployments, Services, Secrets, Konfigurationen).

In einem Standard-Kubernetes-Setup laufen Control Plane und Workloads auf getrennten Servern (Control-Plane-Nodes und Worker-Nodes). k3s vereinfacht das: In diesem Tutorial laufen alle drei Nodes als Server-Nodes. Jeder Server-Node betreibt sowohl die Control Plane als auch Ihre Workloads. Alle drei Nodes halten eine Kopie von etcd. etcd verwendet einen Konsens-Algorithmus (Raft), der eine Mehrheit der Nodes benötigt, um Schreibvorgänge zu bestätigen. Bei drei Nodes ist die Mehrheit zwei. Das bedeutet: Fällt ein Node aus, können die verbleibenden zwei weiterhin Entscheidungen treffen. Fallen zwei Nodes aus, fehlt die Mehrheit und der Cluster kann keine neuen Änderungen mehr annehmen (laufende Workloads auf dem verbleibenden Node sind davon nicht betroffen, aber neue Deployments oder Skalierungen sind nicht möglich).

Longhorn stellt replizierte Speichervolumes bereit. Wenn ein Pod persistente Daten benötigt (Datenbank, Uploads), erstellt Longhorn ein Volume und repliziert es auf mehrere Nodes. Fällt ein Node aus, sind die Daten auf den anderen Nodes weiterhin verfügbar.

Traefik fungiert als Ingress Controller und ist in k3s bereits integriert. Es empfängt eingehenden HTTP/HTTPS-Traffic und leitet ihn anhand von Routing-Regeln an die passenden Services weiter.

Kubernetes-Cluster-Architektur
Kubernetes-Cluster-Architektur

NodeÖffentliche IPWireGuard-IPRolle
seed-k8s-01(Ihre IP)10.222.0.1Server
seed-k8s-02(Ihre IP)10.222.0.2Server
seed-k8s-03(Ihre IP)10.222.0.3Server

Voraussetzungen

  • 3 Seeds in der dataforest Cloud. Minimum: Plan entry-c4-m8-s80 (4 CPU, 8 GB RAM, 80 GB SSD). Empfehlung für komfortables Arbeiten: Plan entry-c8-m16-s320 (8 CPU, 16 GB RAM, 320 GB SSD). Kubernetes selbst, k3s, Longhorn und das Overlay-Netzwerk verbrauchen bereits Ressourcen. Mit dem größeren Plan bleibt genug Kapazität für Ihre tatsächlichen Workloads.
  • SSH-Zugriff auf alle drei Seeds
  • kubectl auf Ihrem lokalen Rechner (offizielle Installationsanleitung)
  • Optional: Eine Domain mit DNS-Zugriff, um HTTPS-Ingress mit Let's Encrypt zu testen
  • Grundkenntnisse: Docker, Linux-Terminal, SSH

Die drei Seeds lassen sich über die dataforest Cloud UI oder per Public API erstellen. Per API sieht das für den ersten Node so aus:

bash
curl -X POST "https://api.dataforest.net/api/v1/public/seeds" \
  -H "Authorization: Bearer <API-Token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "seed-k8s-01",
    "plan": "lines/entry/models/entry-c4-m8-s80",
    "location": "fra01",
    "project_id": "<Projekt-ID>",
    "ssh_keys": ["<SSH-Key-ID>"],
    "source": {
      "type": "image",
      "ref": "images/debian/versions/debian-v13"
    },
    "enable_ipv4": true
  }'

Wiederholen Sie den Aufruf mit den Namen seed-k8s-02 und seed-k8s-03. API-Token und Projekt-ID finden Sie in den Team-Einstellungen der Cloud UI. Die verfügbaren SSH-Key-IDs lassen sich mit GET /sshkeys abrufen.

Seeds vorbereiten

Die folgenden Schritte führen Sie auf allen drei Nodes aus. Verbinden Sie sich per SSH und arbeiten Sie als root.

System aktualisieren

bash
apt update && apt upgrade -y

Ein frisches System mit aktuellen Paketen vermeidet Kompatibilitätsprobleme bei der Installation von k3s und Longhorn.

Swap deaktivieren

Der kubelet-Prozess berechnet verfügbare Ressourcen, plant Pods basierend auf Speicher-Requests und erzwingt Limits. Wenn das Betriebssystem im Hintergrund Speicher auf die Festplatte auslagert, werden diese Berechnungen unzuverlässig. Pods könnten scheinbar mehr Speicher nutzen als verfügbar ist, und bei tatsächlichem Speichermangel reagiert das System mit extremer Verlangsamung statt mit einem kontrollierten Neustart des Pods. Seit Kubernetes 1.34 gibt es einen stabilen Swap-Modus (LimitedSwap), der Burstable-Pods kontrollierten Swap-Zugriff erlaubt. Für einen Cluster-Setup wie dieses ist deaktivierter Swap aber weiterhin die einfachste und sicherste Variante.

bash
swapoff -a
sed -i '/swap/d' /etc/fstab

Der erste Befehl deaktiviert Swap sofort. Der zweite entfernt den Swap-Eintrag aus /etc/fstab, damit Swap nach einem Neustart nicht automatisch wieder aktiviert wird.

Kernel-Module laden

Container-Netzwerke benötigen zwei Kernel-Module: overlay für das Overlay-Filesystem (wie Container ihre Dateisysteme schichten) und br_netfilter für die korrekte Verarbeitung von Bridge-Traffic durch iptables.

bash
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF

modprobe overlay
modprobe br_netfilter

Die Datei unter /etc/modules-load.d/ sorgt dafür, dass die Module nach einem Neustart automatisch geladen werden. Die modprobe-Befehle laden sie sofort in den laufenden Kernel.

Sysctl-Parameter setzen

Kubernetes-Netzwerke erfordern, dass der Kernel Pakete zwischen Netzwerk-Bridges korrekt weiterleitet und durch iptables-Regeln filtert:

bash
cat <<EOF | tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF

sysctl --system

net.bridge.bridge-nf-call-iptables sorgt dafür, dass Traffic, der über eine Linux-Bridge läuft, die iptables-Regeln durchläuft. Ohne diese Einstellung würden NetworkPolicies und Service-Routing nicht funktionieren. net.ipv4.ip_forward erlaubt dem Kernel, Pakete zwischen Netzwerk-Interfaces weiterzuleiten, was für das Routing zwischen Pods auf verschiedenen Nodes erforderlich ist.

Firewall konfigurieren

Debian 13 bringt kein iptables vorinstalliert mit. Installieren Sie es:

bash
apt install iptables

Der Cluster benötigt folgende Ports. Jeder Port erfüllt eine spezifische Funktion:

PortProtokollVerwendung
51820UDPWireGuard-Tunnel zwischen den Nodes
6443TCPKubernetes API Server (kubectl-Kommunikation, Node-Registrierung)
9345TCPk3s Supervisor API (Nodes treten dem Cluster bei)
10250TCPKubelet API (API Server kommuniziert mit Kubelets auf jedem Node)
2379-2380TCPetcd Client- und Peer-Kommunikation (Cluster-State)
8472UDPVXLAN (Flannel Overlay-Netzwerk zwischen Pods)
80TCPHTTP Ingress (eingehender Web-Traffic)
443TCPHTTPS Ingress (eingehender Web-Traffic, TLS)

Öffnen Sie diese Ports für den Cluster-internen Traffic (WireGuard-Subnetz) und die öffentlich erreichbaren Ports:

bash
# WireGuard: muss von allen anderen Nodes erreichbar sein
iptables -A INPUT -p udp --dport 51820 -j ACCEPT

# Kubernetes API Server: öffentlich für kubectl-Zugriff
iptables -A INPUT -p tcp --dport 6443 -j ACCEPT

# k3s Supervisor: nur aus dem WireGuard-Netz
iptables -A INPUT -s 10.222.0.0/24 -p tcp --dport 9345 -j ACCEPT

# Kubelet API: nur aus dem WireGuard-Netz
iptables -A INPUT -s 10.222.0.0/24 -p tcp --dport 10250 -j ACCEPT

# etcd: nur aus dem WireGuard-Netz
iptables -A INPUT -s 10.222.0.0/24 -p tcp --dport 2379:2380 -j ACCEPT

# Flannel VXLAN: nur aus dem WireGuard-Netz
iptables -A INPUT -s 10.222.0.0/24 -p udp --dport 8472 -j ACCEPT

# Ingress: öffentlich erreichbar
iptables -A INPUT -p tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT

Damit diese Regeln einen Neustart überleben, installieren Sie iptables-persistent:

bash
apt install iptables-persistent
netfilter-persistent save

Bei der Installation fragt das Paket, ob die aktuellen Regeln gespeichert werden sollen. Bestätigen Sie mit Ja. Nach zukünftigen Änderungen speichern Sie erneut mit netfilter-persistent save.

WireGuard-Mesh einrichten

Kubernetes verlangt, dass alle Pods ohne NAT miteinander kommunizieren können und dass Nodes alle Pods direkt erreichen. Auf dedizierten Servern ohne verwaltetes VLAN erfüllt ein WireGuard-Mesh diese Anforderung: Es spannt ein verschlüsseltes Overlay-Netzwerk über die öffentlichen IPs Ihrer Nodes.

WireGuard ist seit Kernel 5.6 (März 2020) fester Bestandteil des Linux-Kernels. Der reine Verschlüsselungs-Overhead liegt laut Benchmarks der Universität Amsterdam bei unter 0,5 ms pro Hop. In der Praxis kommen Faktoren wie Routing und Systemlast hinzu, sodass Sie mit niedrigen einstelligen Millisekunden rechnen können.

In diesem Tutorial nutzt k3s VXLAN als Flannel-Backend (--flannel-backend=vxlan) und routet es über das WireGuard-Interface (--flannel-iface=wg0). Das WireGuard-Mesh verschlüsselt dabei den gesamten Traffic zwischen den Nodes, sodass eine zweite Verschlüsselungsschicht auf Flannel-Ebene nicht nötig ist. Control-Plane-Kommunikation (API-Server, eingebettetes etcd) und Pod-zu-Pod-Traffic laufen beide über das Mesh. Deshalb muss es stehen, bevor k3s installiert wird.

WireGuard installieren

Führen Sie auf allen drei Nodes aus:

bash
apt install wireguard

Das Paket installiert die Verwaltungstools wg und wg-quick. Das WireGuard-Kernelmodul ist seit Linux 5.6 fest in den Kernel integriert und in Debian 13 standardmäßig verfügbar.

Schlüsselpaare generieren

Jeder Node benötigt ein eigenes Schlüsselpaar. Der private Schlüssel bleibt auf dem jeweiligen Node. Die öffentlichen Schlüssel werden an die anderen Nodes weitergegeben.

Führen Sie auf jedem der drei Nodes aus:

bash
wg genkey | tee /etc/wireguard/private.key
chmod 600 /etc/wireguard/private.key
cat /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key

Notieren Sie sich den öffentlichen Schlüssel jedes Nodes:

bash
cat /etc/wireguard/public.key

Sie benötigen diese drei Public Keys im nächsten Schritt für die Konfiguration der Peer-Abschnitte.

Konfiguration erstellen

In einem Mesh-Netzwerk kennt jeder Node die anderen beiden als Peers. Erstellen Sie auf jedem Node die Datei /etc/wireguard/wg0.conf mit dem entsprechenden Inhalt.

Node 1 (seed-k8s-01): /etc/wireguard/wg0.conf

ini
[Interface]
PrivateKey = <PRIVATE_KEY_NODE_1>
Address = 10.222.0.1/24
ListenPort = 51820
MTU = 1420

[Peer]
PublicKey = <PUBLIC_KEY_NODE_2>
AllowedIPs = 10.222.0.2/32
Endpoint = <PUBLIC_IP_NODE_2>:51820

[Peer]
PublicKey = <PUBLIC_KEY_NODE_3>
AllowedIPs = 10.222.0.3/32
Endpoint = <PUBLIC_IP_NODE_3>:51820

Node 2 (seed-k8s-02): /etc/wireguard/wg0.conf

ini
[Interface]
PrivateKey = <PRIVATE_KEY_NODE_2>
Address = 10.222.0.2/24
ListenPort = 51820
MTU = 1420

[Peer]
PublicKey = <PUBLIC_KEY_NODE_1>
AllowedIPs = 10.222.0.1/32
Endpoint = <PUBLIC_IP_NODE_1>:51820

[Peer]
PublicKey = <PUBLIC_KEY_NODE_3>
AllowedIPs = 10.222.0.3/32
Endpoint = <PUBLIC_IP_NODE_3>:51820

Node 3 (seed-k8s-03): /etc/wireguard/wg0.conf

ini
[Interface]
PrivateKey = <PRIVATE_KEY_NODE_3>
Address = 10.222.0.3/24
ListenPort = 51820
MTU = 1420

[Peer]
PublicKey = <PUBLIC_KEY_NODE_1>
AllowedIPs = 10.222.0.1/32
Endpoint = <PUBLIC_IP_NODE_1>:51820

[Peer]
PublicKey = <PUBLIC_KEY_NODE_2>
AllowedIPs = 10.222.0.2/32
Endpoint = <PUBLIC_IP_NODE_2>:51820

Erläuterung der Parameter:

  • PrivateKey: Der private Schlüssel dieses Nodes (aus /etc/wireguard/private.key).
  • Address: Die WireGuard-IP dieses Nodes im privaten Subnetz 10.222.0.0/24.
  • ListenPort: Der UDP-Port, auf dem WireGuard Verbindungen annimmt.
  • MTU = 1420: WireGuard fügt jedem Paket einen Header hinzu (60 Bytes bei IPv4, 80 Bytes bei IPv6). Die Standard-MTU von Ethernet liegt bei 1500 Bytes. 1500 minus 80 ergibt 1420 Bytes nutzbare Paketgröße innerhalb des Tunnels. Ein zu hoher MTU-Wert führt zu Paketfragmentierung und Performanceproblemen.
  • PublicKey: Der öffentliche Schlüssel des jeweiligen Peer-Nodes.
  • AllowedIPs: Definiert, welche IP-Adressen über diesen Peer erreichbar sind. Bei einem Mesh-Setup ist das die einzelne WireGuard-IP des Peers (/32).
  • Endpoint: Die öffentliche IP und der Port des Peer-Nodes, über die der Tunnel aufgebaut wird.

WireGuard starten

Aktivieren und starten Sie den Tunnel auf allen drei Nodes:

bash
systemctl enable --now wg-quick@wg0

Dieser Befehl startet das WireGuard-Interface sofort und konfiguriert den automatischen Start nach einem Neustart.

Verbindung validieren

Testen Sie von jedem Node die Erreichbarkeit der anderen beiden. Auf Node 1:

bash
ping -c 3 10.222.0.2
ping -c 3 10.222.0.3

Auf Node 2:

bash
ping -c 3 10.222.0.1
ping -c 3 10.222.0.3

Auf Node 3:

bash
ping -c 3 10.222.0.1
ping -c 3 10.222.0.2

Alle Pings müssen Antworten liefern. Wenn ein Ping fehlschlägt, fahren Sie nicht mit der k3s-Installation fort. Der Cluster funktioniert nur, wenn alle Nodes über das WireGuard-Mesh kommunizieren können.

Troubleshooting WireGuard

Falls ein Ping fehlschlägt, prüfen Sie der Reihe nach:

Tunnel-Status anzeigen:

bash
wg show

Unter latest handshake sehen Sie, ob eine Verbindung zum Peer besteht. Fehlt dieser Eintrag, hat noch kein Handshake stattgefunden.

Port erreichbar? Prüfen Sie auf dem Ziel-Node, ob WireGuard auf dem korrekten Port lauscht:

bash
ss -ulnp | grep 51820

Falls keine Ausgabe erscheint, ist WireGuard nicht gestartet oder der Port ist falsch konfiguriert.

Firewall prüfen: Stellen Sie sicher, dass UDP Port 51820 nicht blockiert wird:

bash
iptables -L INPUT -n | grep 51820

Public Keys prüfen: Ein häufiger Fehler ist das Vertauschen von öffentlichen und privaten Schlüsseln oder das Verwenden des falschen Public Keys in der Peer-Konfiguration. Vergleichen Sie die Ausgabe von cat /etc/wireguard/public.key auf dem jeweiligen Node mit dem PublicKey-Eintrag in den Peer-Abschnitten der anderen Nodes.

Endpoint-IP korrekt? Der Endpoint-Wert muss die öffentliche IP des Peer-Nodes sein, nicht die WireGuard-IP.

k3s installieren

k3s wird in zwei Phasen installiert. Node 1 initialisiert den Cluster mit embedded etcd. Die Nodes 2 und 3 treten anschließend als zusätzliche Server-Nodes bei. Alle drei Nodes sind gleichwertige Server (nicht Agents). Das bedeutet: Jeder Node betreibt die vollständige Kubernetes-Control-Plane und kann bei einem Ausfall die Leader-Rolle übernehmen.

Token generieren

Alle Nodes verwenden ein gemeinsames Token, um sich gegenseitig zu authentifizieren. Generieren Sie ein sicheres Token auf Node 1:

bash
openssl rand -hex 32

Notieren Sie die Ausgabe. Sie verwenden diesen Wert als K3S_TOKEN bei der Installation auf allen drei Nodes.

Node 1: Cluster initialisieren

Node 1 erstellt den Cluster. Führen Sie folgenden Befehl auf seed-k8s-01 aus:

bash
curl -sfL https://get.k3s.io | K3S_TOKEN=<IHR_TOKEN> sh -s - server \
  --cluster-init \
  --node-ip=10.222.0.1 \
  --node-external-ip=<PUBLIC_IP_NODE_1> \
  --flannel-iface=wg0 \
  --flannel-backend=vxlan \
  --tls-san=<PUBLIC_IP_NODE_1> \
  --tls-san=10.222.0.1

Erläuterung jedes Flags:

  • K3S_TOKEN: Das gemeinsame Geheimnis, mit dem sich Nodes beim Cluster authentifizieren. Ohne gültiges Token kann kein Node beitreten.
  • --cluster-init: Aktiviert embedded etcd und startet einen neuen HA-Cluster. Ohne dieses Flag würde k3s SQLite als Datenbank verwenden, was keine Hochverfügbarkeit ermöglicht.
  • --node-ip=10.222.0.1: Teilt k3s mit, welche IP-Adresse für die interne Cluster-Kommunikation verwendet werden soll. Durch die Angabe der WireGuard-IP läuft sämtlicher Cluster-Traffic über den verschlüsselten Tunnel.
  • --node-external-ip=<PUBLIC_IP_NODE_1>: Die öffentliche IP-Adresse dieses Nodes. Wird für Ingress-Traffic verwendet, damit externe Anfragen den Node erreichen.
  • --flannel-iface=wg0: Weist das Flannel-Overlay-Netzwerk an, das WireGuard-Interface für die Kommunikation zwischen Pods auf verschiedenen Nodes zu verwenden. Ohne dieses Flag würde Flannel das Standard-Interface (eth0) nutzen und Traffic unverschlüsselt über das öffentliche Netzwerk senden.
  • --flannel-backend=vxlan: Verwendet VXLAN als Overlay-Protokoll. k3s bietet auch wireguard-native als Backend an. Da die Verbindung zwischen den Nodes bereits durch das WireGuard-Mesh verschlüsselt ist, wäre eine zweite WireGuard-Schicht redundant und würde nur Overhead erzeugen.
  • --tls-san=<PUBLIC_IP_NODE_1> und --tls-san=10.222.0.1: Fügt diese IP-Adressen als Subject Alternative Names zum TLS-Zertifikat des API Servers hinzu. Ohne diese Einträge würde kubectl von Ihrem lokalen Rechner aus eine Zertifikatsfehlermeldung erhalten, weil die IP nicht im Zertifikat enthalten ist.

Warten Sie, bis der Node bereit ist. Das dauert 30 bis 60 Sekunden:

bash
kubectl get nodes

Die Ausgabe sollte einen Node mit Status Ready zeigen:

text
NAME           STATUS   ROLES                       AGE   VERSION
seed-k8s-01   Ready    control-plane,etcd,master   45s   v1.31.x+k3s1

Falls der Status NotReady zeigt, warten Sie weitere 30 Sekunden und versuchen Sie es erneut. k3s benötigt einen Moment, um alle System-Pods zu starten.

Node 2 und 3: Dem Cluster beitreten

Führen Sie auf seed-k8s-02 aus:

bash
curl -sfL https://get.k3s.io | K3S_TOKEN=<IHR_TOKEN> sh -s - server \
  --server https://10.222.0.1:6443 \
  --node-ip=10.222.0.2 \
  --node-external-ip=<PUBLIC_IP_NODE_2> \
  --flannel-iface=wg0 \
  --flannel-backend=vxlan \
  --tls-san=<PUBLIC_IP_NODE_2> \
  --tls-san=10.222.0.2

Und auf seed-k8s-03:

bash
curl -sfL https://get.k3s.io | K3S_TOKEN=<IHR_TOKEN> sh -s - server \
  --server https://10.222.0.1:6443 \
  --node-ip=10.222.0.3 \
  --node-external-ip=<PUBLIC_IP_NODE_3> \
  --flannel-iface=wg0 \
  --flannel-backend=vxlan \
  --tls-san=<PUBLIC_IP_NODE_3> \
  --tls-san=10.222.0.3

Der entscheidende Unterschied: --server https://10.222.0.1:6443 zeigt auf die WireGuard-IP von Node 1, nicht auf dessen öffentliche IP. Sämtliche Cluster-Kommunikation läuft über das verschlüsselte WireGuard-Netzwerk. Node 2 und 3 verwenden kein --cluster-init, sondern treten einem bestehenden Cluster bei.

Warten Sie jeweils 30 bis 60 Sekunden nach der Installation. etcd benötigt Zeit, um die neuen Mitglieder in das Quorum aufzunehmen.

Cluster validieren

Prüfen Sie auf einem beliebigen Node den Cluster-Status:

bash
kubectl get nodes

Die Ausgabe sollte drei Nodes mit Status Ready zeigen:

text
NAME           STATUS   ROLES                       AGE     VERSION
seed-k8s-01   Ready    control-plane,etcd,master   5m      v1.31.x+k3s1
seed-k8s-02   Ready    control-plane,etcd,master   2m      v1.31.x+k3s1
seed-k8s-03   Ready    control-plane,etcd,master   90s     v1.31.x+k3s1

Alle drei Nodes tragen die Rollen control-plane, etcd und master. Das bestätigt, dass der Cluster vollständig hochverfügbar ist.

Kubeconfig auf den lokalen Rechner kopieren

Um den Cluster von Ihrem lokalen Rechner aus zu verwalten, benötigen Sie die kubeconfig-Datei. Diese enthält die Verbindungsdaten und Zugangsdaten für den API Server. Auf jedem k3s-Server liegt sie unter /etc/rancher/k3s/k3s.yaml.

Kopieren Sie die Datei von Node 1:

bash
scp root@<PUBLIC_IP_NODE_1>:/etc/rancher/k3s/k3s.yaml ~/.kube/config-k8s-cluster

Die Datei enthält 127.0.0.1 als Server-Adresse, da sie für die lokale Nutzung auf dem Node gedacht ist. Ersetzen Sie diese durch die öffentliche IP von Node 1:

bash
sed -i 's/127.0.0.1/<PUBLIC_IP_NODE_1>/' ~/.kube/config-k8s-cluster

Setzen Sie die Umgebungsvariable, damit kubectl diese Konfiguration verwendet:

bash
export KUBECONFIG=~/.kube/config-k8s-cluster

Testen Sie den Zugriff:

bash
kubectl get nodes

Sie sollten dieselbe Ausgabe mit drei Ready-Nodes sehen wie auf dem Server. Wenn die Verbindung fehlschlägt, prüfen Sie, ob Port 6443 auf Node 1 erreichbar ist und ob die --tls-san Flags die öffentliche IP enthalten.

Für dauerhafte Nutzung fügen Sie den Export in Ihre Shell-Konfiguration ein (z.B. ~/.bashrc oder ~/.zshrc).

Troubleshooting k3s

Node tritt dem Cluster nicht bei:

Prüfen Sie zuerst die WireGuard-Konnektivität. Von Node 2 oder 3 muss ping 10.222.0.1 funktionieren. Prüfen Sie dann, ob die erforderlichen Ports offen sind:

bash
# Auf Node 1 ausführen: ist Port 6443 erreichbar?
ss -tlnp | grep 6443

# Auf Node 1 ausführen: ist Port 9345 erreichbar?
ss -tlnp | grep 9345

Token stimmt nicht: Vergleichen Sie den Token auf allen Nodes. Der Wert muss exakt übereinstimmen, einschließlich Groß-/Kleinschreibung.

Logs prüfen:

bash
journalctl -u k3s -f

Häufige Fehlermeldungen und ihre Ursachen:

  • certificate signed by unknown authority: Die --tls-san Flags fehlen oder die IP stimmt nicht.
  • etcd cluster is not healthy: Die WireGuard-Verbindung zwischen den Nodes ist instabil. Prüfen Sie wg show auf aktive Handshakes.
  • connection refused auf Port 6443: k3s ist auf Node 1 noch nicht vollständig gestartet. Warten Sie 60 Sekunden und versuchen Sie es erneut.

Persistenten Storage einrichten

Kubernetes unterscheidet zwischen kurzlebigen Containern und dauerhaften Daten. Standardmäßig gehen alle Dateien innerhalb eines Containers verloren, wenn der Pod neu gestartet wird. Für Datenbanken, Uploads oder Konfigurationsdateien braucht der Cluster ein Storage-System, das Daten unabhängig vom einzelnen Pod und idealerweise unabhängig vom einzelnen Node speichert.

Storage-Optionen im Überblick

In Kubernetes wird Storage über PersistentVolumeClaims (PVC) angefordert und über StorageClasses bereitgestellt. Eine StorageClass definiert, welches Backend die Volumes erstellt. Je nach Umgebung gibt es verschiedene Optionen:

  • Cloud-Provider-Storage: Bei verwalteten Cloud-Plattformen provisioniert der Provider Block-Storage-Volumes (vergleichbar mit virtuellen Festplatten), die sich automatisch an Pods anhängen und zwischen Nodes verschieben lassen. Das ist die einfachste Variante, setzt aber voraus, dass der Provider einen CSI-Treiber (Container Storage Interface) bereitstellt.
  • local-path-provisioner (in k3s vorinstalliert): Erstellt Volumes direkt auf der lokalen Festplatte des Nodes. Kein Overhead, keine Replikation. Fällt der Node aus, sind die Daten nicht verfügbar. Geeignet für Entwicklungsumgebungen und Anwendungen, die ihren State extern speichern.
  • Distributed Block Storage: Software, die auf den lokalen Festplatten aller Nodes aufbaut und daraus ein verteiltes, repliziertes Storage-System bildet. Longhorn, Piraeus/LINSTOR und Rook-Ceph fallen in diese Kategorie. Der Vorteil: Daten werden automatisch auf mehrere Nodes repliziert. Der Nachteil: zusätzlicher Ressourcenverbrauch und niedrigere IOPS als native SSDs.

In diesem Tutorial verwenden wir Longhorn, weil es speziell für Kubernetes-Cluster mit lokalen Festplatten entwickelt wurde, sich per Helm mit einem Befehl installieren lässt und eine Web-UI für die Verwaltung mitbringt.

Wie Longhorn funktioniert

Wenn ein Pod ein PersistentVolumeClaim erstellt, reserviert Longhorn Speicherplatz auf den lokalen SSDs der Nodes und erstellt ein Block Device. Dieses Block Device wird per iSCSI an den Node angebunden, auf dem der Pod läuft. Gleichzeitig repliziert Longhorn die Daten synchron auf andere Nodes (konfigurierbar, in diesem Tutorial auf 2 von 3 Nodes). Jede Replik ist eine vollständige Kopie des Volumes.

Fällt der Node aus, auf dem ein Pod mit einem Longhorn-Volume läuft, startet Kubernetes den Pod auf einem anderen Node. Longhorn erkennt, dass eine Replik des Volumes auf diesem Node (oder einem erreichbaren Node) existiert, und bindet das Volume dort an. Die Daten sind sofort verfügbar, ohne dass ein kompletter Rebuild nötig ist.

Longhorn installieren

Voraussetzungen auf allen Nodes

Longhorn nutzt iSCSI intern für die Volume-Verwaltung zwischen seinen Komponenten. Dieses Paket muss auf jedem der drei Nodes installiert sein:

bash
apt install open-iscsi nfs-common
systemctl enable --now iscsid

open-iscsi stellt den iSCSI-Initiator bereit, über den Longhorn Volumes an die richtigen Pods anbindet. nfs-common wird für ReadWriteMany-Volumes und Backups benötigt. Der Befehl enable --now startet den Dienst sofort und aktiviert ihn dauerhaft.

Wiederholen Sie diesen Schritt auf seed-k8s-01, seed-k8s-02 und seed-k8s-03.

Helm installieren

Helm ist der Standard-Paketmanager für Kubernetes. Er installiert komplexe Anwendungen (bestehend aus vielen YAML-Manifesten) als sogenannte "Charts" mit einem einzigen Befehl.

bash
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

Prüfen Sie die Installation:

bash
helm version

Longhorn per Helm installieren

Fügen Sie das offizielle Longhorn-Repository hinzu und installieren Sie das Chart:

bash
helm repo add longhorn https://charts.longhorn.io
helm repo update
helm install longhorn longhorn/longhorn \
  --namespace longhorn-system \
  --create-namespace \
  --set defaultSettings.defaultReplicaCount=2

Die einzelnen Parameter im Detail:

  • --namespace longhorn-system erstellt einen dedizierten Namespace, damit alle Longhorn-Komponenten vom restlichen Cluster isoliert sind.
  • --create-namespace legt den Namespace an, falls er noch nicht existiert.
  • --set defaultSettings.defaultReplicaCount=2 speichert jedes Volume auf zwei der drei Nodes. Das bietet Ausfallsicherheit (ein Node darf ausfallen), ohne den Speicherverbrauch zu verdreifachen.

Warten Sie, bis alle Pods bereit sind:

bash
kubectl -n longhorn-system get pods

Dieser Vorgang kann 2 bis 5 Minuten dauern. Alle Pods sollten den Status Running erreichen.

Standard-StorageClass konfigurieren

k3s bringt eine eigene StorageClass namens local-path mit, die Daten nur lokal auf einem einzigen Node speichert. Longhorn registriert bei der Installation eine eigene StorageClass namens longhorn.

Es gibt zwei Wege, Longhorn-Storage zu nutzen:

  1. Explizit pro PVC: Jeder PersistentVolumeClaim gibt storageClassName: longhorn an. Das ist expliziter und dokumentiert im Manifest, welcher Storage verwendet wird.
  2. Als Standard-StorageClass: Longhorn wird zum Default. PVCs ohne explizite storageClassName verwenden automatisch Longhorn.

In diesem Tutorial setzen wir Longhorn als Standard, damit PVCs ohne explizite Angabe automatisch replizierten Storage erhalten:

bash
kubectl patch storageclass longhorn -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

Die StorageClass local-path bleibt verfügbar. Wenn ein PVC explizit storageClassName: local-path angibt, wird weiterhin lokaler Storage ohne Replikation verwendet. Das ist sinnvoll für temporäre Daten oder Caches, bei denen Replikation unnötiger Overhead wäre.

Validierung: Test-Volume erstellen

Erstellen Sie eine Datei test-pvc.yaml:

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi

Wenden Sie das Manifest an und prüfen Sie den Status:

bash
kubectl apply -f test-pvc.yaml
kubectl get pvc

In der Spalte STATUS sollte nach wenigen Sekunden Bound stehen. Das bedeutet: Longhorn hat erfolgreich ein 1-GB-Volume erstellt und auf zwei Nodes repliziert.

Räumen Sie das Test-Volume wieder auf:

bash
kubectl delete pvc test-pvc

Optional: Longhorn UI

Longhorn bringt eine Web-Oberfläche mit, über die Sie Volumes, Snapshots und den Zustand der Nodes einsehen können. Für einen schnellen Zugriff ohne Ingress:

bash
kubectl -n longhorn-system port-forward svc/longhorn-frontend 8080:80

Die UI ist dann unter http://localhost:8080 erreichbar (über SSH-Tunnel oder direkt auf dem Node).

Troubleshooting

PVC bleibt im Status Pending:

bash
kubectl describe pvc test-pvc

Häufige Ursachen:

  • Der iscsid-Dienst läuft nicht auf einem oder mehreren Nodes.
  • Die StorageClass ist nicht korrekt als Default markiert.
  • Longhorn-Pods sind noch nicht vollständig gestartet.

Longhorn-Pods in CrashLoopBackOff:

bash
kubectl -n longhorn-system logs <pod-name>
df -h
free -m

Häufige Ursachen:

  • Zu wenig freier Speicherplatz auf dem Node (Longhorn benötigt mindestens 25% freien Platz).
  • Zu wenig Arbeitsspeicher für die Longhorn-Komponenten.

Erster Workload: Stateless Anwendung

Mit funktionierendem Cluster und Storage-System ist es Zeit für den ersten Workload. Sie deployen eine einfache Webanwendung mit zwei Instanzen, einem internen Service und optionalem HTTPS-Zugriff über eine eigene Domain.

Namespace erstellen

bash
kubectl create namespace demo

Namespaces gruppieren zusammengehörige Ressourcen und isolieren sie voneinander. Alle folgenden Manifeste landen im Namespace demo.

Deployment erstellen

Erstellen Sie eine Datei deployment.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:alpine
          ports:
            - containerPort: 80

Die einzelnen Felder im Detail:

  • apiVersion: apps/v1 gibt die API-Gruppe an, die Deployments bereitstellt.
  • kind: Deployment sorgt dafür, dass Kubernetes die gewünschte Anzahl an Pods automatisch aufrechterhält.
  • replicas: 2 erstellt zwei identische Pods. Fällt einer aus, läuft der andere weiter.
  • selector.matchLabels verbindet das Deployment mit seinen Pods über das Label app: web.
  • template ist die Vorlage für jeden Pod. Alle Pods bekommen das Label app: web.
  • image: nginx:alpine verwendet das offizielle nginx-Image in der schlanken Alpine-Variante.
  • containerPort: 80 dokumentiert, auf welchem Port die Anwendung lauscht.

Service erstellen

Erstellen Sie eine Datei service.yaml:

yaml
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: demo
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80

Ein Service ist eine stabile Abstraktionsschicht innerhalb des Clusters. Er erhält eine feste Cluster-IP und einen DNS-Namen, über den andere Pods die Anwendung erreichen. Der Service verteilt eingehenden Traffic automatisch auf alle Pods mit dem passenden Label. Andere Pods im Cluster erreichen diese Anwendung über http://web.demo.svc.cluster.local oder kurz http://web (innerhalb desselben Namespace).

Anwenden und prüfen

bash
kubectl apply -f deployment.yaml -f service.yaml
kubectl -n demo get pods -o wide

Das Flag -o wide zeigt zusätzliche Spalten, unter anderem auf welcher Node jeder Pod läuft. Bei replicas: 2 sollten die Pods auf unterschiedlichen Nodes verteilt sein. Kubernetes versucht standardmäßig, Pods über verfügbare Nodes zu streuen.

Internen Zugriff testen

bash
kubectl -n demo exec -it deploy/web -- curl -s http://web

Dieser Befehl startet curl innerhalb eines der web-Pods und ruft den Service auf. Sie sollten die Standard-nginx-Willkommensseite als HTML erhalten.

HTTPS-Zugriff mit eigener Domain

Wenn Sie eine Domain auf Ihren Cluster zeigen möchten, sind drei Schritte erforderlich: Traefik für Let's Encrypt konfigurieren, DNS-Records setzen und einen Ingress erstellen.

Schritt 1: Let's-Encrypt-Resolver konfigurieren. k3s erlaubt die Konfiguration von Traefik über eine HelmChartConfig-Ressource. Erstellen Sie auf Node 1 eine Datei /var/lib/rancher/k3s/server/manifests/traefik-config.yaml:

yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    additionalArguments:
      - "--certificatesresolvers.le.acme.email=ihre-email@example.com"
      - "--certificatesresolvers.le.acme.storage=/data/acme.json"
      - "--certificatesresolvers.le.acme.tlschallenge=true"

Ersetzen Sie die E-Mail-Adresse durch Ihre eigene. Dateien im Verzeichnis /var/lib/rancher/k3s/server/manifests/ werden von k3s automatisch angewandt. Traefik startet innerhalb weniger Sekunden mit der neuen Konfiguration neu.

Schritt 2: DNS-Records setzen. Erstellen Sie drei A-Records für Ihre Domain, die auf die öffentlichen IP-Adressen aller drei Nodes zeigen.

Schritt 3: Ingress erstellen. Erstellen Sie eine Datei ingress.yaml:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
  namespace: demo
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: websecure
    traefik.ingress.kubernetes.io/router.tls.certresolver: le
spec:
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80

Ersetzen Sie app.example.com durch Ihre tatsächliche Domain.

bash
kubectl apply -f ingress.yaml

Traefik bezieht automatisch ein Let's-Encrypt-Zertifikat für die Domain. Die erste Anfrage kann einige Sekunden dauern, da das Zertifikat im Hintergrund ausgestellt wird.

Zugriff über IP (ohne Domain)

In diesem Tutorial testen wir der Einfachheit halber den Zugriff über die öffentliche IP. Für produktive Anwendungen empfehlen wir, eine eigene Domain zu konfigurieren und den Ingress mit automatischen HTTPS-Zertifikaten zu nutzen (wie im vorherigen Abschnitt beschrieben).

Ohne konfigurierte Domain können Sie den Service direkt über die öffentliche IP eines beliebigen Nodes testen:

bash
curl -s http://<OEFFENTLICHE_IP_NODE_1>

ServiceLB (der in k3s integrierte Load Balancer) öffnet die Service-Ports auf allen Nodes. Egal welche der drei IPs Sie aufrufen, der Traffic erreicht Ihre Pods.

Rolling Update demonstrieren

Ein Rolling Update tauscht die laufenden Pods schrittweise gegen eine neue Version aus. Während des Updates sind immer Pods erreichbar.

bash
kubectl -n demo set image deployment/web nginx=nginx:stable-alpine
kubectl -n demo rollout status deployment/web

Der erste Befehl ändert das Image-Tag. Kubernetes startet daraufhin neue Pods mit dem aktualisierten Image und terminiert die alten erst, wenn die neuen bereit sind. rollout status zeigt den Fortschritt in Echtzeit. Das Ergebnis: Zero-Downtime-Update.


Zweiter Workload: Stateful Anwendung (PostgreSQL)

Eine Datenbank ist der klassische Test für persistenten Storage. Sie deployen PostgreSQL, schreiben Daten und weisen nach, dass die Daten einen Pod-Neustart überleben.

PersistentVolumeClaim erstellen

Erstellen Sie eine Datei pvc.yaml:

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-data
  namespace: demo
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

ReadWriteOnce bedeutet: Genau ein Pod darf gleichzeitig schreibend auf das Volume zugreifen. Für eine einzelne Datenbank-Instanz ist das der richtige Modus. Longhorn erstellt automatisch zwei Repliken dieses Volumes auf unterschiedlichen Nodes.

PostgreSQL-Deployment erstellen

Erstellen Sie eine Datei postgres-deployment.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16-alpine
          env:
            - name: POSTGRES_PASSWORD
              value: "changeme"
            - name: POSTGRES_DB
              value: "demo"
            - name: PGDATA
              value: "/var/lib/postgresql/data/pgdata"
          ports:
            - containerPort: 5432
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: postgres-data

Die wichtigsten Punkte:

  • replicas: 1, da PostgreSQL kein Multi-Writer-Dateisystem unterstützt. Für Hochverfügbarkeit gibt es spezialisierte Operatoren, die hier den Rahmen sprengen würden.
  • env setzt das Passwort und den initialen Datenbanknamen. In einer Produktionsumgebung gehört das Passwort in ein Kubernetes Secret.
  • PGDATA setzt das tatsächliche Datenverzeichnis auf ein Unterverzeichnis (pgdata). Longhorn-Volumes enthalten ein lost+found-Verzeichnis im Root. PostgreSQL startet nicht in einem Verzeichnis, das bereits Dateien enthält. Durch das Unterverzeichnis wird das Problem umgangen.
  • volumeMounts hängt das Longhorn-Volume unter /var/lib/postgresql/data ein.
  • volumes verweist auf den zuvor erstellten PersistentVolumeClaim.

PostgreSQL-Service erstellen

Erstellen Sie eine Datei postgres-service.yaml:

yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: demo
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432

Andere Pods im Cluster erreichen die Datenbank nun unter postgres.demo.svc.cluster.local:5432.

Anwenden und prüfen

bash
kubectl apply -f pvc.yaml -f postgres-deployment.yaml -f postgres-service.yaml
kubectl -n demo get pods,pvc

Warten Sie, bis der Pod Running und der PVC Bound ist.

Daten schreiben

bash
kubectl -n demo exec -it deploy/postgres -- psql -U postgres -d demo -c "
  CREATE TABLE test (id SERIAL PRIMARY KEY, message TEXT, created_at TIMESTAMP DEFAULT NOW());
  INSERT INTO test (message) VALUES ('Kubernetes funktioniert');
"

Dieser Befehl öffnet eine psql-Sitzung im PostgreSQL-Pod, erstellt eine Tabelle und fügt einen Datensatz ein.

Persistenz-Test: Pod löschen

Der entscheidende Test: Überlebt die Datenbank einen Pod-Neustart?

bash
kubectl -n demo delete pod -l app=postgres

Kubernetes erkennt sofort, dass der gewünschte Zustand (1 Pod) nicht mehr erfüllt ist, und startet einen neuen Pod. Beobachten Sie den Vorgang:

bash
kubectl -n demo get pods -w

Sobald der neue Pod Running ist, prüfen Sie die Daten:

bash
kubectl -n demo exec -it deploy/postgres -- psql -U postgres -d demo -c "SELECT * FROM test;"

Die Tabelle und der Datensatz sind vorhanden. Das Longhorn-Volume hat den Pod-Neustart überlebt und wurde automatisch an den neuen Pod angebunden.


Resilienz testen

Ein Kubernetes-Cluster ist nur so gut wie sein Verhalten bei Ausfällen. Dieser Abschnitt beweist, dass der Cluster die versprochene Ausfallsicherheit liefert.

Pod-Selbstheilung

bash
kubectl -n demo delete pod -l app=web
kubectl -n demo get pods -w

Innerhalb weniger Sekunden erstellt Kubernetes neue Pods, um den gewünschten Zustand (replicas: 2) wiederherzustellen. Für Endnutzer entsteht keine Unterbrechung, da der Service den Traffic nur an laufende Pods weiterleitet.

Rolling Updates

Wie bereits im vorherigen Abschnitt demonstriert: Der Befehl kubectl set image tauscht Pods schrittweise aus. Neue Pods starten, bevor alte terminiert werden. Kein Zeitfenster ohne erreichbare Instanz.

Node-Wartung mit drain

Für geplante Wartungsarbeiten (Updates, Hardware-Austausch) können Sie ein Node aus dem Cluster nehmen, ohne Ausfallzeit:

bash
kubectl drain seed-k8s-03 --ignore-daemonsets --delete-emptydir-data

Die Flags im Detail:

  • --ignore-daemonsets lässt systemweite DaemonSet-Pods (wie Longhorn oder Flannel) auf der Node laufen. Diese werden von ihren DaemonSets verwaltet.
  • --delete-emptydir-data erlaubt das Löschen von Pods mit temporären emptyDir-Volumes.

Prüfen Sie, dass alle Workload-Pods auf die verbleibenden Nodes umgezogen sind:

bash
kubectl get pods -n demo -o wide

Nach der Wartung geben Sie die Node wieder frei:

bash
kubectl uncordon seed-k8s-03

Die Node ist nun wieder verfügbar für neue Pod-Zuweisungen.

Was passiert, wenn zwei von drei Nodes ausfallen?

Wie im Architektur-Abschnitt beschrieben, verwendet etcd den Raft-Konsens-Algorithmus. Eine Schreiboperation (z.B. neues Deployment erstellen) wird erst bestätigt, wenn die Mehrheit der etcd-Mitglieder zustimmt. Bei drei Nodes ist die Mehrheit zwei.

Fällt eine Node aus, bleiben zwei Nodes übrig. Zwei von drei ist eine Mehrheit. Der Cluster arbeitet vollständig weiter: Pods werden neu gescheduled, Deployments können erstellt werden, Skalierungen sind möglich.

Fallen zwei Nodes aus, bleibt ein Node übrig. Eins von drei ist keine Mehrheit. etcd kann keine neuen Schreibvorgänge bestätigen. Der Cluster geht in einen Read-Only-Zustand:

  • Pods auf der verbliebenen Node laufen weiter und beantworten Anfragen.
  • Neue Pods können nicht gescheduled werden.
  • Konfigurationsänderungen (neue Deployments, Skalierung) sind nicht möglich.
  • Sobald eine zweite Node zurückkehrt, wird das Quorum wiederhergestellt und der Cluster arbeitet normal weiter.

Die Formel für die Anzahl tolerierter Ausfälle ist: (n - 1) / 2, wobei n die Anzahl der Nodes ist. Drei Nodes tolerieren einen Ausfall. Fünf Nodes tolerieren zwei. Deshalb wird Kubernetes immer mit einer ungeraden Anzahl von Server-Nodes betrieben.


kubectl Grundlagen

kubectl ist das zentrale Werkzeug zur Verwaltung des Clusters. Hier eine Übersicht der wichtigsten Befehle als Referenz.

Ressourcen anzeigen

bash
kubectl get pods                    # Alle Pods im Default-Namespace
kubectl get pods -n demo            # Pods im Namespace "demo"
kubectl get pods -A                 # Pods in allen Namespaces
kubectl get all -n demo             # Pods, Services, Deployments auf einen Blick

Details und Fehlerbehebung

bash
kubectl describe pod <pod-name> -n demo    # Detaillierte Informationen und Events
kubectl logs <pod-name> -n demo            # Ausgabe der Anwendung
kubectl logs <pod-name> -n demo -f         # Log-Stream in Echtzeit

describe zeigt unter "Events" die letzten Aktionen des Schedulers. Wenn ein Pod nicht startet, stehen hier die Gründe (fehlende Images, ungenügend Ressourcen, Volume-Probleme).

In einen Pod einsteigen

bash
kubectl exec -it <pod-name> -n demo -- /bin/sh

Das öffnet eine Shell innerhalb des Containers. Nützlich für Debugging, Konnektivitätstests oder manuelle Prüfungen.

Skalieren

bash
kubectl scale deployment web -n demo --replicas=3

Ändert die Anzahl der Pods sofort. Kubernetes startet oder stoppt Pods, bis der gewünschte Zustand erreicht ist.

Ressourcen löschen

bash
kubectl delete -f deployment.yaml       # Alles löschen, was in der Datei definiert ist
kubectl delete pod <pod-name> -n demo   # Einzelnen Pod löschen
kubectl delete namespace demo           # Gesamten Namespace inkl. aller Ressourcen löschen

Namespaces

bash
kubectl create namespace produktion
kubectl get namespaces

Namespaces trennen Umgebungen (z.B. staging und produktion) oder verschiedene Anwendungen voneinander.

Labels und Selektoren

bash
kubectl get pods -n demo -l app=web          # Nur Pods mit Label app=web
kubectl get pods -n demo -l app=postgres     # Nur Pods mit Label app=postgres

Labels sind Schlüssel-Wert-Paare, über die Kubernetes zusammengehörige Ressourcen verknüpft. Services, Deployments und Ingresses verwenden Labels, um ihre Ziel-Pods zu finden.

kubectl vs. Helm vs. Raw YAML

AnsatzEinsatz
kubectl apply -fEinzelne Manifeste, einfache Anwendungen, Lernzwecke
Helm ChartsKomplexe Anwendungen mit vielen Ressourcen, konfigurierbar über Values
Raw YAML in GitGitOps-Workflows, wo ein Repository den gewünschten Cluster-Zustand beschreibt

Nützliche Abkürzung

Fügen Sie Ihrer Shell-Konfiguration hinzu:

bash
echo "alias k=kubectl" >> ~/.bashrc
source ~/.bashrc

Ab sofort genügt k get pods statt kubectl get pods.


Backup und Wartung

Ein Cluster ohne Backup-Strategie ist ein Cluster, der auf Datenverlust wartet. Drei unabhängige Ebenen sichern Ihren Kubernetes-Cluster ab.

etcd-Snapshots

etcd speichert den gesamten Cluster-Zustand: Deployments, Services, Secrets, ConfigMaps. k3s erstellt automatisch periodische Snapshots:

bash
ls -la /var/lib/rancher/k3s/server/db/snapshots/

Für einen manuellen Snapshot vor Wartungsarbeiten:

bash
k3s etcd-snapshot save --name manual-backup

Der Snapshot liegt anschließend im selben Verzeichnis. Bewahren Sie kritische Snapshots zusätzlich auf einem externen System auf.

Longhorn Snapshots und Backups

Longhorn erstellt auf Volume-Ebene Snapshots, die sich über die Longhorn UI oder per kubectl verwalten lassen. Für eine vollständige Backup-Strategie kann Longhorn Volumes auf S3-kompatiblen Speicher exportieren. Die Konfiguration erfolgt in der Longhorn UI unter "Settings > Backup Target".

dataforest Cloud Backups

Für vollständige Server-Backups bietet die dataforest Cloud eine zubuchbare Zusatzoption, die komplette Seeds auf Infrastrukturebene sichert. Diese Backups sind unabhängig von Kubernetes und erfassen das gesamte System inkl. aller lokalen Daten.

k3s-Version aktualisieren

Das k3s-Install-Script überschreibt bei jedem Lauf die systemd-Unit. Flags, die Sie beim ersten curl | sh übergeben haben (--cluster-init, --node-ip, --flannel-iface etc.), gehen verloren, wenn sie nicht erneut angegeben werden. Die sicherste Methode ist, die gesamte Konfiguration in eine Datei auszulagern, die das Install-Script nicht anfasst.

Erstellen Sie auf jedem Node die Datei /etc/rancher/k3s/config.yaml mit der jeweiligen Konfiguration. Beispiel für Node 1:

yaml
cluster-init: true
token: <IHR_TOKEN>
node-ip: 10.222.0.1
node-external-ip: <PUBLIC_IP_NODE_1>
flannel-iface: wg0
flannel-backend: vxlan
tls-san:
  - <PUBLIC_IP_NODE_1>
  - 10.222.0.1

Für Node 2 und 3 ersetzen Sie cluster-init: true durch server: https://10.222.0.1:6443 und passen node-ip, node-external-ip und tls-san entsprechend an.

Sobald die Konfiguration in config.yaml liegt, aktualisieren Sie k3s Node für Node:

bash
kubectl drain seed-k8s-01 --ignore-daemonsets --delete-emptydir-data
curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable sh -
kubectl uncordon seed-k8s-01

drain verschiebt alle Workloads auf die verbleibenden Nodes. curl ... | sh installiert die neueste stabile k3s-Version und liest die Konfiguration automatisch aus config.yaml. uncordon gibt die Node wieder frei. Wiederholen Sie den Vorgang für seed-k8s-02 und seed-k8s-03.

Prüfen Sie nach dem Update die Cluster-Version:

bash
kubectl get nodes

Alle Nodes sollten dieselbe Version anzeigen.

WireGuard-Mesh erweitern

Wenn Sie eine vierte Node hinzufügen, muss deren WireGuard-Peer-Konfiguration auf allen bestehenden Nodes ergänzt werden. Jede Node benötigt einen [Peer]-Block mit dem Public Key und der Endpoint-Adresse der neuen Node. Umgekehrt erhält die neue Node die Peer-Blöcke aller bestehenden Nodes.


Weiterführende Schritte

Der Cluster läuft, Storage ist eingerichtet, Workloads sind deployed. Für den produktiven Betrieb gibt es weitere Bausteine, die auf dieser Grundlage aufbauen:

Helm Charts für komplexe Anwendungen: Statt jede Anwendung manuell per YAML zu konfigurieren, stellen Community-Charts fertige Pakete für Datenbanken, Message-Queues, Monitoring-Stacks und mehr bereit. Ein einziger helm install-Befehl deployt eine vollständig konfigurierte Anwendung.

GitOps mit Flux oder ArgoCD: Ein Git-Repository wird zur Single Source of Truth für den Cluster-Zustand. Änderungen werden per Pull Request reviewed und automatisch auf den Cluster angewandt.

Monitoring mit kube-prometheus-stack: Prometheus sammelt Metriken von allen Nodes und Pods. Grafana visualisiert sie in Dashboards. Alertmanager benachrichtigt bei Problemen. Der kube-prometheus-stack installiert alles zusammen per Helm Chart.

Cert-Manager als Alternative: Traefik's eingebauter ACME-Resolver reicht für einfache Setups. Cert-Manager bietet zusätzlich Wildcard-Zertifikate, DNS-01-Challenges und automatische Zertifikatserneuerung als eigenständige Kubernetes-Ressource.

Horizontal Pod Autoscaler: Skaliert Deployments automatisch basierend auf CPU- oder Speicherauslastung. Mehr Traffic bedeutet mehr Pods, weniger Traffic bedeutet weniger Ressourcenverbrauch.

Weitere Nodes hinzufügen: Der Cluster lässt sich jederzeit um eine vierte oder fünfte Node erweitern. Mehr Nodes bedeuten mehr Rechenleistung und höhere Ausfalltoleranz (fünf Nodes tolerieren zwei gleichzeitige Ausfälle).

Mehr über die Vorteile eines eigenen Kubernetes-Clusters erfahren Sie auf unserer Lösungsseite.

Zusammenfassung

Diese Anleitung hat die folgenden Komponenten eingerichtet:

  • Drei Server, verbunden über ein verschlüsseltes WireGuard-Mesh (10.222.0.0/24)
  • k3s als Kubernetes-Distribution mit hochverfügbarer Control Plane (embedded etcd, 3 Server-Nodes)
  • Longhorn als replizierter Storage (2 Replicas pro Volume)
  • Traefik als Ingress Controller mit optionaler Let's-Encrypt-Integration
  • Eine Stateless-Anwendung (nginx, 2 Replicas) und eine Stateful-Anwendung (PostgreSQL mit persistentem Volume)

Der Cluster toleriert den Ausfall eines Servers ohne Unterbrechung. Pods werden automatisch neu verteilt, Daten bleiben auf den Replicas verfügbar. Rolling Updates deployen neue Versionen ohne Downtime.

Bereit loszulegen?

Erstellen Sie Ihren ersten Seed und starten Sie in wenigen Minuten.