
🌐 Auch auf: English · Français · Español
Deep Dive aus der Reihe rund um die Ansible-Tour. Niveau: fortgeschrittene Linux-Administration. Die Tour erklärt die Rollen; die Deep Dives erklären die Technik darunter – wie sie funktioniert, warum sie hier ist, was genau dieses Setup damit macht, wie man sie im Alltag betreibt und was es sonst noch gegeben hätte.
K3s ist zertifiziertes Kubernetes in einem einzigen Binary von etwa 70 MB. API-Server, Scheduler, Controller-Manager, Kubelet, kube-proxy, containerd, Flannel, CoreDNS und ein Network-Policy-Controller starten alle aus einem Prozess und einer systemd-Unit. Das macht es zur naheliegenden Wahl für einen Cluster aus genau einem Knoten – und es verbirgt einiges. Dieser Beitrag erklärt, was in diesem Binary tatsächlich läuft, was Teil 2 der Tour konfiguriert, und sechs Dinge, die K3s nicht von selbst tat und die erst im Betrieb auffielen.
🧭 Was im Binary steckt
Ein Supervisor, viele Komponenten
k3s server ist ein Supervisor. Er startet die Control-Plane-Komponenten von Kubernetes als Goroutinen in seinem eigenen Prozess – API-Server, Scheduler, Controller-Manager – und daneben die Knoten-Komponenten: den Kubelet, kube-proxy, das Flannel-CNI und einen eingebauten Network-Policy-Controller (Code aus kube-router). containerd läuft als eigener Kindprozess mit eigenem Socket unter /run/k3s/containerd/. Alles teilt sich einen Log-Strom im Journal (journalctl -u k3s) und eine Versionsnummer.
Der Datenspeicher: SQLite statt etcd
Upstream-Kubernetes speichert seinen Zustand in etcd. Ein K3s mit einem Knoten nutzt SQLite über kine, eine Zwischenschicht, die die etcd-API in SQL übersetzt. Der gesamte Cluster-Zustand – jedes Deployment, Secret, jede ConfigMap, jedes Event – liegt in /var/lib/rancher/k3s/server/db/state.db: 24 MB auf dem Blog, 23 MB auf einem Nextcloud-Server, dazu ein Write-Ahead-Log von 20 MB. Mit mehr als einem Server-Knoten wechselt K3s auf eingebettetes etcd.
containerd, das CRI und der Pause-Container
Der Kubelet spricht über das Container Runtime Interface mit containerd. Für jeden Pod startet containerd zuerst einen winzigen Pause-Container, der den Netzwerk-Namespace des Pods hält; die eigentlichen Container treten ihm bei. k3s crictl ist der CRI-Client, um direkt auf diese Ebene zu sehen – Pods, Container, Images – ohne den Umweg über den API-Server. K3s nutzt auf AlmaLinux 9 cgroup v2, die Ressourcengrenzen eines Pods sind also normale cgroup-Dateien unter /sys/fs/cgroup/kubepods.slice/.
Netzwerk: Flannel, kube-proxy, der Policy-Controller
- Flannel gibt jedem Pod eine Adresse aus
10.42.0.0/16(auf einem Knoten: ein/24), legt eine Bridgecni0und einveth-Paar pro Pod an. Backend hier: VXLAN – auf einem Knoten bedeutungslos, aber der Standard für einen zweiten. - kube-proxy macht aus Services iptables-Regeln (über iptables-nft, in nftables-Tabellen): ClusterIP-DNAT, NodePorts, Lastverteilung über die Endpoints.
- Der Network-Policy-Controller macht aus NetworkPolicies Chains (
KUBE-ROUTER-*,KUBE-NWPLCY-*, eineKUBE-POD-FW-*pro Pod). Er ist Teil des Binarys und standardmäßig an. Den sollte man sich merken.
Zertifikate und Tokens
K3s erzeugt beim ersten Start eine eigene CA und alle Zertifikate (/var/lib/rancher/k3s/server/tls/), gültig für ein Jahr, und erneuert sie automatisch, wenn der Dienst innerhalb von 90 Tagen vor Ablauf startet. Die Admin-Kubeconfig ist /etc/rancher/k3s/k3s.yaml; der Node-Token in /var/lib/rancher/k3s/server/token ist das, was ein zweiter Knoten zum Beitreten bräuchte.
🛡️ Warum es hier ist
- Kubernetes-Semantik auf einem kleinen Server. Deployments mit Rolling- oder Recreate-Updates, Probes, die einen hängenden Container neu starten, NetworkPolicies zwischen Pods, Ressourcengrenzen, eine Admission-Policy für Images – ohne einen Control-Plane-Cluster zu betreiben.
- Ein Binary, eine Unit, eine gepinnte Version. Die ganze Plattform ist aus Ansible reproduzierbar; ein Update heißt „Binary ersetzen, Unit neu starten“.
- Gute Unterstützung für AlmaLinux und SELinux: eine gepflegte SELinux-Policy (
k3s-selinux) und containerd mit SELinux-Labels.
🔧 Was dieses Repository macht
config.yaml: was ausgeschaltet, was eingeschaltet wird
disable:
- traefik # stattdessen F5 nginx-ingress (per Helm installiert)
- servicelb # keine LoadBalancer-Services; der Ingress nutzt hostNetwork
- local-storage # überhaupt keine PVCs - alles ist hostPath (0 PVCs auf allen Hosts)
flannel-backend: vxlan
write-kubeconfig-mode: "0600"
selinux: true
kube-apiserver-arg:
- "audit-log-path=/var/lib/rancher/k3s/server/logs/audit.log"
- "audit-policy-file=/etc/rancher/k3s/audit-policy.yaml"
- "audit-log-maxage=30"
- "audit-log-maxbackup=10"
- "audit-log-maxsize=100"
- "enable-admission-plugins=NodeRestriction,EventRateLimit"
- "admission-control-config-file=/etc/rancher/k3s/admission-control-config.yaml"
kubelet-arg:
- "pod-max-pids=4096"Die meisten Argumente stammen aus dem CIS-Benchmark für Kubernetes: ein API-Audit-Log mit Rotation, EventRateLimit, damit ein abstürzender Pod die API nicht mit Events flutet, eine PID-Grenze pro Pod gegen Fork-Bomben. Eine Empfehlung wird bewusst nicht befolgt: AlwaysPullImages würde bei jedem Pod-Start jedes Image aus der Registry ziehen – auch beim WordPress-Cron-Pod alle fünf Minuten – und einen kurzen Registry-Ausfall in fehlschlagende Pods überall verwandeln, für einen Mandanten-Schutz, den ein Cluster mit einem einzigen Betreiber nicht braucht.
Gepinnt, und in zwei Schritten installiert
Das Binary kommt aus dem GitHub-Release von k3s_version (versions.yml), das Installationsskript vom selben Tag – nicht von get.k3s.io, das immer das neueste Skript ausliefert und an dem Tag, an dem der zweite Nextcloud-Server eingerichtet wurde, eine Weile mit HTTP 500 antwortete. Das Skript läuft mit INSTALL_K3S_SKIP_DOWNLOAD=true, nur um die Unit anzulegen. Rund um K3s installiert die Rolle Helm, cert-manager mit einem Let’s-Encrypt-ClusterIssuer und den F5-nginx-Ingress-Controller und patcht jeden default-ServiceAccount, damit Pods kein API-Token mehr bekommen, das sie nie nutzen (CIS 5.1.5). Die Anwendungsrollen ergänzen pro Namespace eine ValidatingAdmissionPolicy, die Pods ablehnt, deren Images nicht aus einer erlaubten Liste von Registries stammen – seit Kubernetes 1.30 im API-Server eingebaut, ohne zusätzliche Komponente.
Der Weg eines Updates
- Die laufende Version lesen (
k3s --version). - Weicht sie vom Pin ab: das Binary mit
get_urlherunterladen,force: true. restoreconauf das Binary.- Die Handler sofort ausführen – der Neustart passiert hier, nicht am Ende des Plays.
- Warten, bis
kubectl get nodesantwortet, dann mit den Rollen weitermachen, die in den Cluster ausrollen. - Am Ende des Laufs vergleicht
common_version_activedie Kubelet-Version, die die API meldet, mit dem Pin.
Die Schritte 2, 3 und 6 gibt es jeweils wegen eines Fehlers, siehe unten.
🐛 Sechs Dinge, die K3s nicht von selbst tat
1. Das Update, das nichts tat
Bis zum 19. Oktober 2026 hatte die Download-Aufgabe kein force. Ansibles get_url überspringt den Download dann, wenn die Zieldatei schon existiert – jede Versionsanhebung seit der Erstinstallation war auf den bestehenden Hosts also lautlos wirkungslos geblieben. Es fiel auf, weil der Drift-Check für v1.37.1 für K3s überhaupt keine Änderung zeigte. Seitdem liest der Rauchtest am Ende jedes Laufs (common_version_active) die Version, die der Kubelet tatsächlich meldet, nicht die aus dem Inventar.
2. Pods, die SELinux nicht einschränkte
SELinux stand auf allen Hosts auf enforcing, und die HostPath-Verzeichnisse trugen container_file_t. Aber K3s startet containerd nur dann mit SELinux-Unterstützung, wenn seine Konfiguration selinux: true sagt – und das tat sie nicht. Jeder Pod lief als spc_t oder unconfined_service_t, die SELinux überhaupt nicht einschränkt. Seit dem 19. Oktober 2026 ist die Option gesetzt und die Policy k3s-selinux installiert; jeder Pod läuft jetzt als container_t mit eigenen MCS-Kategorien, und Pods, die schon vor der Umstellung existierten, mussten neu erstellt werden. Der SELinux-Deep-Dive erzählt die ganze Geschichte.
3. Ein neues Binary mit dem falschen Label
get_url schreibt über eine temporäre Datei und benennt sie um. Das neue Binary bekommt dadurch das Label seines Verzeichnisses (bin_t) statt container_runtime_exec_t aus der Policy, und K3s würde in der falschen Domain starten. Ein restorecon nach jedem Download behebt das.
4. Die Policy-Engine, die nie gebraucht wurde
Zwei Monate lang lief Calico neben K3s als vermeintliche NetworkPolicy-Engine. Es setzte nichts durch – Calicos Felix behandelt nur Interfaces namens cali*, Flannel legt veth* an –, und die gesamte Durchsetzung kam die ganze Zeit vom eigenen Controller von K3s. Der Beweis war unangenehm: Mit für einen Test abgeschaltetem Controller (disable-network-policy) erreichte ein Pod im Namespace default MariaDB auf Port 3306. Calico ist weg, und die Konfiguration trägt jetzt einen Kommentar in Großbuchstaben: den eingebauten Controller nie abschalten. Der nftables-Deep-Dive beschreibt die Firewall-Seite.
5. Ein Admission-Flag, das ersetzte statt ergänzte
Im Upstream-Kubernetes fügt --enable-admission-plugins Plugins zur Standardliste hinzu. In K3s ersetzt es die eigene Liste von K3s. Auf nur EventRateLimit gesetzt, fiel still NodeRestriction weg – das Plugin, das einen kompromittierten Kubelet daran hindert, Objekte anderer Knoten zu verändern. Ein erneuter kube-bench-Lauf fiel darauf herein (CIS 1.2.14 ging von PASS auf FAIL). Beide stehen jetzt ausdrücklich in der Liste. Eine verwandte Falle: Der API-Server legt das Verzeichnis seines Audit-Logs nicht selbst an und startet ohne es nicht; die Rolle legt es vor dem Neustart an.
📊 Gemessen
| Blog | Nextcloud-Server | |
|---|---|---|
| K3s-Version | v1.37.1+k3s1 | v1.37.1+k3s1 |
| Pods | 17 | 14 |
Speicher von k3s server (RSS) | 789 MB | 826 MB |
| containerd (RSS) | 202 MB | 190 MB |
CPU-Zeit von k3s server | 23 min in 7,5 h (≈ 5 % eines Kerns) | 2,5 h in 9,5 h (≈ 26 % eines Kerns) |
| Datenspeicher (state.db + WAL) | 24 + 20 MB | 23 + 20 MB |
| API-Audit-Log vor dem 20.10.2026 | 1,1 GB (an der eingestellten Obergrenze) | 1,0 GB, ~230 MB/Tag |
| API-Audit-Log nach der Policy-Änderung | – | ~28 MB/Tag |
Etwa ein Gigabyte RAM für die ganze Control Plane ist der Preis für „echtes Kubernetes auf einem Knoten“. Zwei Zahlen fielen auf. Das Audit-Log stand an seiner Obergrenze von zehn 100-MB-Sicherungen – die Rotation funktionierte, aber aus den „30 Tagen“ der Konfiguration wurden in Wahrheit viereinhalb. Und auf dem Nextcloud-Server brauchte die Control Plane fünfmal so viel CPU wie auf dem Blog, bei weniger Pods.
Siebtens, kein Fehler: dieselbe Arbeit zum fünffachen Preis
Das Audit-Log zeigte auf allen drei Servern dieselbe Schreiblast, und ein CPU-Profil über 30 Sekunden – ohne Neustart über /debug/pprof des Kubelets aufgenommen, das den ganzen K3s-Prozess profiliert – zeigte keinen einzelnen Schuldigen: Alles war etwa viereinhalbmal so teuer, vor allem der Go-Scheduler beim Aufwecken von Goroutinen und die Systemaufrufe dahinter. Go auf vier Threads zu begrenzen (GOMAXPROCS=4) änderte nichts. Die Antwort stand in /sys/devices/system/cpu/vulnerabilities/: Die Nextcloud-Server laufen auf einer älteren AMD-EPYC-Generation, die für Retbleed anfällig ist, und der Kernel schützt jeden Rücksprung aus einer Kernel-Funktion mit einem „Return Thunk“. Für ein Programm, das von Systemaufrufen und Kontextwechseln lebt, kostet das viel; die neuere CPU des Blogs ist „not affected“. Den Schutz auf einer geteilten virtuellen Maschine abzuschalten wäre leichtsinnig – etwa 2,6 % eines Servers mit acht Kernen sind der Preis.
Sechstens: ein Audit-Log, das in seinem eigenen Rauschen ertrank
Die Aufschlüsselung einer 100-MB-Datei zeigte, woher das Volumen kam: Lease-Herzschläge der Leader-Wahlen und des Kubelets (23 %), Health-, Discovery- und OpenAPI-Abfragen (14 %), der API-Server im Gespräch mit sich selbst (12 %) und Lesezugriffe der Plattform-Komponenten, ServiceAccounts und Nodes (etwa 40 %). Nichts davon hat Sicherheitswert; es schob nur die interessanten Einträge aus dem Aufbewahrungsfenster. Die Policy lässt jetzt genau diese weg – mit zwei Regeln, die absichtlich vor der Weglass-Regel stehen: Lesezugriffe auf Secrets bleiben für jeden Leser im Log, und alles, was ein Administrator tut, ebenso. Eine Simulation an der echten Datei sagte voraus, dass 10 % übrig bleiben; nach dem Ausrollen wuchs das Log um etwa 28 MB am Tag statt um 230.
rules:
- level: None
nonResourceURLs: ["/healthz*", "/livez*", "/readyz*", "/version", "/openapi*", "/api*", "/metrics"]
- level: None
users: ["system:apiserver"]
- level: None
resources: [{group: "coordination.k8s.io", resources: ["leases"]}]
- level: Metadata # vor der nächsten Regel: Secrets für jeden Leser
resources: [{group: "", resources: ["secrets"]}]
- level: None
verbs: ["get", "list", "watch"]
userGroups: ["system:serviceaccounts", "system:nodes"]
... # exec/attach auf Request, RBAC auf RequestResponse, Rest auf Metadata🧰 Betrieb im Alltag
# der Cluster
k3s kubectl get nodes -o wide
k3s kubectl get pods -A
k3s kubectl top pods -A # metrics-server gehört zu K3s
# eine Ebene tiefer: containerd direkt, ohne den API-Server
k3s crictl pods
k3s crictl ps
k3s crictl images
# Logs der ganzen Plattform (API-Server, Kubelet, Flannel, Policy-Controller)
journalctl -u k3s --since -1h
# Zertifikate: wann laufen sie ab? (Erneuerung bei einem Neustart innerhalb von 90 Tagen)
for c in /var/lib/rancher/k3s/server/tls/*.crt; do
printf '%-45s ' "$(basename "$c")"; openssl x509 -enddate -noout -in "$c" | cut -d= -f2
done
# SELinux: in welcher Domain laufen die Pods?
ps -eZ | grep container_t | headUpdates
Immer nur eine Minor-Version auf einmal, wie Kubernetes es verlangt (1.36 → 1.37, nicht 1.35 → 1.37). k3s_version in versions.yml anheben, den Drift-Check laufen lassen (--check --diff) – er muss jetzt den Binary-Download zeigen –, das Playbook laufen lassen und am Ende den Versionsbericht lesen. Ein Neustart von K3s startet weder die Pods noch die Container von containerd neu; die API ist etwa 20 Sekunden weg, die Websites nicht.
Backups – und was wirklich gebraucht wird
Der Datenspeicher wird hier bewusst nicht gesichert. Jedes Objekt darin legt Ansible aus dem Repository und dem Vault an: Namespaces, Secrets, Deployments, Policies. Die Daten, auf die es ankommt, liegen außerhalb des Clusters in HostPath-Verzeichnissen und in den Datenbanken, und die zieht der Backup-Server jede Nacht ab. Einen Server wiederherzustellen heißt daher: AlmaLinux installieren, das Playbook laufen lassen, die Datenverzeichnisse zurückspielen. Der Cluster selbst ist ersetzbar – das stärkste Argument dafür, alles im Code zu halten.
🆘 Spickzettel: Symptom → Befehl
Die Kurzfassung dieses Artikels für den Moment, in dem etwas nicht stimmt. Auf dem Host als root ausführen; <…> sind Platzhalter.
| Symptom | Befehl |
|---|---|
| Ein Pod startet nicht | k3s kubectl get pods -A |
| Warum ist ein Container abgestürzt? | k3s kubectl -n <ns> logs <pod> -c <container> --previous |
| Was ist zuletzt passiert? | k3s kubectl get events -A --sort-by=.lastTimestamp | tail -20 |
| kubectl hängt oder der Node ist NotReady | systemctl status k3s |
| Wer verbraucht CPU und Speicher? | k3s kubectl top pods -A |
| Geänderte ConfigMap oder Secret nicht aktiv | k3s kubectl -n <ns> rollout restart deployment/<name> |
| Platte füllt sich mit alten Images | k3s crictl rmi --prune |
| Pod von SELinux blockiert? | ausearch -m AVC -ts recent | grep container_t |
| Wann laufen die Zertifikate ab? | openssl x509 -enddate -noout -in /var/lib/rancher/k3s/server/tls/serving-kube-apiserver.crt |
🔭 Was als Nächstes kommt
- kube-proxy im nftables-Modus, sobald K3s ihn zum Standard macht – dann kann die iptables-nft-Kompatibilitätsschicht weg.
- Ein zweiter Knoten würde drei Dinge ändern: etcd statt SQLite, HostPath-Volumes funktionierten nicht mehr für Pods, die umziehen können, und Flannels VXLAN würde tatsächlich Verkehr tragen.
⚖️ Alternativen – und warum K3s
| Option | Stärke | Warum nicht hier |
|---|---|---|
| kubeadm | Upstream-Kubernetes, nichts verborgen | etcd, die Control-Plane-Komponenten und das CNI betreibt und aktualisiert man selbst und einzeln – viel für einen Knoten. |
| k0s | Ebenfalls ein Binary, saubere Standardwerte | Vergleichbar; K3s hat die gepflegte SELinux-Policy und mehr Dokumentation für AlmaLinux. |
| MicroK8s | Einfache Add-ons | Wird als Snap verteilt – auf AlmaLinux nicht zu Hause. |
| Docker/Podman Compose | Viel einfacher | Keine Probes mit Neustart, keine NetworkPolicies, keine Admission-Policy, keine Rolling Updates. |
| K3s | Volle Kubernetes-API, kleinster Fußabdruck, SELinux-Policy | Gewählt. |
K3s hält sein Versprechen: echtes Kubernetes in einem Binary. Die letzten Wochen haben gezeigt, dass „ein Binary“ auch „ein Ort, an dem sich Standardwerte verstecken“ heißt. Vier der sechs Probleme oben waren keine Fehler in K3s, sondern Standardwerte und Annahmen – SELinux aus, solange man nicht danach fragt, ein Admission-Flag, das ersetzt statt ergänzt, ein Policy-Controller, der schon da war, und unsere eigene Auffangregel im Audit, die jeden Herzschlag mitschrieb. Jedes davon steht jetzt als Kommentar in der Rolle, direkt neben der Zeile, die es zurückbringen würde.


