Die Welt der Container-Orchestrierung hat in den letzten Jahren eine rasante Entwicklung erlebt. Es gibt immer mehr nützliche Tools am Markt, die einem bei der Administration behilflich sind. Da wären Docker, Podman oder der Platzhirsch Kubernetes. Letzterer hat sich als das dominierende Werkzeug für die Verwaltung von Containeranwendungen etabliert.
Mit seiner Fähigkeit, komplexe Container-Infrastrukturen auf einer Vielzahl von Hosts zu managen, hat Kubernetes die Art und Weise, wie wir moderne Anwendungen bereitstellen und verwalten, revolutioniert. Doch der Einsteig in die Materie ist kein leichter. Wer mit einem eigenen Kubernetes-Cluster starten möchte, wird von den schier unzähligen Optionen erschlagen.
Da wären auf der einen Seite die Managed-Cloud-Angebote und auf der anderen die komplexeren On-Prem-Lösungen. Hier hat man dann die Qual der Wahl zwischen komplett zusammengestellten K8s-Distributionen wie Rancher oder der manuellen Installation aller benötigten Binaries. Doch es gibt auch noch einen spannenden Mittelweg. Die Rede ist natürlich von Kubeadm.
In diesem Beitrag werde ich dir Schritt für Schritt zeigen, wie du damit dein erstes Kubernetes-Cluster erstellst. Ich gehe dabei intensiv auf meine Konfiguration sowie das Hinzufügen von Master- und Worker-Nodes ein. Ob du dich letztendlich für den Weg mit Kubeadm entscheidest oder eine andere Route einschlägst, K8s verspricht eine aufregende Reise in die Welt der Container-Orchestrierung.
Kubernetes-Cluster mit Kubeadm:
Wer Kubernetes meistern möchte, wird um ein eigenes Cluster zum Experimentieren nicht herumkommen. Die gesamte Materie ist nämlich sehr komplex und muss daher vollständig durchdrungen werden. Bevor man aber mit einem Kubeadm-Cluster startet, sollte man noch 3 Dinge wissen:
- 2 Server stellen das absolute Minimum für ein Kubernetes-Cluster mit Kubeadm dar. Einer von Ihnen wird als reiner Manager fungieren, während der Andere als Worker die Container bereitstellt.
- Signifikante Hardwareanforderungen gibt es nicht. Als Faustregel gelten 2 GB RAM und 2 CPU-Kerne pro Node als sinnvoll. Mehr ist dabei natürlich kein Nachteil. Ein Kubernetes-Cluster mit Kubeadm läuft beispielsweise sehr performant auf Raspberry Pis der 4. Generation.
- Als Betriebssystem eignen sich Debian, Ubuntu oder auch Rocky Linux sehr gut. Wirklich entscheidend ist das OS bei einem Container-Host aber nicht.
Ich selbst bin ein großer Fan von Red Hat und dessen kostenlosen Derivaten. Alle meine privaten K8s-Cluster nutzen daher Rocky Linux 8 und 9 als Grundlage. Anpassen muss man dabei nur wenige Dinge am Betriebssystem, bevor es mit der Installation von Kubernetes losgehen kann. Ich würde für den Anfang empfehlen, die vorhandene Firewall auf den Servern zu deaktivieren:
# Firewalld abschalten:
systemctl stop firewalld
systemctl disable firewalld
systemctl unmask firewalld
# Iptables deaktivieren:
systemctl stop iptables
systemctl disable iptables
systemctl unmask iptables
Natürlich kann man auch alle Ports für die Kubernetes-Objekte mit einer gut unterstützten Firewall wie iptables freigeben. Da bei einem Cluster mit Kubeadm kein Ingress mitkommt, wäre es sinnvoll einen Teil der Ports zwischen 30000 und 32767 aufzumachen. Diese werden nämlich vom NodePort-Service genutzt und dienen damit dem Exposen von Anwendungen.
Bei Red Hat sollte man außerdem noch das standardmäßig aktivierte SELinux abschalten. Der praktische Sicherheitsmechanismus mit seiner ausgeklügelten Zugriffskontrolle sorgt zwar für ein schwierig zu kompromittierendes System, kann einem aber bei Kubernetes das Leben sehr schwer machen. Wie das dauerhafte Abstellen genau funktioniert, habe ich kurz aufgelistet:
# Sofortiges, aber nur temporäres Deaktivieren:
setenforce 0
# Abschalten für die Ewigkeit, wird nachdem Reboot aktiv:
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
Gerne vergessen wird noch das Deaktivieren des Swap-Speichers auf Kubernetes-Systemen. Leider führt dies häufig zu abrupt auftretenden Leistungsproblemen, wenn beispielsweise ein Teil der im RAM befindlichen Daten auf die viel langsamere Festplatte ausgelagert wird. Nicht selten bricht ein betroffener Container dadurch zusammen. Führe also noch diese 2 Befehle aus:
# Temporäres Deaktivieren des Swap:
swapoff -a
# Dauerhaftes Abschalten des Swap:
sed -i '/swap/ s/^/#/' /etc/fstab
Kernel-Module aktivieren & konfigurieren:
Damit man die für den Betrieb von Containern notwendigen Netzwerk- und Isolationsfunktionen nutzen kann, muss man den Linux-Kernel erweitern. Das geht ganz einfach mithilfe von 2 Kernel-Modulen. Ohne diese würde Kubernetes seinen Job nämlich gar nicht verrichten können. Wie man diese temporär in den Kernel lädt und schließlich alles Reboot-Safe macht, steht hier geschrieben:
# Kernel-Module temporär aktivieren:
modprobe overlay
modprobe br_netfilter
# Kernel-Erweiterungen dauerhaft laden:
cat <<EOF | tee /etc/modules-load.d/containerd.conf
overlay
br_netfilter
EOF
Mittels modprobe hast du die benötigten Module vorübergehend aktiviert. Allerdings wären nach einem Neustart beide Kernel-Erweiterungen inaktiv. Deshalb wurden sie in der Datei containerd.conf festgehalten. Damit werden Sie nach jedem Start des Servers wieder geladen. Obendrein darf die nun mögliche Netzwerk-Konfiguration für das Container-Runtime-Interface nicht fehlen:
# Konfiguration der beiden Kernel-Module:
cat <<EOF | tee /etc/sysctl.d/cri.conf
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
net.bridge.bridge-nf-call-ip6tables = 1
EOF
Die oben getätigten Einstellungen haben einen direkten Einfluss auf die Netzwerk-Konfiguration und die Netzwerk-Isolierung des Clusters. Unter anderem wurde festgesetzt, dass Iptables-Regeln zur Konfiguration der Bridges eingesetzt werden. Hier können natürlich auch andere kompatible Firewalls genutzt werden oder auch gar kein Eintrag hinzugefügt werden, falls keine Firewall aktiv ist.
Darüber hinaus ist noch die IPv4-Weiterleitung aktiviert worden. Damit ist sichergestellt, dass der Datenverkehr zwischen den Pods und zum Host und natürlich auch zur Außenwelt fließen kann. Wer möchte, kann mit den folgenden Befehlen noch testen, ob alle Kernel-Module aktiv sind und die Einstellungen korrekt übernommen worden sind:
# Schauen, ob die beiden Kernel-Module aktiv sind:
lsmod | grep overlay
lsmod | grep br_netfilter
# Überprüfen der getätigten Einstellungen:
sysctl --system
Container-Runtime aufspielen & anpassen:
An einer Softwarekomponente zur Erstellung, Ausführung und Verwaltung von Containern führt bei Kubernetes natürlich kein Weg vorbei. Früher wurde dafür meist Docker genutzt. Inzwischen setzt man aber auf schlankere Lösungen wie Containerd, Rocket, Podman oder CRI-O. Die erstgenannte Lösung ist dabei am gebräuchlichsten und stellt daher meine Wahl für das heutige Tutorial dar.
Containerd ist nicht in den Standard-Repositorys von Rocky Linux enthalten. Daher muss man zuerst die Yum-Utils installieren, bevor man mit dem enthaltenen Config-Manager ein passendes Repository einbinden kann. Ich setze hierfür auf das Docker-Repo. Andernfalls könnte ich nämlich gar nicht erwähnen, dass Docker Containerd als Kernkomponente seiner eigenen Container-Runtime nutzt.
Aber zurück zur Erstellung unseres Kubernetes-Clusters mit Kubeadm. Dabei kann es gelegentlich zu Konflikten während der Installation von Containerd kommen, insbesondere wenn Podman oder Buildah auf dem Server vorhanden sind. Daher ist es ratsam, in solchen Fällen die Deinstallation beider Tools in Betracht zu ziehen. Benötigt werden diese nämlich nicht.
# Yum-Utils installieren:
dnf install -y yum-utils
# Docker-Repo hinzufügen:
yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
# Podman sowie Buildah vom System löschen:
dnf remove -y podman && dnf remove -y buildah
# Containerd installieren:
dnf install -y containerd.io
Sobald Containerd auf allen Kubernetes-Nodes installiert ist, muss es noch konfiguriert werden. Besonders einfach und schnell geht das, indem die Standard-Werte in die eigene Konfiguration übernommen werden. Lediglich die Systemd-Cgroups müssen nachträglich aktiviert werden. Nur so ist zum Beispiel sichergestellt, dass Container keinen Zugriff auf fremde Prozesse bekommen.
# Ordner für die Konfiguration anlegen:
mkdir -p /etc/containerd
# Containerd-Konfiguration erstellen:
containerd config default | tee /etc/containerd/config.toml
# Systemd-Cgroups aktivieren:
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
# Containerd starten & dauerhaft aktivieren:
systemctl enable --now containerd
Crictl einbauen & einrichten:
In der Containerorchestrierung mit Kubernetes und Containerd ist das Kommandozeilenwerkzeug Crictl nicht mehr wegzudenken. Die Hauptaufgabe des Tools besteht darin, mit dem Container-Runtime-Interface zu interagieren. Es bildet damit eine Schnittstelle zwischen dem auf jedem Node laufenden Kublet und der eigentlichen Container-Runtime.
Für uns Administratoren ist crictl ebenfalls nützlich. So sind Funktionen wie die Container- oder Imageverwaltung sehr hilfreich. Weiterhin sind die mitgelieferten Inspektionsmöglichkeiten sehr praktisch beim Debugging von diffizilen Problemen. Vor der eigentlichen Installation muss auf die Version oder genauer gesagt deren Kompatibilität zur Container-Runtime geachtet werden.
Die Inbetriebnahme geht dann aber schnell und vor allem einfach von der Hand. Vergessen werden darf dabei allerdings nicht das Hinterlegen des Unix-Sockets in der Konfiguration. Ansonsten kann Crictl nämlich nicht mit Containerd interagieren. Wie die Installation funktioniert, wo die Konfiguration liegt und was genau eingetragen werden muss, steht im Folgenden beschrieben:
# Download von Crictl:
curl -L https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.17.0/crictl-v1.17.0-linux-amd64.tar.gz --output crictl-v.1.17.0-linux-amd64.tar.gz
# Entpacken des Tar-Verzeichnisses:
tar zxvf crictl-v.1.17.0-linux-amd64.tar.gz -C /usr/local/bin
# Konfigurieren von Crictl:
cat <<EOF | tee /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 2
debug: true
pull-image-on-create: false
EOF
Kubeadm, Kubectl & Kubelet ausrollen:
Unser Kubernetes-Cluster mit Kubeadm nimmt langsam Gestalt an. Allerdings fehlen noch einige integrale Bestandteile von Kubernetes. Wir brauchen zum Beispiel noch Kubeadm zur Cluster-Initialisierung oder Upgrade-Unterstützung. Außerdem fehlt noch die Befehlszeilenschnittstelle Kubectl, mit der sich beispielsweise Container verwalten und debuggen lassen.
Und zu guter Letzt fehlt auch noch das Kubelet auf all unseren Cluster-Knoten. Dieser Agent wird allerdings dringend gebraucht, damit die Verwaltung und Überwachung der Container auf den jeweiligen Servern sichergestellt ist. Zusammen spielen kubeadm, kubectl und kubelet eine entscheidende Rolle bei der Bereitstellung, Verwaltung, Überwachung und Interaktion mit unserem Cluster.
Damit die Installation der Tools funktioniert, muss man erstmal das passende Repository von Google einpflegen. Direkt im Anschluss lässt sich auch schon alles installieren. Da das Kubelet aber kein Container, sondern ein waschechter Systemd-Service ist, muss dieser auch noch gestartet sowie in den Autostart gelegt werden. Wie all das im Detail funktioniert, habe ich natürlich niedergeschrieben:
# Googles Repository für Kubernetes hinzufügen:
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://packages.cloud.google.com/yum/repos/kubernetes-el7-\$basearch
enabled=1
gpgcheck=1
gpgkey=https://packages.cloud.google.com/yum/doc/yum-key.gpg https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg
exclude=kubelet kubeadm kubectl
EOF
# Obligatorische Tools installieren:
yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
# Kubelet starten & dauerhaft aktivieren:
systemctl enable --now kubelet
Cluster initialisieren & ersten Manager erstellen:
Alle Basis-Softwarekomponenten sind jetzt endlich installiert und auch korrekt konfiguriert. Nun kann man mithilfe von Kubeadm das Cluster kreieren und den ersten Manager in Betrieb nehmen. Wichtig ist dabei, dass man das Netzwerk 10.244.0.0/16 für seine Pods benutzt. Andernfalls muss man im Anschluss extrem viel Konfigurationsaufwand betreiben und auch noch starke Nerven haben.
# Kubernetes-Cluster mit Kubeadm erstellen:
kubeadm init --pod-network-cidr 10.244.0.0/16
Nachdem du kubeadm init mit dieser Option ausgeführt hast, wird der Master-Node initialisiert. Sobald dies abgeschlossen ist, erhältst du ein paar wichtige Anweisungen und Befehle im Terminal, wie du den Worker-Node joinen kannst. Das funktioniert zum aktuellen Zeitpunkt aber noch nicht, da kein Netzwerk-Plugin wie Calico, Flannel oder Weave vorhanden ist.
Erst nach der Installation so eines Container-Network-Interfaces können die Pods auf dem Manager mit denen auf dem Worker kommunizieren. Da wir das Cluster mit dem Standard-Netzwerkbereich eingerichtet haben, geht die Installation der oben vorgestellten Plugins leicht von der Hand. Andernfalls muss man die ganzen Manifeste vor der Installation entsprechend anpassen.
Pod-Netzwerk mittels CNI hinzufügen:
Es hat lange gedauert, aber endlich ist der Zeitpunkt gekommen, an dem das erste Manifest zur Installation und Einrichtung einer Kubernetes-Ressource genutzt werden kann. Und keine Sorge, diese Prozedur ist gar nicht kompliziert. Man muss lediglich den folgenden Befehl ausführen und nach etwa einer Minute ist das Netzwerk-Plugin Flannel am Laufen:
# CNI-Plugin Flannel einsetzen:
kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml
Es ist aufgrund seiner Einfachheit und Skalierbarkeit eine beliebte Wahl als Overlay-Netzwerk in kleineren Kubernetes-Clustern. Zudem werden verschiedene Backend-Technologien wie VXLAN oder Host-Gateway unterstützt. Ebenfalls noch interessant sind Sicherheitsfeatures wie Verschlüsselung und Authentifizierungseinstellungen sowie die sehr gute Dokumentation.
Kubectl auf dem Manager einrichten:
Bereits installiert, aber leider noch nicht funktionsfähig, ist des Admins liebster Freund Kubectl. Dem praktischen Befehlszeilentool fehlt nämlich noch die Cluster-Konfiguration, um sich mit dem Manager-Knoten verständigen zu können. Standardmäßig erwartet wird die Konfiguration auf dem Manager im Home-Verzeichnis und dort im versteckten Ordner .kube.
Anstellte die Original-Datei einfach hier herzukopieren, nutzen wir lieber einen auf sie verweisenden Symlink. So ist nämlich sichergestellt, dass Kubectl stets die aktuellste Konfiguration zur Clusterkommunikation nutzt. Wie du alle diese Arbeitsschritte im Detail ausführen musst, sollen dir die unten stehenden Code-Zeilen näher bringen:
# Versteckten Ordner .kube im Home-Verzeichniss anlegen:
mkdir -p $HOME/.kube
# Symboliclink auf die Original-Konfiguration erstellen:
ln -s /etc/kubernetes/admin.conf $HOME/.kube/config
# Korrekte Datei-Berechtigungen setzen:
chown $(id -u):$(id -g) $HOME/.kube/config
Ersten Kubernetes-Worker joinen:
Unser Cluster besteht derzeit nur aus einem einzigen Manager-Knoten. Damit können wir natürlich nicht viel anfangen. Nicht mal ein Anwendungscontainer könnte so zum Laufen gebracht werden. Es wird also allerhöchste Zeit, den ersten Worker in das Cluster zu integrieren. Dafür muss der Beitritts-Befehl vom Master kopiert und auf dem künftigen Arbeiter eingefügt werden.
Oftmals sind die Beitrittsinformationen aber schon abgelaufen oder man findet den besagten Befehl gar nicht mehr in der Terminal-Ausgabe. Das kann passieren und ist auch nicht weiter schlimm. Abhilfe schafft in so einem Fall das folgende Kommando auf dem Master-Knoten:
# Beitrittsinformationen erneut auf dem Manager ausgeben lassen:
kubeadm token create --print-join-command
In der Terminal-Ausgabe solltest du nun einen funktionsfähigen Befehl finden, mit dem der Worker dem gerade erstellten Cluster beitreten kann. Die ganze Prozedur dauert je nach Netzwerk-Geschwindigkeit und Hardware-Ressourcen ein paar Minuten. Bei Problemen gibt es aber zum Glück aussagekräftige Meldungen. Ein häufiger Fehler ist die falsche Konfiguration der Firewall oder des CNI-Plugins.
Hier sollte man bei der Fehlersuche zuerst ansetzen, wenn man mit den Informationen im Terminal wenig anfangen kann. Ist hingegen alles glattgelaufen, kann man sich die Früchte seiner Arbeit im Terminal anzeigen lassen. Dafür muss man auf den Manager-Knoten gehen und einen kleinen Befehl absenden. Als Ausgabe erhält man dann alle Clusterknoten samt Rollen und Status:
# Cluster in der erweiterten Ansicht anzeigen lassen:
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
nebula-01.container.fabianscloud.de Ready control-plane 8m v1.28.2 10.10.10.218 <none> Rocky Linux 9.2 (Blue Onyx) 5.14.0-284.30.1.el9_2.x86_64 containerd://1.6.24
nebula-02.container.fabianscloud.de Ready <none> 5m v1.28.2 10.10.10.219 <none> Rocky Linux 9.2 (Blue Onyx) 5.14.0-284.30.1.el9_2.x86_64 containerd://1.6.24
Falls man Kubectl zusätzlich auch noch auf dem Worker nutzen möchte, kann man in diesem Fall leider keinen Symlink auf die passende Konfiguration erstellen. Hier muss man die Konfiguration aus dem Manager herauskopieren und unter .kube/config auf dem Worker einfügen. Dann funktioniert aber auch hier alles wie gewohnt und es kann los mit dem Kubernetes-Spaß gehen.
Ersten Pod im Kubeadm-Cluster deployen:
Das Cluster steht so weit, aber einen vorzeigbaren Workload gibt es noch nicht. Hier muss also noch Hand angelegt werden. Ich teste meine Kubeadm-Cluster sehr gerne mithilfe eines Nginx-Pod, der anhand eines NodePort-Service der Öffentlichkeit zugänglich gemacht wird. Das fertige Manifest ist nicht besonders komplex und schaut wie folgt aus:
apiVersion: v1
kind: Namespace
metadata:
name: nginx-test
---
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
namespace: nginx-test
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx:latest
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: nginx-test
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
nodePort: 31800
targetPort: 80
type: NodePort
Damit du den Pod sowie den dazugehörigen Service online nehmen kannst, musst du auf einem der vorhandenen Cluster-Knoten eine leere Datei mit der Endung .yaml erstellen. Dort fügst du jetzt einfach den obigen YAML-Code ein. Nun müssen diese API-Anweisungen nur noch vom Cluster verarbeitet werden. Das geht ganz einfach mithilfe des Tools Kubectl:
# Manifest verarbeiten lassen:
kubectl apply -f deine-angelegte-datei.yaml
Du kannst nun auf deinen Nginx-Pod zugreifen, indem du die IP-Adresse irgendeines Cluster-Knotens mit der im Code hinterlegten Portnummer 31800 kombinierst. Sollte das nicht auf Anhieb funktionieren, bedarf es noch etwas Fehlersuche. Folgende Befehle sollten dir dabei eine große Hilfe sein und dich das Problem umgehend beheben lassen:
# Nginx-Pod anzeigen lassen:
kubectl get pods -n nginx-test
# NodePort-Service ausgeben lassen:
kubectl get services -n nginx-test
Dauerhaft möchte man natürlich keinen Nginx-Container mit einer öden Hallo-Botschaft am Laufen haben. Die 3 frisch erstellten Kubernetes-Objekte lassen sich direkt wieder in einem Rutsch entfernen. Möglich mach dies der Befehl kubectl delete -f <deine-angelegte-datei.yaml> Gratulation an dieser Stelle, dass du bis hier durchgehalten hast. Du kannst stolz auf dich sein!
Häufig Fragen & meine Antworten:
Das Aufsetzen eines Kubernetes-Clusters mithilfe von Kubeadm ist schnell gemacht. Bei der täglichen Arbeit ergeben sich dann aber häufig ein paar Fragen, deren Beantwortung gar nicht so leicht ist. Aufgrund dessen erreichen mich ab und an E-Mails mit interessanten Problemen. Das hat mich schließlich dazu veranlasst, diesen Beitrag um einen kleinen Frage-Antwort-Bereich zu erweitern:
Im Prinzip reicht es schon den Befehl kubeadm reset auf dem Server auszuführen. Gibt es im Cluster dann nur einen Manager, sind alle Kubernetes-Objekte für immer verschwunden. Es sollten aber noch die Konfigurationen unter /etc/kubernetes/ und /var/lib/kubernetes/ entfernt werden. Nun befindet sich das ganze Cluster in einem sauberen Zustand und kann erneut initialisiert werden.
Ich selbst nutze als Betriebssystem Rocky Linux 8 oder 9. Allerdings lässt sich die folgende Anleitung auch auf andere Betriebssysteme adaptieren. Anhängen lassen sich die Rollen, indem man die Konfiguration in der .bashrc-Datei einfach um eine kleine Zeile erweitert: PS1=“\u@\h [Worker]:\w\$ “
Diese Anpassung bewirkt, dass mein Shell-Prompt nun den Benutzernamen (\u), den Hostnamen (\h) und die Rollenbezeichnung ([Worker]) anzeigt, gefolgt vom aktuellen Verzeichnis (\w) und dem üblichen Eingabe-Prompt ($). Diese einfache Änderung erleichtert mir die Identifizierung der Rolle jedes Servers und trägt zur Übersichtlichkeit meiner Arbeit bei.