Un cluster Kubernetes de homelab n'a pas besoin de trois nœuds de contrôle et d'un etcd répliqué. K3s tient dans un binaire unique et fait tourner l'ensemble des services web du homelab sur une seule VM. Voici ce qu'il y a dedans, et les trois pièges qui coûtent une soirée chacun.
L'architecture en un coup d'œil
Le cluster tourne sur une VM KVM provisionnée par Proxmox, sur un VLAN dédié routé par le pare-feu à zones du routeur principal. Le TLS est terminé en amont par HAProxy : Traefik ne reçoit que du HTTP interne.
Il est monté par un playbook Ansible, k3s-deploy.
flowchart TB
subgraph entree["Entrée"]
TFK["Traefik<br/>Gateway API"]
CS["CrowdSec<br/>middleware"]
end
subgraph sites["Sites web"]
WAF["waf<br/>inspection + routage"]
VRN["varnish<br/>cache"]
WP["jbsky-fr-production<br/>WordPress"]
end
subgraph socle["Socle"]
LH["longhorn-system<br/>stockage"]
CM["cert-manager<br/>certificats"]
LOG["logging<br/>journaux"]
RUN["gitea-runner<br/>intégration"]
end
TFK --> CS --> WAF --> VRN --> WP
WP --> LH
Le chemin d'une requête depuis Internet — terminaison TLS, re-chiffrement, alignement des timeouts — est traité dans le reverse proxy. Ce qui suit ne regarde que l'intérieur du cluster.
Ce qui tourne
L'isolation se fait par namespace, un par fonction :
| Namespace | Contenu |
|---|---|
jbsky-fr-production |
WordPress : nginx, php-fpm, Redis, deux exporters — chart Helm |
waf |
l'entrée unique des sites web : un server_name par domaine, et ModSecurity réglé domaine par domaine |
varnish |
le cache HTTP, en StatefulSet |
crowdsec |
l'API locale et l'agent qui lit les journaux |
cert-manager |
les certificats, émis par DNS-01 |
longhorn-system |
le stockage |
logging |
la collecte de journaux |
gitea-runner |
l'exécuteur de la chaîne d'intégration |
Le reste du cluster héberge quelques services annexes — un agenda CalDAV, un whoami de diagnostic — dans leurs propres namespaces.
Cette séparation n'est pas cosmétique : elle porte les quotas de ressources et les règles réseau, qui s'appliquent par namespace.
Longhorn : le volume est l'unité qui compte
Même sur un nœud unique, Longhorn apporte ce que le stockage local du nœud ne donne pas : des snapshots pris sans interrompre le service, une cible de sauvegarde externe (un partage NFS), et une restauration qui ne demande pas de reconstruire le pod.
Le choix a une conséquence directe sur la sauvegarde. Les images du parc sont FROM scratch : il n'y a ni tar, ni shell, ni rien pour lire les fichiers depuis l'intérieur du conteneur. Un kubectl exec … tar cf - n'a nulle part où s'exécuter. La sauvegarde se fait donc au niveau du volume, pas au niveau applicatif — ce qui est de toute façon le bon niveau : le volume contient l'état, l'image contient le code, et le code se reconstruit.
Un seul volume persistant porte tout wp-content : les uploads, les extensions et les thèmes. Le cœur de WordPress, lui, n'est pas sauvegardé — il est retéléchargé à chaque démarrage du pod.
L'ingress : Gateway API plutôt qu'Ingress
K3s embarque Traefik. La configuration s'appuie sur les CRDs Gateway API — un HTTPRoute par service, avec ses règles de correspondance sur l'hôte et le chemin — plutôt que sur les objets Ingress, dont les annotations propres à chaque contrôleur rendaient la configuration non portable.
Les middlewares Traefik s'insèrent avant les backends : limitation de débit, en-têtes de sécurité, et le bouncer CrowdSec qui refuse les adresses déjà signalées avant même que la requête n'atteigne le WAF.
Le cluster se construit tout seul
Rien de tout cela n'est appliqué à la main. K3s applique au démarrage ce qu'il trouve dans /var/lib/rancher/k3s/server/manifests/ : le playbook y dépose Traefik, CrowdSec et cert-manager, et le cluster converge de lui-même — pas de kubectl apply, pas d'ordre de passage à respecter, pas d'état à réconcilier après une réinstallation.
Traefik mérite une précision. Il est désactivé dans la configuration de K3s, puis redéployé par un manifeste à nous. C'est ce qui permet de choisir sa version et sa configuration au lieu de subir celles de la distribution — et d'y brancher le bouncer CrowdSec.
Trois pièges qui coûtent une soirée
Un ResourceQuota rend requests et limits obligatoires
Dès qu'un ResourceQuota est actif dans un namespace, tout pod doit déclarer ses requests et ses limits CPU et mémoire. Sans elles, l'admission controller refuse le pod avec un forbidden: failed quota.
Le piège n'est pas le refus, il est dans son moment : le quota a été posé il y a des mois, le déploiement qu'on ajoute aujourd'hui est refusé, et le message ne dit pas que la cause est un quota posé ailleurs.
Un ConfigMap ne se met à jour que dans un seul des deux modes
Un ConfigMap se monte de deux façons, et elles ne se comportent pas pareil :
| Montage | Effet | kubelet met-il à jour ? |
|---|---|---|
| Le volume entier sur un répertoire | remplace le contenu du répertoire | oui, en une minute environ |
Un fichier avec subPath |
dépose un seul fichier sans toucher au reste | non, jamais |
Les deux coexistent dans un même pod, souvent dans le même volumeMounts. Le WAF
de ce cluster en est l'exemple : son répertoire conf.d est monté en entier et
suit donc les modifications, tandis que son nginx.conf et ses règles sont posés
en subPath et restent figés jusqu'à la recréation du pod.
Ce qui rend le piège coûteux, c'est qu'il ne produit aucune erreur :
kubectl describe configmapmontre bien la nouvelle valeur ;- le fichier dans le pod contient toujours l'ancienne ;
- le service tourne, sans rien signaler.
Et la mise à jour du fichier ne suffit pas non plus. Un serveur lit sa
configuration au démarrage : nginx qui voit arriver un nouveau fichier dans
conf.d continue d'appliquer les règles qu'il a en mémoire. Les deux modes
finissent donc au même endroit — un kubectl rollout restart — l'un parce que
le fichier n'a pas bougé, l'autre parce que personne ne l'a relu.
Un tag réutilisé ne redescend jamais
Avec imagePullPolicy: IfNotPresent, une image déjà présente dans le cache du nœud n'est pas retéléchargée — même si le registre a reçu une nouvelle version sous le même tag. Le pod redémarre, le correctif n'est pas là, et rien ne l'indique.
La parade tient en deux règles : ne jamais republier sous un tag existant, et faire porter à chaque image un tag qui ne désigne qu'un seul build. Les images du parc en publient un pour ça, suffixé d'un compteur de révision ; c'est le seul qu'un manifeste doit épingler.
Liens
- Playbook du cluster : k3s-deploy
- Chart Helm WordPress : wordpress-hardened