Installation eines minimal Kubernetes Clusters

Containerize all the things! Seit vielen Jahren ist die Container-Technologie aus der IT-Welt nicht mehr wegzudenken. Mittlerweile gibt es viele Lösungen und Varianten, einige sogar schon wieder veraltet. Über die Container-Engine hinaus gibt es auch Komplettlösungen, die eine Verwaltung bzw. Orchestrierung bieten - wie zum Beispiel Kubernetes.

Kubernetes ist vermutlich jedem bekannt, schließlich gibt es Kubernetes, oder abgekürzt K8s, auch bereits seit 2014/2015. Für mich ist daher umso erstaunlicher, dass ich in all den Jahren nie wirklich mit Containern in Berührung gekommen bin. Aber das IT-Feld ist breit und ich behaupte, dass man sich komplexere Container-Plattformen nicht mal eben nebenbei aneignet (abgesehen von vielleicht mal ein Docker Image pullen und starten). Zusätzlich sollte ein regelmäßiger Kontakt vorhanden sein bzw. damit arbeiten, sonst ist das Wissen wieder sehr sehr schnell verflogen.

Als IT-ler lernt man nie aus und das Thema hat mich immer getriggert, daher war es vor einiger Zeit endlich soweit und ich habe mich privat etwas mehr mit dem Thema auseinandergesetzt. Dieser Artikel ist Start einer kleineren Serie von zusammenhängenden Themen. Es beginnt mit der Installation eines minimalen K8s Clusters auf Proxmox, geht weiter in das Deployment von AWX und endet mit der Erstellung von neuen Debian VMs per Ansible/AWX auf Proxmox.

Ich beschreibe hier eine absolute manuelle minimale Installation, ohne Lösungen wie Kubespray, welche in der Lage sind K8s automatisiert zu installieren.

Voraussetzungen

Wie schon erwähnt, werde ich hier die Installation einer minimalen Variante beschreiben. Das bedeutet, es wird sich um einen 3-Node Cluster handeln: Einen Master (Control-Plane) und zwei Worker Nodes. Als Basis verwendet ich ganz normale Debian 12 VMs auf meiner Proxmox Umgebung.

VM Role CPU RAM HDD
kube01 Master 1 Socket / 2 Cores / Type 'host' 8 GB 30 GB
kube02 Worker 1 Socket / 2 Cores / Type 'host' 16 GB 30 GB
kube03 Worker 1 Socket / 2 Cores / Type 'host' 16 GB 30 GB

Warum hier der CPU Typ 'host' gewählt wurde dürfte auf der Hand liegen. Die Performance steigt erheblich, wenn die Workloads nicht in einer virtualisierten CPU laufen, sondern direkt an die physikalische CPU durchgereicht werden. Der zugewiesene Plattenplatz ist für den Anfang komplett ausreichend. Da es sich um VMs mit logical volumes handelt, können diese jederzeit vergrößert werden.

VM Basis-Konfigurationen

Alle drei Debian 12 VMs sollten auf einem aktuellen Stand und gegenseitig per DNS erreichbar sein. UFW als Firewall wird vorausgesetzt, zudem wird für externe Repositories gpg benötigt. Der qemu-guest-agent sollte auf dem System natürlich ebenfalls laufen.

Vorbereitungen

SWAP deaktivieren

Aus weiteren Perfomancegründen wird empfohlen SWAP auf allen K8s Nodes zu deaktiveren:

$ swapoff -a  
$ sed -i '/ swap / s/^\\(.\*\\)$/#\\1/g' /etc/fstab

UFW Firewall konfigurieren

Master-Node:

$ ufw allow 6443/tcp  
$ ufw allow 2379/tcp  
$ ufw allow 2380/tcp  
$ ufw allow 10250/tcp  
$ ufw allow 10251/tcp  
$ ufw allow 10252/tcp  
$ ufw allow 10255/tcp  
$ ufw reload

Worker-Nodes:

$ ufw allow 10250/tcp  
$ ufw allow 30000:32767/tcp  
$ ufw reload

Container-Engine 'containerd'

Als Container-Engine ist meine Wahl auf containerd gefallen, welches eine reine Engine darstellt und sich darauf fokussiert, eine (Low-Level) Container-Runtime bereitzustellen. Docker auf der anderen Seite als Alternative bietet ein enormes Tool-Set (stellt eine eigene High-Level Container Plattform dar), welches bei einer Kubernetes Installation nicht benötigt wird, da nur eine Engine, bzw. Runtime laufen muss.

Die Installation und Konfiguration von containerd erfolgt auf allen Nodes.

Kernel-Parameter setzen

Folgende Kernel-Parameter auf allen Nodes setzen:

$ cat <<EOF | sudo tee /etc/modules-load.d/containerd.conf  
overlay  
br_netfilter  
EOF
$ modprobe overlay  
$ modprobe br_netfilter
$ cat <<EOF | sudo tee /etc/sysctl.d/99-kubernetes-k8s.conf  
net.bridge.bridge-nf-call-iptables = 1  
net.ipv4.ip_forward = 1  
net.bridge.bridge-nf-call-ip6tables = 1  
EOF
$ sysctl --system

Installation von containerd

Die Installation erfolgt über das OS-seitige Paketmanagement und den regulären Repositories:

$ apt -y install containerd

Konfiguration von containerd

Damit containerd mit Kubernetes zusammenarbeitet sind folgende Konfigurationen notwendig:

$ containerd config default | sudo tee /etc/containerd/config.toml >/dev/null 2>&1

Anpassen der config.toml:

nano /etc/containerd/config.toml

Im Bereich [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]

SystemdCgroup = false

ändern in

SystemdCgroup = true

Achtung: Es gibt ebenfalls einen Parameter

systemd_cgroup = false

unter [plugins."io.containerd.grpc.v1.cri"].

Dieser darf nicht geändert werden, sondern muss auf 'false' stehenbleiben.

containerd aktivieren und starten:

$ systemctl enable containerd
$ systemctl restart containerd 

Installation von Kubernetes

Die Installation erfolgt auf allen Nodes. Im folgenden wird die Version 1.31.6 installiert. Natürlich kann dies auf den aktuellen Stand angepasst werden (Achtung: API Änderungen beachten). 

Hinzufügen des Kubernetes-Repositories:

$ echo "deb \[signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg\] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list

$ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

Installation der Tools:

$ apt update
$ apt -y install kubelet kubeadm kubectl
$ apt-mark hold kubelet kubeadm kubectl

Installation des Master-Nodes (Control-Plane)

kubelet bietet nicht mehr die Installation per Kommandozeile an. Diese sind deprecated. Stattdessen erfolgt die Installation per kubeadm und einer Definitionsdatei (Name frei wählbar).

$ nano kubelet.yml
apiVersion: kubeadm.k8s.io/v1beta3  
kind: InitConfiguration

apiVersion: kubeadm.k8s.io/v1beta3

kind: ClusterConfiguration  
kubernetesVersion: "1.31.6" 
controlPlaneEndpoint: "kube01"

apiVersion: kubelet.config.k8s.io/v1beta1  
kind: KubeletConfiguration

Version und Endpoint entsprechend bei Bedarf anpassen.

Deploy der Control-Plane auf den Master-Node:

$ kubeadm init --config kubelet.yml

Sofern alles funktioniert hat, sollte die Ausgabe folgendermaßen aussehen:

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

mkdir -p $HOME/.kube

sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.

Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:

https://kubernetes.io/docs/concepts/cluster-administration/addons/

You can now join any number of control-plane nodes by copying certificate authorities

and service account keys on each node and then running the following as root:

kubeadm join kube01:6443 --token abcd --discovery-token-ca-cert-hash sha256:1234 --control-plane

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join kube01:6443 --token abcd --discovery-token-ca-cert-hash sha256:1234

Admin-Konfiguration ins root-Home auf dem Master-Node kopieren:

$ mkdir -p $HOME/.kube

$ cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

$ chown $(id -u):$(id -g) $HOME/.kube/config

Der Status des Clusters und er Nodes kann folgendermaßen abgefragt werden:

$ kubectl get nodes  
$ kubectl cluster-info

Dort sollte jetzt nur die Control-Plane bzw. der Master-Node zu sehen sein.

Deploy der Worker Nodes

Auf allen Worker-Nodes ausführen (Token und Hash wurden nach dem Deploy der Control-Plane aufgeführt):

kubeadm join kube01:6443 --token abcd --discovery-token-ca-cert-hash sha256:1234

Prüfen des Status:

$ kubectl get nodes  
$ kubectl cluster-info

Dort sollten jetzt auch die beiden neuen Worker-Node zu sehen sein.

Hinweis: Hier steht nach der Installatio noch 'NotReady'. Damit die Nodes untereinander funktionieren, muss noch ein Pod-Network über den Cluster gespannt werden.

Installation des Network-Addon 'calico'

UFW auf allen Nodes konfigurieren:

$ ufw allow 179/tcp  
$ ufw allow 4789/udp  
$ ufw allow 51820/udp  
$ ufw allow 51821/udp  
$ ufw reload

Calico deployen (direkt aus dem Github-Repo):

kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.2/manifests/calico.yaml

Die Version kann/sollte man anpassen, das Repository kann natürlich ebenfalls vorher geklont/heruntergeladen werden.

Prüfen der calico-Pods:

$ kubectl get pods -n kube-system

Diese sollten auf "Running" stehen.

Ab jetzt können Deployments/Pods auf dem Cluster ausgerollt werden.

Einfaches Nginx Webserver Deployment

Ein simples Nginx Deployment könnte so aussehen:

nano sample-webpage.yml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
  name: sample-webpage-deployment
  labels:
    app: sample-webpage
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-webpage
  template:
    metadata:
      labels:
        app: sample-webpage
    spec:
      containers:
      - name: nginx-container
        image: nginx:latest
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  namespace: default
  name: sample-webpage-service
spec:
  selector:
    app: sample-webpage
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      nodePort: 30080
  type: NodePort

Definiert wird das Deployment mit einem Replica und dem Nginx Container. Zusätzlich wird ein Service erstellt, über den das Deployment erreichbar ist. In diesem Fall wird der Typ 'NodePort' verwendet, da noch keine Loadbalancer o.Ä. auf dem Cluster laufen. Deployment und Service können natürlich auch in separate Dateien ausgelagert werden.

Ausrollen des Deployments und des Services:

kubectl apply -f sample-webpage.yml

Prüfen, ob der Pod gestartet wurde:

kubectl get pods -o wide

Hinweis: -o wide zeigt, auf welchem Worker-Node der Pod läuft.

Prüfen, ob der Service läuft und mit dem angegebenen Port:

kubectl get services

Der Pod und Service laufen und sind unter Port 80 bzw. 30080 erreichbar. Da wir wissen, dass der Pod auf Node 3 (kube03) läuft, kann ein Testaufruf aus einem Webbrowser erfolgen:

Hier natürlich die IP des jeweiligen Worker-Node anpassen.

Fazit

Die Installation eines einfachen 3-Node Kubernetes Clusters ist simpler als gedacht. Zugegeben in dieser einfachsten aller Formen (außer K3s und minikube) läuft darauf auch nicht viel, aber ich habe damals schon zu Beginn mit deutlich mehr Hürden gerechnet.

Im nächsten Artikel beschreibe ich, wie Ingress und AWX installiert wird.

Fragen, Kommentare und Anregungen wie immer auf Mastodon.


Datum Änderung
11.07.2025 Veröffentlichung