
🌐 Aussi en: English · Deutsch · Español
Deep Dive de la série autour de l’Ansible Tour. Niveau : administration Linux avancée. Le Tour explique les rôles ; les deep dives expliquent la technique en dessous – comment elle fonctionne, pourquoi elle est là, ce que cette installation en fait exactement, comment l’exploiter au quotidien et quelles autres options il y aurait eu.
K3s est un Kubernetes certifié dans un seul binaire d’environ 70 Mo. API server, scheduler, controller manager, kubelet, kube-proxy, containerd, Flannel, CoreDNS et un contrôleur de NetworkPolicy démarrent tous depuis un processus et une unité systemd. Cela en fait le choix évident pour un cluster d’exactement un nœud – et cela cache beaucoup de choses. Cet article explique ce qui tourne réellement dans ce binaire, ce que configure la partie 2 de la visite, et six choses que K3s n’a pas faites tout seul, découvertes seulement en exploitation.
🧭 Ce que contient le binaire
Un superviseur, de nombreux composants
k3s server est un superviseur. Il démarre les composants du plan de contrôle de Kubernetes comme goroutines dans son propre processus – API server, scheduler, controller manager – et, à côté, les composants du nœud : le kubelet, kube-proxy, le CNI Flannel et un contrôleur de NetworkPolicy intégré (du code de kube-router). containerd tourne comme processus enfant séparé, avec son propre socket sous /run/k3s/containerd/. Tout partage un flux de journaux (journalctl -u k3s) et un numéro de version.
Le stockage : SQLite au lieu d’etcd
Kubernetes upstream stocke son état dans etcd. Un K3s à un nœud utilise SQLite via kine, une couche qui traduit l’API d’etcd en SQL. Tout l’état du cluster – chaque Deployment, Secret, ConfigMap, événement – se trouve dans /var/lib/rancher/k3s/server/db/state.db : 24 Mo sur le blog, 23 Mo sur un serveur Nextcloud, plus un journal d’écriture anticipée de 20 Mo. Avec plus d’un nœud serveur, K3s passe à un etcd intégré.
containerd, le CRI et le conteneur pause
Le kubelet parle à containerd via la Container Runtime Interface. Pour chaque pod, containerd démarre d’abord un minuscule conteneur pause qui porte l’espace de noms réseau du pod ; les vrais conteneurs le rejoignent. k3s crictl est le client CRI pour regarder directement à ce niveau – pods, conteneurs, images – sans passer par l’API server. Sur AlmaLinux 9, K3s utilise cgroup v2 : les limites de ressources d’un pod sont de simples fichiers cgroup sous /sys/fs/cgroup/kubepods.slice/.
Réseau : Flannel, kube-proxy, le contrôleur de politiques
- Flannel donne à chaque pod une adresse de
10.42.0.0/16(sur un nœud : un/24), crée un pontcni0et une pairevethpar pod. Backend ici : VXLAN – sans importance sur un nœud, mais la valeur par défaut pour un second. - kube-proxy transforme les Services en règles iptables (via iptables-nft, dans des tables nftables) : DNAT des ClusterIP, NodePorts, répartition de charge sur les endpoints.
- Le contrôleur de NetworkPolicy transforme les NetworkPolicies en chaînes (
KUBE-ROUTER-*,KUBE-NWPLCY-*, uneKUBE-POD-FW-*par pod). Il fait partie du binaire et il est actif par défaut. À retenir.
Certificats et jetons
K3s crée au premier démarrage sa propre CA et tous les certificats (/var/lib/rancher/k3s/server/tls/), valables un an, et les renouvelle automatiquement quand le service démarre dans les 90 jours précédant l’expiration. Le kubeconfig d’administration est /etc/rancher/k3s/k3s.yaml ; le jeton du nœud dans /var/lib/rancher/k3s/server/token est ce dont un second nœud aurait besoin pour rejoindre le cluster.
🛡️ Pourquoi il est là
- La sémantique de Kubernetes sur un petit serveur. Des Deployments avec mise à jour progressive ou recréation, des sondes qui redémarrent un conteneur bloqué, des NetworkPolicies entre pods, des limites de ressources, une politique d’admission pour les images – sans faire tourner un cluster de plan de contrôle.
- Un binaire, une unité, une version épinglée. Toute la plateforme est reproductible depuis Ansible ; une mise à jour signifie « remplacer le binaire, redémarrer l’unité ».
- Un bon support d’AlmaLinux et de SELinux : une politique SELinux maintenue (
k3s-selinux) et containerd avec étiquetage SELinux.
🔧 Ce que fait ce dépôt
config.yaml : ce qui est désactivé, ce qui est activé
disable:
- traefik # F5 nginx-ingress à la place (installé avec Helm)
- servicelb # pas de Services LoadBalancer ; l'ingress utilise hostNetwork
- local-storage # aucun PVC - tout est en hostPath (0 PVC sur tous les hôtes)
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"La plupart des arguments viennent du benchmark CIS pour Kubernetes : un journal d’audit de l’API avec rotation, EventRateLimit pour qu’un pod en boucle de plantage n’inonde pas l’API d’événements, une limite de PID par pod contre les fork bombs. Une recommandation n’est volontairement pas suivie : AlwaysPullImages tirerait chaque image du registre à chaque démarrage de pod – y compris le pod cron de WordPress toutes les cinq minutes – et transformerait une courte panne du registre en pods défaillants partout, pour une protection multi-locataire dont un cluster à un seul exploitant n’a pas besoin.
Épinglé, et installé en deux étapes
Le binaire vient de la release GitHub de k3s_version (versions.yml), le script d’installation du même tag – pas de get.k3s.io, qui sert toujours le script le plus récent et a répondu HTTP 500 un moment le jour où le second serveur Nextcloud a été installé. Le script tourne avec INSTALL_K3S_SKIP_DOWNLOAD=true, seulement pour créer l’unité. Autour de K3s, le rôle installe Helm, cert-manager avec un ClusterIssuer Let’s Encrypt et le contrôleur F5 nginx-ingress, et modifie chaque ServiceAccount default pour que les pods ne reçoivent plus un jeton d’API qu’ils n’utilisent jamais (CIS 5.1.5). Les rôles applicatifs ajoutent par namespace une ValidatingAdmissionPolicy qui refuse les pods dont les images ne viennent pas d’une liste de registres autorisés – intégrée à l’API server depuis Kubernetes 1.30, sans composant supplémentaire.
Le chemin d’une mise à jour
- Lire la version en cours (
k3s --version). - Si elle diffère de l’épinglage : télécharger le binaire avec
get_url,force: true. restoreconsur le binaire.- Exécuter tout de suite les handlers – le redémarrage a lieu ici, pas à la fin du play.
- Attendre que
kubectl get nodesréponde, puis continuer avec les rôles qui déploient dans le cluster. - En fin d’exécution,
common_version_activecompare la version du kubelet rapportée par l’API à l’épinglage.
Les étapes 2, 3 et 6 existent chacune à cause d’une erreur, voir ci-dessous.
🐛 Six choses que K3s n’a pas faites tout seul
1. La mise à jour qui ne faisait rien
Jusqu’au 19 octobre 2026, la tâche de téléchargement n’avait pas de force. Le get_url d’Ansible saute alors le téléchargement si le fichier de destination existe déjà – chaque montée de version depuis la première installation était donc restée sans effet sur les hôtes existants, en silence. Cela s’est vu parce que la vérification de dérive pour v1.37.1 n’affichait aucun changement pour K3s. Depuis, le test de fumée en fin d’exécution (common_version_active) lit la version que le kubelet rapporte vraiment, pas celle de l’inventaire.
2. Des pods que SELinux ne confinait pas
SELinux était en mode enforcing sur tous les hôtes et les répertoires HostPath portaient container_file_t. Mais K3s ne démarre containerd avec le support SELinux que si sa configuration indique selinux: true – et ce n’était pas le cas. Chaque pod tournait en spc_t ou unconfined_service_t, que SELinux ne restreint pas du tout. Depuis le 19 octobre 2026, l’option est active et la politique k3s-selinux installée ; chaque pod tourne désormais en container_t avec ses propres catégories MCS, et les pods existant avant le changement ont dû être recréés. Le deep dive SELinux raconte toute l’histoire.
3. Un nouveau binaire avec la mauvaise étiquette
get_url écrit via un fichier temporaire puis le renomme. Le nouveau binaire reçoit donc l’étiquette de son répertoire (bin_t) au lieu de container_runtime_exec_t défini par la politique, et K3s démarrerait dans le mauvais domaine. Un restorecon après chaque téléchargement corrige cela.
4. Le moteur de politiques dont on n’avait jamais eu besoin
Pendant deux mois, Calico a tourné à côté de K3s comme moteur supposé de NetworkPolicy. Il n’appliquait rien – Felix, le composant de Calico, ne gère que les interfaces nommées cali*, Flannel crée des veth* – et toute l’application était assurée depuis le début par le contrôleur propre de K3s. La preuve fut désagréable : avec ce contrôleur coupé pour un test (disable-network-policy), un pod du namespace default a atteint MariaDB sur le port 3306. Calico a disparu, et la configuration porte maintenant un commentaire en majuscules : ne jamais désactiver le contrôleur intégré. Le deep dive nftables décrit le côté pare-feu.
5. Un drapeau d’admission qui remplaçait au lieu d’ajouter
Dans Kubernetes upstream, --enable-admission-plugins ajoute des plugins à la liste par défaut. Dans K3s, il remplace la liste propre à K3s. Réglé sur le seul EventRateLimit, il a supprimé en silence NodeRestriction – le plugin qui empêche un kubelet compromis de modifier les objets d’autres nœuds. Un nouveau passage de kube-bench l’a détecté (CIS 1.2.14 est passé de PASS à FAIL). Les deux sont maintenant listés explicitement. Un piège voisin : l’API server ne crée pas le répertoire de son journal d’audit et refuse de démarrer sans lui ; le rôle le crée avant le redémarrage.
📊 Mesures
| Blog | Serveur Nextcloud | |
|---|---|---|
| Version de K3s | v1.37.1+k3s1 | v1.37.1+k3s1 |
| Pods | 17 | 14 |
Mémoire de k3s server (RSS) | 789 Mo | 826 Mo |
| containerd (RSS) | 202 Mo | 190 Mo |
Temps CPU de k3s server | 23 min en 7,5 h (≈ 5 % d’un cœur) | 2,5 h en 9,5 h (≈ 26 % d’un cœur) |
| Stockage (state.db + WAL) | 24 + 20 Mo | 23 + 20 Mo |
| Journal d’audit de l’API avant le 20/10/2026 | 1,1 Go (au plafond configuré) | 1,0 Go, ~230 Mo/jour |
| Journal d’audit après le changement de politique | – | ~28 Mo/jour |
Environ un gigaoctet de RAM pour tout le plan de contrôle, c’est le prix d’un « vrai Kubernetes sur un nœud ». Deux chiffres sautaient aux yeux. Le journal d’audit était à son plafond de dix sauvegardes de 100 Mo – la rotation fonctionnait, mais les « 30 jours » de la configuration n’étaient en réalité que quatre et demi. Et sur le serveur Nextcloud, le plan de contrôle consommait cinq fois plus de CPU que sur le blog, avec moins de pods.
Septième, pas un bug : le même travail pour cinq fois le prix
Le journal d’audit montrait la même charge d’écriture sur les trois serveurs, et un profil CPU de 30 secondes – pris sans redémarrage via le /debug/pprof du kubelet, qui profile tout le processus K3s – n’a désigné aucun coupable unique : tout coûtait environ quatre fois et demie plus, surtout le scheduler de Go qui réveille des goroutines et les appels système qui suivent. Limiter Go à quatre threads (GOMAXPROCS=4) n’a rien changé. La réponse se trouvait dans /sys/devices/system/cpu/vulnerabilities/ : les serveurs Nextcloud tournent sur une génération plus ancienne d’AMD EPYC, vulnérable à Retbleed, et le noyau protège chaque retour d’une fonction du noyau par un « return thunk ». Pour un programme qui vit d’appels système et de changements de contexte, cela coûte cher ; le processeur plus récent du blog est « not affected ». Désactiver la protection sur une machine virtuelle partagée serait imprudent – environ 2,6 % d’un serveur à huit cœurs, c’est le prix.
Sixième : un journal d’audit noyé dans son propre bruit
La décomposition d’un fichier de 100 Mo a montré d’où venait le volume : les battements de cœur des leases des élections de leader et du kubelet (23 %), les requêtes de santé, de découverte et OpenAPI (14 %), l’API server qui se parle à lui-même (12 %), et les lectures des composants de la plateforme, des ServiceAccounts et des nœuds (environ 40 %). Rien de tout cela n’a de valeur pour la sécurité ; cela ne faisait que pousser les entrées intéressantes hors de la fenêtre de conservation. La politique supprime maintenant exactement cela – avec deux règles volontairement placées avant la règle de suppression : les lectures de secrets restent journalisées pour tout lecteur, et tout ce que fait un administrateur aussi. Une simulation sur le vrai fichier prévoyait qu’il en resterait 10 % ; après le déploiement, le journal a grandi d’environ 28 Mo par jour au lieu de 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 # avant la règle suivante : secrets pour tout lecteur
resources: [{group: "", resources: ["secrets"]}]
- level: None
verbs: ["get", "list", "watch"]
userGroups: ["system:serviceaccounts", "system:nodes"]
... # exec/attach en Request, RBAC en RequestResponse, le reste en Metadata🧰 Exploitation au quotidien
# le cluster
k3s kubectl get nodes -o wide
k3s kubectl get pods -A
k3s kubectl top pods -A # metrics-server fait partie de K3s
# un niveau plus bas : containerd directement, sans l'API server
k3s crictl pods
k3s crictl ps
k3s crictl images
# journaux de toute la plateforme (API server, kubelet, Flannel, contrôleur de politiques)
journalctl -u k3s --since -1h
# certificats : quand expirent-ils ? (renouvelés lors d'un redémarrage dans les 90 jours)
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 : dans quel domaine tournent les pods ?
ps -eZ | grep container_t | headMises à jour
Une version mineure à la fois, comme Kubernetes l’exige (1.36 → 1.37, pas 1.35 → 1.37). Relever k3s_version dans versions.yml, lancer la vérification de dérive (--check --diff) – elle doit maintenant montrer le téléchargement du binaire –, lancer le playbook et lire le rapport de versions à la fin. Un redémarrage de K3s ne redémarre ni les pods ni les conteneurs de containerd ; l’API disparaît une vingtaine de secondes, les sites web non.
Sauvegardes – et ce dont on a vraiment besoin
Le stockage n’est volontairement pas sauvegardé ici. Chaque objet qu’il contient est créé par Ansible à partir du dépôt et du vault : namespaces, Secrets, Deployments, politiques. Les données qui comptent sont hors du cluster, dans des répertoires HostPath et dans les bases de données, et le serveur de sauvegarde les récupère chaque nuit. Restaurer un serveur signifie donc : installer AlmaLinux, lancer le playbook, restaurer les répertoires de données. Le cluster lui-même est jetable – l’argument le plus fort pour tout garder dans le code.
🆘 Antisèche : symptôme → commande
La version courte de cet article, pour le moment où quelque chose ne va pas. À lancer sur l’hôte en root ; <…> sont des espaces réservés.
| Symptôme | Commande |
|---|---|
| Un pod ne démarre pas | k3s kubectl get pods -A |
| Pourquoi un conteneur a-t-il planté ? | k3s kubectl -n <ns> logs <pod> -c <container> --previous |
| Que s’est-il passé récemment ? | k3s kubectl get events -A --sort-by=.lastTimestamp | tail -20 |
| kubectl bloque ou le nœud est NotReady | systemctl status k3s |
| Qui consomme CPU et mémoire ? | k3s kubectl top pods -A |
| ConfigMap ou Secret modifié pas pris en compte | k3s kubectl -n <ns> rollout restart deployment/<name> |
| Le disque se remplit d’anciennes images | k3s crictl rmi --prune |
| Pod bloqué par SELinux ? | ausearch -m AVC -ts recent | grep container_t |
| Quand les certificats expirent-ils ? | openssl x509 -enddate -noout -in /var/lib/rancher/k3s/server/tls/serving-kube-apiserver.crt |
🔭 La suite
- kube-proxy en mode nftables dès que K3s en fera la valeur par défaut – alors la couche de compatibilité iptables-nft pourra disparaître.
- Un second nœud changerait trois choses : etcd au lieu de SQLite, les volumes HostPath ne fonctionneraient plus pour des pods qui peuvent se déplacer, et le VXLAN de Flannel transporterait vraiment du trafic.
⚖️ Alternatives – et pourquoi K3s
| Option | Point fort | Pourquoi pas ici |
|---|---|---|
| kubeadm | Kubernetes upstream, rien de caché | etcd, les composants du plan de contrôle et le CNI sont à faire tourner et mettre à jour soi-même, séparément – beaucoup pour un nœud. |
| k0s | Lui aussi un seul binaire, des valeurs par défaut propres | Comparable ; K3s a la politique SELinux maintenue et plus de documentation pour AlmaLinux. |
| MicroK8s | Modules complémentaires faciles | Distribué en snap – pas chez lui sur AlmaLinux. |
| Docker/Podman Compose | Beaucoup plus simple | Pas de sondes avec redémarrage, pas de NetworkPolicies, pas de politique d’admission, pas de mises à jour progressives. |
| K3s | API Kubernetes complète, empreinte minimale, politique SELinux | Choisi. |
K3s tient sa promesse : un vrai Kubernetes dans un seul binaire. Les dernières semaines ont montré qu’« un binaire », c’est aussi « un endroit où se cachent les valeurs par défaut ». Quatre des six problèmes ci-dessus n’étaient pas des bugs de K3s mais des valeurs par défaut et des suppositions – SELinux désactivé tant qu’on ne le demande pas, un drapeau d’admission qui remplace au lieu d’ajouter, un contrôleur de politiques déjà présent, et notre propre règle d’audit fourre-tout qui notait chaque battement de cœur. Chacun figure maintenant en commentaire dans le rôle, juste à côté de la ligne qui le ramènerait.


