Ready to go?

Kubernetes im Homelab mit kubeadm aufsetzen

20. Dezember 2020

Kubernetes aufsetzen ist leicht. Zumindest wenn man alles genau so macht wie ein HowTo es einem erklärt. Doch sobald man etwas anders machen will wird es schnell mal kompliziert und klappt meistens auch nicht auf Anhieb. Ich wurde mit diesem Problem konfrontiert als ich versuchte in meinem Homelab einen Cluster mit kubeadm, containerd, custom DNS-Name, etcd. aufzusetzen. Ich habe schnell mal den Überblick verloren und wusste nicht mehr was ich bereits gemacht habe und was nicht. Deshalb habe ich mich dazu entschlossen meine Erfahrungen in diesem Blog zu dokumentieren. Vielliecht hilft es dir bei deinen Experimenten und sonst weiss ich dann in 2 Jahren wo ich nachschauen kann...

 

Überblick

Hier zuerst einen kurzen Überblick wie ich dies versuche zu bewerkstelligen. Ich habe ein IPv4 Netz 192.168.113.0/24 mit einem Router (pfSense) der die IP 192.168.113.1 hat. Innerhalb dieses Netzes können die Server frei miteinander kommunizeiren. Folgende Server habe ich bereits aufgesetzt und mit einer default config bespielt (SSH Keys, oh-my-zsh,...):

Name OS CPU Memory Mode IP
banana Ubuntu Server 20.04 LTS 2 vCores 2048MB Control Plane 192.168.113.11
kiwi-01 Ubuntu Server 20.04 LTS 4 vCores 6144MB Worker

192.168.113.21

Auf beiden Servern ist keine Firewall installiert (ufw oder änhliches).

Kubernetes kann nicht installiert werden sofern SWAP aktiviert ist, deshalb habe ich diesen permanent deaktiviert mit "sudo swapoff -a" und der entsprechenden Zeile auskommentiert im /etc/fstab File.

Container Runtime

Das erste was installiert wird ist die Container Runtime. Viele der Tutorials benutzen hier Docker. Ich wollte Docker jedoch nicht benutzen aufgrund von diesem Post im Kubernetes Blog. Doch wie installiert man containerd? oder Cri-O? In der offizielen Dokumentation von Kubernetes findet man ein Quickstart Guide.

Zur Info: Die Container Runtime installiert man sowohl auf der Control Plane als auch auf den Workern. Man könnte zwar die einzelnen Komponenten der Control Plane auch als Service direkt auf dem OS laufen lassen, aber der schnellere Weg ist es diese als Pods laufen zu lassen und einfach das kubelet als Service zu installieren (mindestens das braucht es als non-container Version)

Soweit so gut, das erste was mich danach aber verwirrt hat ist die Diskussion rund um cgroups. Wie in obigem verlinkten Artikel aus der Dokumentation entgeht wird kubeadm sowie das kubelet per default cgroupfs einsetzen. Wenn man will kann man aber als cgroup manager auch systemd nutzen. Laut der Dokumentation ist dies die stabilere und besser ins OS verankerte Variante, deshalb wähle ich diese. Doch das ist nicht default, das heisst wir müssen containerd mitteilen das wir dies nutzen möchten.

Dazu muss im Config file von containerd (/etc/containerd/config.toml) folgender Eintrag gesetzt werden:

[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
  ...
  [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
    SystemdCgroup = true

Damit ist die Installation der Container Runtime abgeschlossen. Im nächsten Schritt installieren wir uns die Grundlegenden Tools die es braucht um den Cluster zu initalisieren.

Kubeadm,kubectl,kubelet

Wie im Titel steht wird dieser Cluster mittels kubeadm initialisiert. kubeadm hilft dabei die Core Komponenten des Cluster zu deployen damit dieser an sich funktionsfähig ist und den Standards entspricht. kubeadm deployed kein CNI, keine Monitoring Solution und auch keine sonstigen Addons die man für einen nutzbaren Cluster benötigt. Dies macht es zu einem guten Tool für Automatisierungsjobs in denen kubeadm als Grundlage für das Cluster Bootstraping gebraucht wird. Es ist aber wichtig zu wissen das es noch viele andere Wege gibt einen Cluster zu deployen und manche dieser Wege mehr installieren als nur das Minimum. Man könnte sagen kubeadm ist eine automatisiert Form von dem was man im Kubernetes the hard way installiert.

Wir installieren kubeadm aus dem offizielen Repository von Google. Dazu kommt noch das kubelet, der Service der als Agent zwischen der Container Runtime und den Cluster Komponenten dient. Kubectl wird während der Initialisierung des Cluster nicht gebraucht aber spätestens danach, deshalb installieren wir es auch mit.

Hier findet man die Installationsanleitung um das Repo zu konfigurieren.

Wir markieren die drei Tools damit APT diese später nicht automatisch updated. Dies ist wichtig da neue Versionen des Cluster nur mit gewissen Versionen von kubeadm geupgraded werden können. Es macht also Sinn das der Cluster und kubeadm auf der gleichen Version sind (das kubelet natürlich auch). Kubectl könnte theoretisch zwei Versionen voraus sein aber warum nicht auch gleich halten? Im Endeffekt braucht man kubectl so oder so meistens vom eigene Laptop aus.

kubeadm init

So weit so gut, kommen wir zum spannenden Teil, der Initialisierung des Cluster. Wir führen dazu kubeadm init auf der Control Plane aus und geben diverse Parameter mit. Grundsätzlich ist es möglich kubeadm init mit nur einem Argument zu starten, aber weil ich ein Fan von Customizing bin und wir aufgrund der bisherigen Konfiguration auch nicht mehr ganz default unterwegs sind, braucht es dennoch eine Argumente.

Man kann diese Argumente entweder als Argumente auf der Shell übergeben oder via YAML File. Da einige der benötigten Optionen nur via File gehen, nehmen wir diese Option.

Hier mein Konfigfile:

Ich habe mir mittels dem Command "kubeadm config print init-defaults" das DefaultConfig File ausgeben lassen und daran einige Änderungen vorgenommen. Die Dokumentation zum Configfile ist ein bisschen versteckt aber ich habe sie hier gefunden.

Die Änderungen im Detail:

Zeile 43: Den ganzen Block KubeletConfiguration habe ich aufgrund dieses Kapitels in der Dokumentation eingefügt. Da wir als cgroup manager systemd verwenden müssen wir dies nicht nur containerd sondern auch dem kubelet mitteilen, dies machen wir hier in der kubeadm config.

Zeile 14: Den Socket müssen wir anpassen da der Default auf Docker zeigt. Ich habe ein "sudo systemctl status containerd" ausgeführt um den richtigen Socket zu finden.

Zeile 35: Der DNS Name des Clusters ist standardmässig auf cluster.local. Ich wollte hier jedoch einen anderen Namen haben und habe deshalb den hier geändert. Dies ist aber nicht nötig.

Zeile 25: Den Namen des Clusters muss man auch nicht unbedingt ändern, standardmässig steht dort "kubernetes"

Zeile 36: Diese Zeile ist notwendig damit wir später ein CNI deployen können und dieses Weiss welches Subnet wir für Pod IPs verwenden wollen.

Zeile 39: Das offiziele Tutorial fürs Bootstrapen eines Clusters mit kubeadm empfiehlt dieses Feld auf einen DNS Namen zu setzen sofern man im Sinn hat später die Control Plane HA zu machen. Ich habe hier einen Generischen DNS Namen angegeben der momentan eifach auf die IP des Control Plane Server zeigt. Falls ich in Zukunft einmal ein HA Setup daraus mache, kann ich diesen Namen einfach auf einen LoadBalancer zeigen lassen. Sofern ich diese Option nicht angebe wird dies nicht möglich sein.

Sofern wir mit dem Configfile zufrieden sind können wir den Cluster initialisieren:

sudo kubeadm init --config /path/to/kubeadm-init-config.yaml

Kubeadm wird diverse Checks durchführen, Images pullen und schlussendlich die Komponenten deployen.

Der Output wenn alles erfolgreich war, ist unmissverständlich. Wenn nicht empfiehlt es sich mal auf der Troubleshooting Seite nachzuschauen.

Wie der Output auch schon sagt, können wir uns nun die kubeconfig für den Cluster Admin exportieren und in unser Home Verzeichnis legen, neue Control Plane oder Worker Nodes dem Cluster joinen und ein CNI deployen. Das CNI ist sehr wichtig, ohne dieses funktioniert der Cluster nicht richtig. Dies sieht man auch wenn wir uns mal die Kubeconfig parat legen und einen ersten kubectl Command machen:

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get nodes

Wie der Output des letzen Commands zeigt ist der Control Plane Server noch nicht ready. Die Details über CNI Plugins sind in der Dokumentation zu finden. Ich deploye im folgenden das CNI Calico.

Laut der Quickstart Dokumentation sind diese Commands ausreichend:

kubectl create -f https://docs.projectcalico.org/manifests/tigera-operator.yaml
kubectl create -f https://docs.projectcalico.org/manifests/custom-resources.yaml
watch kubectl get pods -n calico-system

Jedoch haben wir ja eine eigene Pod Netzwerk Range gesetzt bei der Initialisierung unseres Clusters. Deshalb lade ich mir die zweite YAML Resource zuerst als File herunter und editiere sie ein bisschen:

Danach kreiere ich diese Resource auch und schaue zu wie die Pods deployed werden.

Werfen wir nun nochmals einen Blick auf den Status der Control Plane sehen wir das er nun Ready ist.

Disclaimer: Ich habe Calico als CNI noch nie verwendet und habe deshalb keine Erfahrung damit. Ob die oben genannte Installationsmethode die beste ist weiss ich nicht. So wie es scheint gehört zu Calico auch noch das Calicoctl dazu welches installiert werden muss. Mehr dazu hier.

Join Worker Nodes

Nun da unser Control Plane funktionsfähig ist, müssen wir den Worker Node dem Cluster joinen. Dies wird ebenfalls über Kubeadm gemacht. Während der Initialisierung der Control Plane hat kubeadm uns den Join Command dazu quasi bereits geliefert:

kubeadm join kubeapi.green.lab:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:223432145798041750987uifosghjdfksgh43954328437983457

Dies muss auf dem Worker Node als Root (oder Sudo) ausgeführt werden. Der erfolgreiche Output ist auch hier selbsterklärend.

Final sieht nun mein Cluster so aus:

Wie weiter?

Was nun kommt ist nicht einfach zu beantworten. Auf einem Kubernetes Cluster kann alles mögliche gemacht werden. In der Doku werden einige Punkte genannt was nun noch gemacht werden kann um das ganze zu optimieren oder es zu gebrauchen. Was ich auf jeden Fall machen werde ist ein Taint auf der Control Plane setzten damit meine Applikationspods nur auf dem Worker Node ausgeführt werden.

Anmelden