Diese Anleitung beschreibt die skriptbasierte Installation eines Vanilla Kubernetes Clusters (Version 1.36) auf Ubuntu 24.04 VMs in einer Lab-Umgebung. Die einzelnen Schritte im Skript sind zur besseren Verständlichkeit mit entsprechenden Kommentaren versehen. Die Anleitung passt nicht zu einem produktiven HA-Design, bei dem drei Control Nodes Pflicht sind.
Es wird vorausgesetzt, dass die VMs bereits installiert sind und über feste IP-Adressen (oder eine DHCP-Reservierung) verfügen. Die IP-Adressen müssen ggf. an die jeweilige Umgebung angepasst werden.
Infrastruktur
| Name | IP-Adresse | Rolle |
| akr-c01 | 10.10.191.205 | Control-Plane |
| akr-w01 | 10.10.191.206 | Worker |
| akr-w02 | 10.10.191.168 | Worker |
| akr-w03 | 10.10.191.207 | Worker |
Cluster-Netzbereiche (CIDR):
| Netztyp | CIDR | Bedeutung |
| Node-/VM-Netz | 10.10.191.0/24 | Ubuntu-VMs |
| Pod-Netz | 172.28.0.0/16 | Interne Ips der Pods |
| Service-Netz | 172.29.0.0/16 | Virtuelle ClusterIPs der Services |
Die „klassischen“ Pods (10.244.0.0/16) und die Service-CIDR (10.96.0.0/12) werden nicht verwendet, um eine mögliche Überlappung zu vermeiden.
Installationsschritte
Schritt 1: Skript
Auf allen vier VMs wird dasselbe Skript verwendet. Es kann direkt von dieser Seite per Copy-Paste in ein neues Shell-Skript eingefügt werden. Die Datei muss ausführbar gemacht werden.
chmod +x bootstrap-k8s-cloudlab.sh
Schritt 2: Skript ausführen
Starten Sie den Installationsprozess auf dem Control Node.
sudo NODE_NAME=akr-c01 NODE_IP=10.10.191.205 ./bootstrap-k8s-cloudlab.sh control-plane
Das Skript bereitet das Betriebssystem vor, installiert containerd, kubeadm/kubelet/kubectl, initialisiert den Cluster und installiert Cilium als CNI. Zum Abschluss wird ein kubeadm-join Befehl ausgegeben, der im nächsten Schritt auf den Worker Nodes benötigt wird.
Schritt 3: Worker beitreten lassen
Sowohl die IP als auch der Node-Name müssen auf allen drei Workern (akr-w01, akr-w02 und akr-w03) entsprechend angepasst werden.
sudo NODE_NAME=akr-w01 NODE_IP=10.10.191.206 ./bootstrap-k8s-cloudlab.sh worker "kubeadm join 10.10.191.205:6443 --token ... --discovery-token-ca-cert-hash sha256:..."
Der ausgegebene Befehl kubeadm join enthält einen schutzwürdigen Token. Dieser sollte nicht öffentlich geteilt oder in einem öffentlichen Repository gespeichert werden, da damit weitere Nodes dem Cluster beitreten können.
Skript:
#!/usr/bin/env bash # # bootstrap-k8s-cloudlab.sh # # Vanilla Kubernetes auf Ubuntu Server 24.04 mit kubeadm + containerd + Cilium. # Dieses Skript ist kommentiert, damit man jeden Schritt nachvollziehen kann, # bevor es mit root-Rechten auf einer VM ausgeführt wird. # # Zielumgebung: 1 Control-Plane + 3 Worker # # Nodes (Beispielwerte, an deine Umgebung anpassen): # akr-c01 10.10.191.205 Control-Plane # akr-w01 10.10.191.206 Worker # akr-w02 10.10.191.168 Worker # akr-w03 10.10.191.207 Worker # ### Nutzung: # Control-Plane: # sudo NODE_NAME=akr-c01 NODE_IP=10.10.191.205 ./bootstrap-k8s-cloudlab.sh control-plane # # Worker (Join-Befehl kommt aus der Ausgabe des Control-Plane-Laufs): # sudo NODE_NAME=akr-w01 NODE_IP=10.10.191.206 ./bootstrap-k8s-cloudlab.sh worker "kubeadm join 10.10.191.205:6443 --token ... --discovery-token-ca-cert-hash sha256:..." # # Optional per Umgebungsvariable überschreibbar (Defaults siehe unten): # K8S_MINOR, POD_CIDR, SERVICE_CIDR, CONTROL_PLANE_IP, # CONTROL_PLANE_ENDPOINT, CILIUM_VERSION, CRI_SOCKET # # Alle vier VMs benötigen feste IP-Adressen oder eine # DHCP-Reservierung. # set -Eeuo pipefail macht das Skript robuster als ein einfaches "set -e": # -E Fehler-Traps werden auch in Funktionen weitervererbt # -e das Skript bricht sofort ab, sobald ein Befehl fehlschlägt # -u die Verwendung nicht gesetzter Variablen ist ein Fehler # -o pipefail ein Fehler mitten in einer Pipe wird nicht verschluckt # Bei einer Kubernetes-Installation ist das wichtig, weil ein halb # erfolgreiches Setup später nur schwer zu debuggen ist. set -Eeuo pipefail # Erstes Argument ist die Rolle (control-plane oder worker). ROLE="${1:-}" # Danach wird das erste Argument entfernt ("shift"). Alles, was übrig # bleibt, ist - falls vorhanden - der komplette kubeadm-join-Befehl. # Das ist nur für Worker relevant. [[ $# -gt 0 ]] && shift || true JOIN_CMD_RAW="$*" # Standardwerte. Wenn du beim Aufruf keine anderen Werte per # Umgebungsvariable übergibst, verwendet das Skript diese hier. K8S_MINOR="${K8S_MINOR:-1.36}" POD_CIDR="${POD_CIDR:-172.28.0.0/16}" SERVICE_CIDR="${SERVICE_CIDR:-172.29.0.0/16}" CONTROL_PLANE_IP="${CONTROL_PLANE_IP:-10.10.191.205}" # CONTROL_PLANE_ENDPOINT ist in diesem einfachen Lab direkt die IP von # akr-c01. CONTROL_PLANE_ENDPOINT="${CONTROL_PLANE_ENDPOINT:-${CONTROL_PLANE_IP}}" CILIUM_VERSION="${CILIUM_VERSION:-1.19.5}" CRI_SOCKET="${CRI_SOCKET:-unix:///run/containerd/containerd.sock}" # Wenn NODE_NAME nicht gesetzt ist, nimmt das Skript den kurzen Hostnamen # der VM. NODE_IP bleibt zunächst leer und wird weiter unten automatisch # ermittelt, falls du sie nicht explizit per Umgebungsvariable setzt. NODE_NAME="${NODE_NAME:-$(hostname -s)}" NODE_IP="${NODE_IP:-}" # log() erzeugt gut lesbare Statusausgaben zwischen den Installationsschritten. log() { echo -e "\n==> $*" } # fail() gibt eine Fehlermeldung auf STDERR aus und beendet das Skript # sofort mit einem Fehlercode. fail() { echo "FEHLER: $*" >&2 exit 1 } # Die Installation verändert Systemdateien und installiert Pakete, # deshalb muss das Skript als root laufen (via sudo). require_root() { [[ "${EUID}" -eq 0 ]] || fail "Bitte mit root-Rechten ausführen, z.B. sudo $0 ${ROLE}" } # Zeigt die korrekte Verwendung an - wird aufgerufen, wenn kein # gültiger Modus angegeben wurde oder beim Worker der Join-Befehl fehlt. usage() { cat <<USAGE Usage: Control-Plane: sudo NODE_NAME=akr-c01 NODE_IP=10.10.191.205 $0 control-plane Worker: sudo NODE_NAME=akr-w01 NODE_IP=10.10.191.206 $0 worker "kubeadm join 10.10.191.205:6443 --token ... --discovery-token-ca-cert-hash sha256:..." Rollen: control-plane Installiert Pakete, initialisiert den Cluster, installiert Cilium. worker Installiert Pakete und führt den kubeadm-join-Befehl aus. USAGE } # Ermittelt die Node-IP. Wenn NODE_IP bereits per Umgebungsvariable # gesetzt wurde, passiert nichts weiter. Sonst versucht das Skript, # anhand des Node-Namens die passende Lab-IP zuzuordnen (an deine # Umgebung anpassen). Ist der Name unbekannt, wird die erste lokale IP # aus dem VM-Netz verwendet. Findet sich gar keine, bricht das Skript ab. resolve_node_ip() { if [[ -n "${NODE_IP}" ]]; then return 0 fi case "${NODE_NAME}" in akr-c01) NODE_IP="10.10.191.205" ;; akr-w01) NODE_IP="10.10.191.206" ;; akr-w02) NODE_IP="10.10.191.168" ;; akr-w03) NODE_IP="10.10.191.207" ;; *) # Fallback: erste IPv4-Adresse aus dem VM-Netz verwenden. # Das Muster 10\.10\.191\. an dein eigenes Node-Netz anpassen. NODE_IP="$(hostname -I | tr ' ' '\n' | awk '/^10\.10\.191\./ {print; exit}')" ;; esac [[ -n "${NODE_IP}" ]] || fail "NODE_IP konnte nicht automatisch ermittelt werden. Bitte NODE_IP=... setzen." } # Prüft, dass nur "control-plane" oder "worker" als Rolle erlaubt sind, # und dass beim Worker ein Join-Befehl mitgegeben wurde. validate_role() { case "${ROLE}" in control-plane|worker) ;; *) usage; exit 1 ;; esac if [[ "${ROLE}" == "worker" && -z "${JOIN_CMD_RAW}" ]]; then usage fail "Für 'worker' muss der kubeadm-join-Befehl als Argument übergeben werden." fi } # Schreibt Namen und IP-Adressen aller Nodes in /etc/hosts, damit sich # die VMs gegenseitig per Hostname erreichen können (z.B. ping akr-w01). # Ein eventuell vorhandener alter Block wird vorher entfernt, damit # keine Duplikate entstehen. write_hosts_entries() { log "Hostnamen in /etc/hosts ergänzen" local marker="# Kubernetes Lab nodes" sed -i '/# Kubernetes Lab nodes/,+4d' /etc/hosts cat >> /etc/hosts <<EOF_HOSTS ${marker} 10.10.191.205 akr-c01 10.10.191.206 akr-w01 10.10.191.168 akr-w02 10.10.191.207 akr-w03 EOF_HOSTS } # Bereitet das Betriebssystem Kubernetes-tauglich vor: # - Swap wird sofort deaktiviert und in /etc/fstab auskommentiert, # damit er nach einem Reboot nicht wieder aktiv wird (Kubernetes # erwartet, dass kein Swap verwendet wird). # - Die Kernel-Module overlay und br_netfilter werden dauerhaft geladen. # - sysctl-Parameter sorgen dafür, dass Bridge-Traffic von iptables # gesehen wird und IPv4-Forwarding aktiv ist - beides Voraussetzung # für funktionierendes Pod-Networking. prepare_os() { log "System vorbereiten: Swap deaktivieren, Kernel-Module laden, sysctl setzen" swapoff -a || true sed -ri '/\sswap\s/s/^/#/' /etc/fstab cat >/etc/modules-load.d/k8s.conf <<'EOF_MODULES' overlay br_netfilter EOF_MODULES modprobe overlay modprobe br_netfilter cat >/etc/sysctl.d/k8s.conf <<'EOF_SYSCTL' net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF_SYSCTL sysctl --system >/dev/null } # Installiert Hilfspakete für HTTPS-Repositories, Zertifikate, Downloads # und GPG-Schlüsselverarbeitung. DEBIAN_FRONTEND=noninteractive # verhindert, dass apt während der Installation nach Eingaben fragt. install_base_packages() { log "Basis-Pakete installieren" export DEBIAN_FRONTEND=noninteractive apt-get update -y apt-get install -y apt-transport-https ca-certificates curl gpg gnupg lsb-release jq } # containerd wird hier als die Container Runtime genutzt # SystemdCgroup = true ist wichtig, damit kubelet und containerd # denselben cgroup-Mechanismus verwenden kann, ohne das kommt es zu # schwer diagnostizierbaren Fehlern beim Start von Pods. install_containerd() { log "containerd installieren und für systemd-cgroups konfigurieren" export DEBIAN_FRONTEND=noninteractive apt-get install -y containerd mkdir -p /etc/containerd containerd config default >/etc/containerd/config.toml sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml systemctl daemon-reload systemctl enable --now containerd # Erleichtert späteres Debugging von Containern mit crictl. cat >/etc/crictl.yaml <<EOF_CRICTL runtime-endpoint: ${CRI_SOCKET} image-endpoint: ${CRI_SOCKET} timeout: 10 debug: false EOF_CRICTL } # Richtet das offizielle Kubernetes-Apt-Repository für die gewünschte # Minor-Version ein und installiert kubelet, kubeadm und kubectl. # apt-mark hold verhindert, dass diese Pakete bei einem normalen # "apt upgrade" ungeplant auf eine neue Minor-Version springen. # Die explizite Node-IP in /etc/default/kubelet stellt sicher, dass # kubelet nicht versehentlich eine falsche Interface-IP verwendet, # falls die VM mehrere Netzwerkkarten hat. install_kubernetes_packages() { log "Kubernetes ${K8S_MINOR} Repository einrichten und kubelet/kubeadm/kubectl installieren" mkdir -p -m 755 /etc/apt/keyrings curl -fsSL "https://pkgs.k8s.io/core:/stable:/v${K8S_MINOR}/deb/Release.key" \ | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${K8S_MINOR}/deb/ /" \ >/etc/apt/sources.list.d/kubernetes.list apt-get update -y apt-get install -y kubelet kubeadm kubectl apt-mark hold kubelet kubeadm kubectl cat >/etc/default/kubelet <<EOF_KUBELET KUBELET_EXTRA_ARGS=--node-ip=${NODE_IP} EOF_KUBELET systemctl enable kubelet } # Kopiert die Admin-kubeconfig nach /root/.kube/config, damit root # direkt kubectl verwenden kann. Falls das Skript per sudo von einem # normalen Benutzer gestartet wurde, wird die kubeconfig zusätzlich # in dessen Home-Verzeichnis abgelegt. copy_kubeconfig() { log "kubeconfig für root und, falls vorhanden, für den sudo-Benutzer kopieren" install -d -m 700 /root/.kube cp -f /etc/kubernetes/admin.conf /root/.kube/config chown root:root /root/.kube/config chmod 600 /root/.kube/config if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]] && id "${SUDO_USER}" >/dev/null 2>&1; then local user_home user_home="$(getent passwd "${SUDO_USER}" | cut -d: -f6)" if [[ -n "${user_home}" && -d "${user_home}" ]]; then install -d -m 700 -o "${SUDO_USER}" -g "${SUDO_USER}" "${user_home}/.kube" cp -f /etc/kubernetes/admin.conf "${user_home}/.kube/config" chown "${SUDO_USER}:${SUDO_USER}" "${user_home}/.kube/config" chmod 600 "${user_home}/.kube/config" fi fi } # Lädt die zur CPU-Architektur passende Cilium-CLI herunter, prüft die # SHA256-Prüfsumme und installiert das Binary nach /usr/local/bin. # Danach steht der Befehl "cilium" zur Verfügung. install_cilium_cli() { log "Cilium CLI installieren" local arch cli_version tmpdir case "$(uname -m)" in x86_64|amd64) arch="amd64" ;; aarch64|arm64) arch="arm64" ;; *) fail "Nicht unterstützte Architektur für Cilium CLI: $(uname -m)" ;; esac cli_version="$(curl -fsSL https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)" tmpdir="$(mktemp -d)" pushd "${tmpdir}" >/dev/null curl -fsSLO "https://github.com/cilium/cilium-cli/releases/download/${cli_version}/cilium-linux-${arch}.tar.gz" curl -fsSLO "https://github.com/cilium/cilium-cli/releases/download/${cli_version}/cilium-linux-${arch}.tar.gz.sha256sum" sha256sum --check "cilium-linux-${arch}.tar.gz.sha256sum" tar xzf "cilium-linux-${arch}.tar.gz" -C /usr/local/bin popd >/dev/null rm -rf "${tmpdir}" } # Der Kern der Control-Plane-Installation: # - kubeadm init startet den Cluster. Die Node-IP wird als # API-Server-Adresse bekanntgegeben, Pod- und Service-CIDR werden # festgelegt, der Node-Name gesetzt und containerd als Runtime # angegeben. # - Danach wird die kubeconfig kopiert. # - Cilium wird als CNI installiert. ipam.mode=kubernetes ist wichtig, # damit Cilium den von kubeadm gesetzten Pod-CIDR verwendet und # nicht seinen eigenen Default-Adresspool (10.0.0.0/8) - das würde # sich sonst mit dem VM-Netz überschneiden. # - Zum Schluss wird der Join-Befehl für die Worker erzeugt und # zusätzlich unter /root/kubeadm-join.sh gespeichert. init_control_plane() { log "Control-Plane initialisieren: ${NODE_NAME} / ${NODE_IP}" kubeadm init \ --apiserver-advertise-address="${NODE_IP}" \ --control-plane-endpoint="${CONTROL_PLANE_ENDPOINT}:6443" \ --pod-network-cidr="${POD_CIDR}" \ --service-cidr="${SERVICE_CIDR}" \ --node-name="${NODE_NAME}" \ --cri-socket="${CRI_SOCKET}" copy_kubeconfig log "CNI installieren: Cilium ${CILIUM_VERSION}" install_cilium_cli KUBECONFIG=/etc/kubernetes/admin.conf cilium install --version "${CILIUM_VERSION}" \ --set ipam.mode=kubernetes \ --wait KUBECONFIG=/etc/kubernetes/admin.conf cilium status --wait log "Join-Befehl für Worker erzeugen" kubeadm token create --print-join-command >/root/kubeadm-join.sh chmod 600 /root/kubeadm-join.sh cat <<EOF_DONE ######################################################## Control-Plane fertig. Join-Befehl für Worker: $(cat /root/kubeadm-join.sh) Beispiel Worker-Aufruf: sudo NODE_NAME=akr-w01 NODE_IP=10.10.191.206 ./bootstrap-k8s-cloudlab.sh worker "$(cat /root/kubeadm-join.sh)" Status prüfen: kubectl get nodes -o wide kubectl get pods -A ######################################################## EOF_DONE } # Wird nur auf Workern ausgeführt. Der übergebene Join-Befehl wird # bereinigt (Zeilenumbrüche und Backslashes aus der mehrzeiligen # kubeadm-Ausgabe entfernt) und in einzelne Argumente zerlegt. Das ist # sicherer als ein simples "eval", weil nicht beliebiger Shell-Code # ausgeführt wird, sondern geprüft wird, dass es sich tatsächlich um # einen "kubeadm join"-Befehl handelt. join_worker() { log "Worker vorbereiten: ${NODE_NAME} / ${NODE_IP}" local cleaned_join cleaned_join="$(printf '%s' "${JOIN_CMD_RAW}" | tr '\n' ' ' | sed 's/\\//g' | xargs)" read -r -a join_args <<< "${cleaned_join}" if [[ "${join_args[0]:-}" != "kubeadm" || "${join_args[1]:-}" != "join" ]]; then fail "Der Join-Befehl muss mit 'kubeadm join' beginnen." fi log "Node dem Cluster hinzufügen" "${join_args[@]}" --node-name "${NODE_NAME}" --cri-socket "${CRI_SOCKET}" echo "Worker ${NODE_NAME} wurde dem Cluster hinzugefügt." } # Ablaufsteuerung: erst werden Rechte, Rolle und IP geprüft. Danach # werden Hosts-Datei, Betriebssystem, Basispakete, containerd und # Kubernetes-Pakete vorbereitet - das ist auf Control-Plane und Worker # identisch. Erst am Ende entscheidet die Rolle, ob der Cluster # initialisiert oder ein Join durchgeführt wird. main() { require_root validate_role resolve_node_ip log "Rolle=${ROLE}, Node=${NODE_NAME}, Node-IP=${NODE_IP}, Kubernetes=${K8S_MINOR}" write_hosts_entries prepare_os install_base_packages install_containerd install_kubernetes_packages if [[ "${ROLE}" == "control-plane" ]]; then init_control_plane else join_worker fi } main "$@"
Schritt 4: Cluster prüfen
Mit folgende Befehlen lässt sich der Installierte Cluster überprüfen.
Auf der Control-Plane:
kubectl get nodes -o wide kubectl get pods -A cilium status cilium connectivity test # Optional
Mithilfe des cilium connectivity test kann zusätzlich überprüft werden, ob die Pod-zu-Pod-, Service- und Policy-relevanten Kommunikationspfade funktionieren.
Erwartbare Ergebnisse:
kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME akr-c01 Ready control-plane 39m v1.36.2 10.10.191.205 <none> Ubuntu 24.04.4 LTS 6.8.0-124-generic (amd64) containerd://2.2.1 akr-w01 Ready <none> 31m v1.36.2 10.10.191.206 <none> Ubuntu 24.04.4 LTS 6.8.0-124-generic (amd64) containerd://2.2.1 akr-w02 Ready <none> 22m v1.36.2 10.10.191.168 <none> Ubuntu 24.04.4 LTS 6.8.0-124-generic (amd64) containerd://2.2.1 akr-w03 Ready <none> 21m v1.36.2 10.10.191.207 <none> Ubuntu 24.04.4 LTS 6.8.0-124-generic (amd64) containerd://2.2.1
kubectl get pods -A
NAMESPACE NAME READY STATUS RESTARTS AGE kube-system cilium-dj6sz 1/1 Running 0 38m kube-system cilium-envoy-jcl4n 1/1 Running 0 22m kube-system cilium-envoy-lc8kg 1/1 Running 0 31m kube-system cilium-envoy-mqw6l 1/1 Running 0 21m kube-system cilium-envoy-z88xg 1/1 Running 0 38m kube-system cilium-gxjnv 1/1 Running 0 22m kube-system cilium-h8dkr 1/1 Running 0 21m kube-system cilium-operator-7bf757c544-5mmvl 1/1 Running 0 38m kube-system cilium-szt6k 1/1 Running 0 31m kube-system coredns-589f44dc88-7j7xr 1/1 Running 0 39m kube-system coredns-589f44dc88-7zpkb 1/1 Running 0 39m kube-system etcd-akr-c01 1/1 Running 0 39m kube-system kube-apiserver-akr-c01 1/1 Running 0 39m kube-system kube-controller-manager-akr-c01 1/1 Running 0 39m kube-system kube-proxy-dk4d5 1/1 Running 0 22m kube-system kube-proxy-n6x2g 1/1 Running 0 31m kube-system kube-proxy-nfvhr 1/1 Running 0 39m kube-system kube-proxy-nzrc8 1/1 Running 0 21m kube-system kube-scheduler-akr-c01 1/1 Running 0 39m
cilium status
Cilium: OK Operator: OK Envoy DaemonSet: OK Hubble Relay: disabled ClusterMesh: disabled
DaemonSet cilium Desired: 4, Ready: 4/4, Available: 4/4 DaemonSet cilium-envoy Desired: 4, Ready: 4/4, Available: 4/4 Deployment cilium-operator Desired: 1, Ready: 1/1, Available: 1/1 Containers: cilium Running: 4 cilium-envoy Running: 4 cilium-operator Running: 1 clustermesh-apiserver hubble-relay Cluster Pods: 2/2 managed by Cilium Helm chart version: 1.19.5 Image versions cilium quay.io/cilium/cilium:v1.19.5@sha256:20fbbc14ac20b55a292c0dcda5571fc1de30a7dbc68c29db3e709390ab0732: 4 cilium-envoy quay.io/cilium/cilium-envoy:v1.36.8-1781157951-a7f42a339078159911b5b9107881b35ecc4e752@sha256:326f872e19ce8aa45170 cilium-operator quay.io/cilium/operator-generic:v1.19.5@sha256:be848a36577e07d0c5a895eda7aec928ddc52a5alfa2f432fd7a286609eldb4: 1