Diese Artikel-Serie beschäftigt sich mit der Einrichtung eines hybriden Kubernetes- und KVM-Clusters mithilfe von Proxmox bei Hetzner Online.
Erster Teil | Vorheriger Teil | Nächster Teil
Im dritten Teil haben wir unserem Proxmox-Cluster Storage hinzugefügt und das Licht der Welt gezeigt. Jetzt, da wir eine Internetverbindung haben, können wir den zweiten Layer installieren. Kubernetes!
Was ist Kubernetes?
Kubernetes (K8s) ist ein Open-Source-System zur Automatisierung der Bereitstellung, Skalierung und Verwaltung von containerisierten Anwendungen.
Quelle: Offizielle Kubernetes Website – https://kubernetes.io/de/
Mit eigenen Worten: Kubernetes ist eine Art Framework, das es uns mit relativ einfachen Mitteln ermöglicht, einfache wie auch komplexe (Multi-)Container-Basierte Anwendungen flexibel skalierbar, redundant und effizient über mehrere Nodes verteilt auszuführen und bereit zu stellen.
Hinweis
Dieser Artikel ist die Fortsetzung unserer Serie, die nun leider für circa ein halbes Jahr unterbrochen werden musste. Oben im Navigator können alle Artikel der Serie nochmals erreicht werden.
Vorbereitung
Wir werden unsere Kubernetes-Umgebung in virtuelle Maschinen verpacken. Dies hat für uns einen großen Vorteil: Einfachere Datensicherung und Replikation. Außerdem können wir den Ingress-Traffic so durch unsere Firewall-VM leiten und damit sogar einige Spielereien betreiben.
Alternativ können wir den VMs aber auch ein Standbein im WAN-Netz geben, um den Ingress direkt über MetalLB mit einer öffentlichen IP zu versehen.
Wir erstellen uns zunächst ein allgemeines Template für Debian 10. Hierfür legen wir wieder eine neue VM an. Ich wähle 2 Cores, 1024MB RAM, 80 GB HDD. Als CD wähle ich die zuvor von mir auf den Cluster geladene Netinst-CD für Debian 10. Die Netzwerkkarte hänge ich in unser LAN.
Betriebssystem installieren
Nach dem Starten der VM wählen wir den nicht grafisch Installer. Die Auswahlen für Sprache, Land etc erkläre ich hier nicht. Die Netzwerkkonfiguration wird keinen DHCP finden, deshalb werden wir aufgefordert, dien nötigen Einstellungen von Hand zu treffen.
Wir vergeben eine IP aus dem LAN-Bereich und nutzen die Firewall als Gateway und DNS.
Der Rechnername kann frei gewählt werden, z.B. debian10tpl.
Wir vergeben ein Root-Kennwort und legen einen “Admin”-User an.
Bei der Partitionierung der Festplatte können wir entweder standardgemäß einen SWAP-Speicher anlegen, den wir dann später für Kubernetes deaktivieren müssen, oder wir lassen ihn gleich von vorn herein weg.
Wir gehen den Standardweg und belassen den SWAP. Dafür nutzen wir die Geführte Partitionierung für die vollständige Festplatte. Wir lassen alle Dateien auf eine Partition legen.

Vorsicht, Falle!
Wenn wir gefragt werden, ob wir eine weitere CD einlegen wollen, wählen wir Zurück. So gelangen wir in ein Auswahlmenü, von dem aus wir Eine Shell ausführen können:

In der Shell müssen wir nun die MTU auf 1350 setzen, sonst werden die Frames von den Switchen verworfen, wenn unsere VM versucht, die Pakete vom Mirror abzurufen.
Zum Glück geht das in dieser Shell ganz leicht. Wir müssen uns zunächst mittels ip link show
anzeigen lassen, wie unser Netzwerk-Interface heißt. In unserem Fall ens18. Danach setzen wir dessen MTU mit ip link set mtu 1350 ens18
.

Wir müssen diese Einstellung nun auch noch im installierten System abändern. Hierfür nutzen wir den Befehl nano /target/etc/network/interfaces
und fügen im Block iface ens… unterhalb des Gateways eine neue Zeile mtu 1350
ein und speichern mit Strg+X -> Y -> Enter:

Wir geben in der Shell den Befehl exit
ein und landen wieder im Installer-Menü. Dort geht es nun mit “Paketmanager konfigurieren” weiter.
Zurück im Assistenten
Wir wollen keine weitere CD einlesen. Im nächsten schritt wählen wir einen Mirror aus. Man könnte hier einen geografisch nahegelegenen Server wählen oder einfach den Standard beibehalten.
Sollte der Installer nun bei 33% stehen bleiben und dann nach ca 15 Minuten mit einem Mirror-Fehler abbrechen, ist dies ein Anzeichen, dass irgendwo die MTU falsch eingestellt ist.
Gegen die Paketverwendungserfassung spricht meines Erachtens nach nichts. Als Basis-Software wählen wir hier ein sehr mageres Set:

Zu guter Letzt lassen wir den Grub Bootmanager noch nach /dev/sda installieren. Der Server sollte nun ins installierte Debian OS 10 rebooten.
Nicht vergessen, die CD zu entfernen.
Weitere Vorbereitungen für das Template
Dieses Template können wir zukünftig nutzen, wenn wir einen neuen Server erstellen wollen. So sparen wir uns eine Menge Arbeit. Zunächst sollten wir noch ein paar Tools installieren und Einstellungen treffen, was uns die Einrichtung neuer Debian-Server fast schon zu einfach machen wird. Wir loggen uns in die Konsole des Template-Servers ein.
Ich persönlich habe gerne meine Standardwerkzeuge beisammen. Nano gehört eh schon dazu, also installieren wir nur noch htop, haveged und den qemu agent mittels apt install htop haveged qemu-guest-agent
.
Wir nutzen ein Monitoring-System, dessen Agent installiere ich beispielsweise in diesem Schritt auch gleich mit. Weitere Konfigurationen (wie beispielsweise der SSH-Key) und Installationen können hier jetzt nach eigenen Vorstellungen durchgeführt werden.
Modern – Ein bisschen Magie, oder auch nicht…
An dieser stelle hätte ich euch gern erzählt, wie toll CloudInit doch ist. Dass man damit mit wenigen Klicks ein Template so vordefinieren kann, dass man in Sekunden automatisch eine VM erzeugen kann bei der bereits Netzwerk, Name usw alles konfiguriert ist. Leider ist Proxmox im Netzwerk-Bereich da aber noch nicht so weit, dass man auch die MTU eintragen kann (bei mir existiert inzwischen zwar das Feld, aber eine Änderung führt zu einem Fehler beim Speichern der Netzwerkkonifguration – Habt ihr andere Erfahrungen? Schreibt es in die Kommentare!).
Wir werden CloudInit daher leider überspringen. Man könnte zwar Config-Files basteln und rein linken, das ist aber so aufwändig, dass man gleich die vier Handgriffe (Netzwerkkonfiguration, Hostname, Hosts-Datei und Host-Keys) selbst erledigen kann.
Der Klassiker – Teil 1 – Das Template Klonen
Wir wandeln unsere VM nach dem Herunterfahren nun also per Rechtsklick in ein Template um. Dieses können wir dann klonen um unsere VMs zu erhalten. Wir müssen dann lediglich diese VMs noch “spezialisieren”. Dies zeige ich nun am Beispiel einer Kubernetes Worker Node-VM:
Zunächst möchte ich noch anregen, für unsere Kubernetes-VMs einen eigenen, hohen ID-Bereich zu erküren. Ich werde 9000ff verwenden.
Wir klonen nun also das Template in eine VM mit der ID 9001 für unseren KubeNode1 durch einen Rechtsklick auf das Template und wählen Clone. Dafür nutzen wir das Full Clone Verfahren, da wir keinen shared Storage nutzen.
Das selbe Verfahren gilt für alle Nodes. Eine KubeNode-VM je Worker Node.

Nun müssen wir unseren Nodes noch ordentlich Ressourcen zuweisen. Das geht wieder über den Hardware-Tab. Ich weise hier 10GiB RAM zu. Die Anzahl Cores je CPU-Socket stellen wir auf 4, die Sockets auf 1. Die Netzerkkarte verbleibt im LAN-Bereich.
Aunahme: Die Control Plane (Ich nenne sie KubeMaster) wird nur 2GiB und 2 Cores erhalten. Das reicht weit.

Hinweis
Ursprünglich war der Plan, die Cloud VM als Control Plane zu nutzen. Leider haben meine Experimente während dieses Projekts gezeigt, dass das Installieren von Docker und Kubernetes parallel zu Proxmox und der darunter liegenden LXC-Engine zu Problemen verschiedenster Art führt. Aus diesem Grund wurde die Strategie geändert.
Der “Master” Node ist nun nur noch Quorum und Router. Warum wir den überhaupt brauchen, und nicht einen der Worker Nodes dafür her nehmen hat einen einfachen Grund: Ausfallsicherheit. Die Cloud-VM ist hochverfügbar.
Der Klassiker – Teil 2 – Aus Klon, mach Variante
Wir brauchen für das weitere Procedere wieder eine IP-Logik.
Wir werden im weiteren verlauf für jeden Kubernetes-Node – Inklusive Control Plane – zwei IPv4-Adressen benötigen. Jeweils eine als Node-IP und eine weitere als Ingress-IP. Im Ingress-Bereich können später weitere IPs folgen.
Unser internes Netz war 192.168.200.0/24
Ich nutze folgende Logik: 192.168.200.2N0 als Node IP 192.168.200.2N1 als Ingress 1, und reserviere mir 192.168.200.2N2-2N9 für weitere Ingress-Verwendungsbereiche.
Somit bietet meine Strategie Platz für Control Plane + 4 Worker Nodes und jeweils 9 Ingress IPs. Für Kubernetes ist damit dann der IP-Adressblock 200-249 reserviert.
Node 1 bekommt dann also als fest konfigurierte IP die 192.168.200.210 und als zusätzliche IPs die 211 – 219.
Plant ihr, mehr Nodes zu brauchen, könnte man die Strategie umdrehen: 2XN wobei X = Ingress Nummer und N = Node ID. Das wären dann CP + 9 Nodes mit je einer Node- und 4 Ingress IPs.
Man kann aber natürlich später auch immer von der Strategie abweichen oder diese um weitere IP-Blöcke erweitern.
Theoretisch bräuchten wir nur eine IP je Node, da wir den Ingress ohnehin auf einen höheren Port legen werden. Wir befinden uns aber in einem internen IP-Netz und haben daher den Luxus, mehr als ausreichend IP-Adressen zur Verfügung zu haben. Daher trennen wir sauber.
Nach Anpassung der Konfiguration starten wir die neu erstellte VM und öffnen die Konsole. Nach dem Einloggen müssen wir folgende Dateien ändern:
- /etc/hostname – Hostname ohne Domain eintragen
- /etc/hosts – Node-IP und Hostname anpassen
- /etc/fstab – Die Swap-Zeile entfernen
- /etc/network/interfaces Node-IP und Zusatz-IPs eintragen. Beispiel:

mtu 1350
nicht vergessen!Danach die VM einmal neu starten, damit sie die Konfiguration übernimmt.
Nun müssen wir nur noch die Host-Identifikation anpassen. Dazu führen wir folgende Befehle aus:
rm /etc/ssh/ssh_host_*
dpkg-reconfigure openssh-server
service ssh restart
Verfügbarkeit
Stand jetzt liegen unsere VMs alle noch auf dem Node, auf dem wir das Template kreiert haben. Das ändern wir, indem wir sie mittels Rechtsklick -> Migrate auf die jeweiligen Nodes verschieben. Damit können wir schon mal einen Host-Ausfall abfedern.
Natürlich haben wir aktuell aber noch einige Single points of failure. Das nehmen wir aber in einem anderen Teil in Angriff. Hier wollen wir uns zunächst noch um die Basisinstallation der Kubernetes-Software kümmern.
Installation der Kubernetes-Software
Die folgenden Schritte werden auf allen Kubernetes-Nodes inklusive Control Plane ausgeführt.
Im weiteren werden wir wieder zwischen Worker Nodes (Die VMs mit den Namen KubeNodeX) und der Control Plane (KubeMaster) unterschieden.
Bereits beim Klonen des Templates haben wir den SWAP deaktiviert. Wir prüfen dies nochmals mit dem Befehl free -h
an der SSH-Konsole.

Wir haben unsere Nodes mit dem aktuellen Release Debian Buster installiert. Es gibt ein paar Pakete, die zum Zeitpunkt des Erscheinens dieses Artikels nicht kompatibel sind, und das sind die “neuen” ip- und arptables. Glücklicherweise ist Debian aber ein sehr konservatives Derivat und ermöglicht uns mit einem einzigen Befehl auf das alte Verhalten zurückzuschalten: update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
update-alternatives --set arptables /usr/sbin/arptables-legacy
update-alternatives --set ebtables /usr/sbin/ebtables-legacy
Hinweis: Es kann bei diesen Befehlen jeweils zu einem Fehler kommen:update-alternatives: error: alternative XY for YZ not registered; not setting
Dieser tritt auf, wenn das jeweilige Paket nicht installiert ist und kann ignoriert werden.
Wir werden auch die Paketquellen für Kubeadm und Docker benötigen. Dafür führen wir folgende Befehle nacheinander aus (basierend auf der offiziellen Dokumentation):
apt update ; apt -y upgrade ; apt install -y apt-transport-https curl gnupg2 ca-certificates software-properties-common
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
# Docker Repo
cat <<EOF | tee /etc/apt/sources.list.d/docker.list
deb [arch=amd64] https://download.docker.com/linux/debian buster stable
EOF
# Kubernetes Repo - Die Debian repos sind nicht vollständig, Ubuntu ist hier aber voll Bitkompatibel
cat <<EOF | tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
apt update
Installation der Container Runtime für Kubernetes
Kubernetes benötigt zum Ausführen unserer Container auf allen Kubernetes-Nodes inklusive Control Plane eine Container Runtime (CRI). Dies ist im Regelfall Docker. Wir werden nun auf allen Nodes eine bestimmte Docker-Version installieren. In unserem Fall ist das die Version 19.03.4.
Man sollte hier immer die aktuellste – unterstützte – Version nutzen.
Die unterstützen Versionen finden sich hier, die verfügbaren Versionen findet man über den Befehl apt-cache madison docker-ce
heraus.

Wir installieren die Docker-Runtime mit den empfohlenen Versionsständen und pinnen diese Version dann fest, um versehentliche Updates im Rahmen der Systemupdates zu vermeiden:
apt -y install \
containerd.io=1.2.10-3 \
docker-ce=5:19.03.4~3-0~debian-buster \
docker-ce-cli=5:19.03.4~3-0~debian-buster
apt-mark hold containerd.io docker-ce docker-ce-cli
Nach dem Abschluss der Installation, die durchaus ein paar Minuten dauern kann, müssen wir Docker gemäß der Empfehlung noch so konfigurieren, dass es systemd als cgroup-driver nutzt und ein Verzeichnis dafür anlegen:
cat > /etc/docker/daemon.json <<EOF
{
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2"
}
EOF
mkdir -p /etc/systemd/system/docker.service.d
Wir laden systemd neu und starten schließlich den Docker-Service neu mitsystemctl daemon-reload
systemctl restart docker
Installation der Kubernetes-Tools
Bevor wir unseren Kubernetes-Cluster erstellen können, brauchen wir schlussendlich noch die dazugehörigen Werkzeuge – ebenfalls auf allen Kubernetes-Nodes und der CP. Wir installieren diese mit apt und pinnen dann deren Versionen fest, damit wir diese nicht versehentlich zu einer inkompatiblen Version updaten. Für Upgrades gibt es spezielle Vorgehensweisen.
apt install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl
Unsere kubelets auf den Nodes befinden sich nun in einer Neustart-Schleife, während sie darauf warten, konfiguriert zu werden.
Die Einrichtung des Clusters
Den ersten Teil der Einrichtung führen wir auf der Control Plane VM durch.
Wir werden nun mit wenigen Befehlen eine Kubernetes Control Plane installieren. Für das Pod-Networking werden wir wieder ein Overlay-Netzwerk aufspannen. Die Wahl fällt hier auf Flannel, welches wohl die einfachste und flexibelste Variante ist.
Zunächst initialisieren wir den Cluster mittelskubeadm init --apiserver-advertise-address=192.168.200.200 --control-plane-endpoint=192.168.200.200 --pod-network-cidr=10.244.0.0/16
Hinweis: Beim –control-plane-endpoint könnte man auch eine andere IP verwenden. Das ermöglicht dann ein späteres Upgrade auf ein Multi-CP-Setup. Die andere IP müsste dann der Control Plane zusätzlich zugeordnet werden (oder schon jetzt über einen Balancer laufen). Ohne Multi-CP-Gedanken könnten wir die Parameter auch weg lassen, ab auf diesen Wege halten wir uns das für später offen.
Die IP-Adresse ggnf anpassen! Wir verwenden hier gezielt die “private” IP in unserem Virtuellen HA-WAN. So können alle Nodes damit sprechen, die API wird aber nicht ins Internet veröffentlicht.
Ist das abgeschlossen, notieren wir uns den Join-Befehl am Ende der Ausgabe.
Wir kopieren uns die Konfiguration für kubectl mittelsmkdir -p $HOME/.kube ; cp -i /etc/kubernetes/admin.conf $HOME/.kube/config ; chown $(id -u):$(id -g) $HOME/.kube/config
in unser Home-Verzeichnis um die Einrichtung fortsetzen zu können.
Nun müssen wir die sysctl aller Nodes anpassen. Also auch hier wieder Worker und CP.
Wir fügen ans Ende der /etc/sysctl.conf Datei folgende Zeile an:net.bridge.bridge-nf-call-iptables=1
danach laden wir die geänderte Einstellung mittels sysctl -p
und initialisieren das Flannel Pod-Netzwerk mit folgendem SSH-Befehl auf der Control Plane:kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
I feel so lonely…
Ein Node allein reicht für einen glücklichen Cluster nicht aus. Darüber hinaus wird unsere Control Plane auch verweigern, Pods auszuführen.
Um diesen Umstand zu verbessern, fügen wir unsere Worker Nodes nun noch zum Cluster hinzu. Dafür führen wir auf allen Worker Node VMs nach und nach das vorhin notierte Join-Command aus: kubeadm join 192.168.200.200:6443 --token TOKEN-HIER --discovery-token-ca-cert-hash sha256:HASH-HIER
Wenn wir jetzt – wie vom Tool vorgeschlagen – auf der Control Plane den Befehl kubectl get nodes ausführen, sollten wir alle Nodes sehen können. Es kann 1-2 Minuten dauern, bis alle den Status Ready haben. Der “Master” wird als Control Plane die “master” role halten:

(Screenshot wurde ein halbes Jahr nach der Einrichtung angefertigt, bei euch dürfte die “Age” nur Minuten betragen.)
Fazit und Ausblick
Wir haben im vierten Teil nun einen 3-Node-Kubernetes-Cluster aufgespannt. Bisher haben wir keine Speicherbereiche sowie Außenkommunikation des Kubernetes-Clusters eingerichtet, das folgt dann im fünften Schritt. Darüber hinaus werden wir uns noch mit der Replikation der Proxmox-VMs befassen.
Ihr habt Fragen oder Anregungen? Womöglich Kritik? Dann ab in die Kommentare damit!
In eigener Sache: Aufgrund der aktuellen Situation und deren Folgen kann ich aktuell die Artikel nur unregelmäßig posten. Hierfür bitte ich um euer Verständnis.
Eigentlich sollte genau ein Ingress auf der IP Adresse des Masters auf http und https hören und dann A) subdomain based oder Path based die Anfragen in den Cluster zu den entrechenden services leiten.
Ich gebe dir prinzipiell recht, allerdings würde das – um meines Erachtens nach sinnvoll zu sein – voraussetzen, dass der Ingress in unserem Fall auf der Hetzner-Cloud-VM läuft.
Ich hatte bei meinen Tests keinen Erfolg, Kubernetes und Proxmox parallel direkt auf dem Host zu installieren. Ich vermute dass die CNI hier das Problem ist, da Proxmox ja selbst u.a. LXC verwendet. Aus diesem Grund sind alle Kubernetes-Nodes (inklusive CP) virtuelle KVM-Maschinen auf den Proxmox Worker Nodes.
Man könnte natürlich einen Ingress definieren, der auf unsere Control Plane (Master) VM gepinnt ist. Ich finde aber den Aspekt der höheren Ausfallsicherheit, wenn jeder Node den Ingress tragen kann, reizvoller. Crasht unsere Control plane, kann der Ingress nahezu nahtlos auf einem anderen Node die Arbeit übernehmen. Dafür sorgt der HAProxy auf der Firewall/RouterVM.
Klar, die Firewall ist damit der SPOF. Das wäre sie aber so wie so.
Ich bin kein Kubernetes-Profi, aber ich sehe keinen echten Nachteil darin. Falls du uns hierzu mehr sagen kannst, bin ich sehr gespannt 🙂
Hallo,
danke für die tollen Artikel! Mich würde ein brauchbares Konzept interessieren, bezüglich HA Storage für die KVM VMs, also über NFS oder CEPH oder so?
Da hapert es bei mir noch.
Hallo Lensch,
danke für die Lorbeeren!
CEPH macht durch die Echtzeit-“Datenreplikation” IMHO nur Sinn, wenn du dahinter eine sehr potente Netzwerk-Infrastruktur (10G) hast.
Außerdem sollte man bei CEPH auch gut Flash-Speicher einplanen, gerade für den Cache (pardon die genauen Bezeichnungen fallen mir gerade nicht ein).
Im 2-Node-Cluster wäre mit Ceph auch zu unflexibel/unsicher, denn bei einer Downtime eines Nodes fehlt potenziell jegliche Daten-Redundanz – Außer du verschwendest den Speicher förmlich und machst 4 Block-Kopien.
NFS wäre da wohl das Mittel der Wahl, bedenke nur, dass du für echte HA auch da eine Redundanz haben wollen wirst. Du musst hier also auch wieder eine Replikation oder ein verteiltes Dateisystem schaffen.
Die Frage ist als nicht primär, wie du den speicher zur VM (bzw zum Node) bekommst, sondern viel mehr wie du den Speicher aufbaust. Du willst ja keine HA-Umgebung, bei der das Storage am ende SPOF (Single Point Of Failure) ist.
Vielleicht wäre auch GlusterFS eine Lösung?
Im Grunde zeigen die Requirements und die Mittel den Weg. Möglichkeiten gibt es viele, was dabei aber sinnvoll ist, muss man für sich selbst unter Beachtung diverser Punkte (Budget, IOPS, Verfügbarkeitsanspruch Echte HA, eventuelle rechtliche Vorgaben) ausloten. Zeil meines Konzepts war eine erhöhte Verfügbarkeit der kritischen Dienste bei möglichst geringen Kosten zu realisieren.