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.
| Node | Öffentliche IP | WireGuard-IP | Rolle |
|---|---|---|---|
| seed-k8s-01 | (Ihre IP) | 10.222.0.1 | Server |
| seed-k8s-02 | (Ihre IP) | 10.222.0.2 | Server |
| seed-k8s-03 | (Ihre IP) | 10.222.0.3 | Server |
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: Planentry-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:
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
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.
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.
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:
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:
apt install iptables
Der Cluster benötigt folgende Ports. Jeder Port erfüllt eine spezifische Funktion:
| Port | Protokoll | Verwendung |
|---|---|---|
| 51820 | UDP | WireGuard-Tunnel zwischen den Nodes |
| 6443 | TCP | Kubernetes API Server (kubectl-Kommunikation, Node-Registrierung) |
| 9345 | TCP | k3s Supervisor API (Nodes treten dem Cluster bei) |
| 10250 | TCP | Kubelet API (API Server kommuniziert mit Kubelets auf jedem Node) |
| 2379-2380 | TCP | etcd Client- und Peer-Kommunikation (Cluster-State) |
| 8472 | UDP | VXLAN (Flannel Overlay-Netzwerk zwischen Pods) |
| 80 | TCP | HTTP Ingress (eingehender Web-Traffic) |
| 443 | TCP | HTTPS Ingress (eingehender Web-Traffic, TLS) |
Öffnen Sie diese Ports für den Cluster-internen Traffic (WireGuard-Subnetz) und die öffentlich erreichbaren Ports:
# 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:
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:
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:
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:
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
[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
[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
[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:
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:
ping -c 3 10.222.0.2
ping -c 3 10.222.0.3
Auf Node 2:
ping -c 3 10.222.0.1
ping -c 3 10.222.0.3
Auf Node 3:
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:
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:
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:
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:
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:
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 auchwireguard-nativeals 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:
kubectl get nodes
Die Ausgabe sollte einen Node mit Status Ready zeigen:
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:
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:
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:
kubectl get nodes
Die Ausgabe sollte drei Nodes mit Status Ready zeigen:
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:
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:
sed -i 's/127.0.0.1/<PUBLIC_IP_NODE_1>/' ~/.kube/config-k8s-cluster
Setzen Sie die Umgebungsvariable, damit kubectl diese Konfiguration verwendet:
export KUBECONFIG=~/.kube/config-k8s-cluster
Testen Sie den Zugriff:
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:
# 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:
journalctl -u k3s -f
Häufige Fehlermeldungen und ihre Ursachen:
certificate signed by unknown authority: Die--tls-sanFlags fehlen oder die IP stimmt nicht.etcd cluster is not healthy: Die WireGuard-Verbindung zwischen den Nodes ist instabil. Prüfen Siewg showauf aktive Handshakes.connection refusedauf 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:
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.
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Prüfen Sie die Installation:
helm version
Longhorn per Helm installieren
Fügen Sie das offizielle Longhorn-Repository hinzu und installieren Sie das Chart:
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-systemerstellt einen dedizierten Namespace, damit alle Longhorn-Komponenten vom restlichen Cluster isoliert sind.--create-namespacelegt den Namespace an, falls er noch nicht existiert.--set defaultSettings.defaultReplicaCount=2speichert 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:
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:
- Explizit pro PVC: Jeder PersistentVolumeClaim gibt
storageClassName: longhornan. Das ist expliziter und dokumentiert im Manifest, welcher Storage verwendet wird. - Als Standard-StorageClass: Longhorn wird zum Default. PVCs ohne explizite
storageClassNameverwenden automatisch Longhorn.
In diesem Tutorial setzen wir Longhorn als Standard, damit PVCs ohne explizite Angabe automatisch replizierten Storage erhalten:
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:
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:
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:
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:
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:
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:
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
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:
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/v1gibt die API-Gruppe an, die Deployments bereitstellt.kind: Deploymentsorgt dafür, dass Kubernetes die gewünschte Anzahl an Pods automatisch aufrechterhält.replicas: 2erstellt zwei identische Pods. Fällt einer aus, läuft der andere weiter.selector.matchLabelsverbindet das Deployment mit seinen Pods über das Labelapp: web.templateist die Vorlage für jeden Pod. Alle Pods bekommen das Labelapp: web.image: nginx:alpineverwendet das offizielle nginx-Image in der schlanken Alpine-Variante.containerPort: 80dokumentiert, auf welchem Port die Anwendung lauscht.
Service erstellen
Erstellen Sie eine Datei service.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
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
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:
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:
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.
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:
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.
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:
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:
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.envsetzt das Passwort und den initialen Datenbanknamen. In einer Produktionsumgebung gehört das Passwort in ein Kubernetes Secret.PGDATAsetzt das tatsächliche Datenverzeichnis auf ein Unterverzeichnis (pgdata). Longhorn-Volumes enthalten einlost+found-Verzeichnis im Root. PostgreSQL startet nicht in einem Verzeichnis, das bereits Dateien enthält. Durch das Unterverzeichnis wird das Problem umgangen.volumeMountshängt das Longhorn-Volume unter/var/lib/postgresql/dataein.volumesverweist auf den zuvor erstellten PersistentVolumeClaim.
PostgreSQL-Service erstellen
Erstellen Sie eine Datei postgres-service.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
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
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?
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:
kubectl -n demo get pods -w
Sobald der neue Pod Running ist, prüfen Sie die Daten:
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
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:
kubectl drain seed-k8s-03 --ignore-daemonsets --delete-emptydir-data
Die Flags im Detail:
--ignore-daemonsetslässt systemweite DaemonSet-Pods (wie Longhorn oder Flannel) auf der Node laufen. Diese werden von ihren DaemonSets verwaltet.--delete-emptydir-dataerlaubt das Löschen von Pods mit temporären emptyDir-Volumes.
Prüfen Sie, dass alle Workload-Pods auf die verbleibenden Nodes umgezogen sind:
kubectl get pods -n demo -o wide
Nach der Wartung geben Sie die Node wieder frei:
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
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
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
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
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
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
kubectl create namespace produktion
kubectl get namespaces
Namespaces trennen Umgebungen (z.B. staging und produktion) oder verschiedene Anwendungen voneinander.
Labels und Selektoren
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
| Ansatz | Einsatz |
|---|---|
kubectl apply -f | Einzelne Manifeste, einfache Anwendungen, Lernzwecke |
| Helm Charts | Komplexe Anwendungen mit vielen Ressourcen, konfigurierbar über Values |
| Raw YAML in Git | GitOps-Workflows, wo ein Repository den gewünschten Cluster-Zustand beschreibt |
Nützliche Abkürzung
Fügen Sie Ihrer Shell-Konfiguration hinzu:
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:
ls -la /var/lib/rancher/k3s/server/db/snapshots/
Für einen manuellen Snapshot vor Wartungsarbeiten:
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:
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:
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:
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.