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.

Das Betriebssystem installiert...
Das Betriebssystem wird nun installiert. Das dauert circa eine Minute.

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.

Ein Screenshot, der die genannten Shell-Kommandos mit Ausgaben zeigt.

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:

Das nano-Fenster mit der geänderten interfaces Datei

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:

Nur SSH-Server und due Standard-Werkzeuge werden installiert.

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.

Hinweis: Ich habe für bessere Verwaltbarkeit zuvor einen Resource Pool definiert. Bei euch bleibt das Feld einfach frei. Die VM-ID kann auch anders gewählt werden – z.B. 9001

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.

Wir garantieren nur 4 der 10 GiB und aktivieren das Ballooning Device. Somit kann der qemu agent ungenutzten Speicher freigeben, den dann andere VMs oder der Host nach Bedarf nutzen können.

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:
Im Bild fehlt der MTU-Wert! Bitte 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.

Ausgabe des free-Befehls. Kubernetes ist mit Swap nicht kompatibel.
So sollte die Ausgabe des Befehls aussehen – Natürlich weichen die Mem-Zahlen je nach zugewiesenem RAM ab. Taucht hier Swap-Speicher auf, muss das zunächst behoben werden.

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.

Eine Liste der verfügbaren docker-ce Versionen
Derzeit ist 19.03.4 die offiziell empfohlene Version, 19.03.5 ist verfügbar, wir halten uns aber an die Empfehlung

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 mit
systemctl 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 mittels
kubeadm 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 mittels
mkdir -p $HOME/.kube ; cp -i /etc/kubernetes/admin.conf $HOME/.kube/config ; chown $(id -u):$(id -g) $HOME/.kube/configin 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:

Sieht es am Ende in etwa so aus, ist alles wunderbar und die Einrichtung war ein Erfolg.
(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.

About the Author

Inhaber / Streamer / Lead Dev von Firesplash Entertainment und ganz nebenbei Fachinformatiker Systemintegration in einem mittelständigen Unternehmen

View Articles