Introduction
Dans un homelab, faire tourner un cluster Kubernetes complet (kubeadm, etcd multi-nœuds, control-plane HA) est souvent disproportionné par rapport aux besoins réels. K3s, la distribution légère de Rancher Labs, offre un compromis idéal : un binaire unique de ~70 Mo qui embarque tout le nécessaire — API server, scheduler, controller-manager, etcd intégré (SQLite ou etcd embarqué), kubelet et kube-proxy — sans sacrifier la compatibilité avec l'écosystème Kubernetes standard.
Cet article détaille l'architecture d'un cluster K3s single-node hébergeant l'ensemble des services web d'un homelab, depuis le reverse-proxy WAF jusqu'au cache Varnish, en passant par WordPress en production et intégration.
Architecture générale
Le cluster tourne sur une machine virtuelle KVM provisionnée via Proxmox VE sur un hyperviseur dédié. La VM dispose de ressources généreuses (8 vCPUs, 16 Go RAM, stockage SSD NVMe) et réside sur un VLAN dédié isolé du reste du réseau. Ce VLAN est routé via le routeur principal qui gère le firewalling zone-based entre les segments.
┌─────────────────────────────────────────────────────────┐
│ Proxmox VE (Hyperviseur) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ VM K3s (VLAN Services) │ │
│ │ │ │
│ │ ┌─────────┐ ┌──────────┐ ┌────────────────┐ │ │
│ │ │ Traefik │──│ WAF │──│ WordPress │ │ │
│ │ │ Ingress │ │ nginx │ │ (prod+integ) │ │ │
│ │ └─────────┘ └──────────┘ └────────────────┘ │ │
│ │ │ │ │ │
│ │ ┌─────────┐ ┌──────────┐ ┌────────────────┐ │ │
│ │ │ Varnish │ │Prometheus│ │ Longhorn │ │ │
│ │ │ Cache │ │Exporters │ │ Storage │ │ │
│ │ └─────────┘ └──────────┘ └────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Namespaces et isolation
L'organisation en namespaces Kubernetes permet une séparation logique claire des workloads :
- jbsky-fr-production : WordPress production (PHP-FPM + nginx), persistant volumes pour les uploads et la base de données
- integ-jbsky-fr : environnement d'intégration miroir de la production, permet de tester les mises à jour WordPress, plugins et thèmes avant promotion
- waf : le Web Application Firewall basé sur nginx + ModSecurity v3 + OWASP Core Rule Set, intercepte tout le trafic entrant avant qu'il n'atteigne les backends
- varnish : cache HTTP Varnish qui accélère les pages WordPress en servant les réponses depuis la mémoire
- kube-system : composants système K3s (CoreDNS, Traefik, metrics-server, local-path-provisioner)
Cette séparation permet d'appliquer des NetworkPolicies granulaires et des quotas de ressources indépendants par environnement.
Stockage : Longhorn
Même sur un cluster single-node, Longhorn apporte une valeur considérable. Ce système de stockage distribué cloud-native fournit :
- Snapshots : capture instantanée de l'état d'un volume à un instant T, sans interruption de service
- Backups programmés : sauvegarde automatique vers un stockage externe (NFS, S3-compatible)
- Réplication : même avec un seul nœud, la réplication intra-nœud protège contre la corruption de données
- Restauration : rollback rapide vers un snapshot précédent en cas de problème
Les PersistentVolumeClaims (PVC) sont provisionnés dynamiquement via la StorageClass Longhorn, avec des politiques de rétention configurées par namespace.
Ingress : Traefik et HTTPRoute
K3s embarque nativement Traefik comme contrôleur d'ingress. Plutôt que d'utiliser les anciens objets Ingress, la configuration s'appuie sur les Gateway API CRDs (HTTPRoute), le standard moderne de Kubernetes pour le routage HTTP :
- Chaque service expose un
HTTPRouteavec ses règles de matching (host, path, headers) - Traefik applique des middlewares (rate-limiting, headers de sécurité, basicAuth pour le dashboard)
- Le TLS est terminé en amont par HAProxy ; Traefik reçoit du trafic HTTP interne
- Les CrowdSec middlewares bloquent les IPs malveillantes avant même d'atteindre le WAF
Workloads déployés
WordPress (production + intégration)
Chaque environnement WordPress est composé de :
- Un Deployment PHP-FPM avec l'image hardened FROM scratch (compilée depuis les sources, aucun shell)
- Un initContainer wordpress-files qui déploie le core WordPress dans un volume partagé emptyDir au démarrage du pod
- Un sidecar nginx qui sert les fichiers statiques et proxifie les requêtes PHP vers FPM via le socket Unix
- Des ConfigMaps pour wp-config.php, nginx.conf, et les configurations PHP
- Des Secrets pour les credentials base de données et les salts WordPress
WAF nginx (ModSecurity + OWASP CRS)
Le WAF intercepte toutes les requêtes entrantes. L'image nginx-waf-hardened embarque ModSecurity v3 compilée avec nginx, plus le Core Rule Set OWASP. Les règles sont configurées via ConfigMap, permettant des exclusions par domaine ou par URI sans rebuild d'image.
Varnish Cache
Varnish se positionne entre le WAF et WordPress pour mettre en cache les pages complètes. Le VCL est déployé via ConfigMap avec des règles de purge automatiques déclenchées par les webhooks WordPress lors des publications.
Prometheus Exporters
Deux exporters tournent en sidecars dans les pods WordPress :
- phpfpm-exporter : expose les métriques du pool FPM (processus actifs, idle, queue, slow requests)
- nginx-exporter : expose les métriques nginx (connexions, requêtes par seconde, statuts HTTP)
Gestion des ressources
Chaque namespace dispose d'un ResourceQuota qui impose des limites strictes :
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
namespace: jbsky-fr-production
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "6"
limits.memory: 12Gi
pods: "20"
Point critique : lorsqu'un ResourceQuota est actif dans un namespace, chaque pod DOIT spécifier ses requests et limits CPU/mémoire dans son spec. Sans cela, le pod est rejeté par l'admission controller avec une erreur forbidden: failed quota. C'est un piège classique lors de l'ajout d'un nouveau déploiement.
Stratégie de déploiement
Tous les Deployments utilisent la stratégie RollingUpdate avec :
maxSurge: 1: un pod supplémentaire est créé avant de supprimer l'ancienmaxUnavailable: 0: zéro downtime garanti pendant le rollout- Des readinessProbes HTTP qui vérifient que le nouveau pod répond correctement avant de recevoir du trafic
- Des livenessProbes qui redémarrent automatiquement un pod en état dégradé
Les initContainers jouent un rôle crucial : l'initContainer wordpress-files extrait le core WordPress depuis une image Alpine dédiée vers un volume emptyDir partagé avec le container principal. Ce pattern évite d'embarquer les fichiers WordPress dans l'image PHP-FPM hardened (FROM scratch, pas de shell pour copier des fichiers au runtime).
Backup des données
La stratégie de backup combine deux approches :
- Snapshots Longhorn : programmés quotidiennement, rétention 7 jours
- Backup applicatif : un script exécuté depuis l'hyperviseur Proxmox utilise
kubectl execpour streamer un tar des données critiques directement vers le stockage de sauvegarde :
# Backup WordPress uploads depuis le pod
kubectl exec -n jbsky-fr-production deploy/wordpress --
tar cf - /var/www/html/wp-content/uploads |
gzip > /backup/wordpress-uploads-$(date +%Y%m%d).tar.gz
Ce mécanisme fonctionne même avec les images FROM scratch car le binaire tar est inclus dans le container initContainer qui reste disponible.
ConfigMaps et le piège du subPath
Un gotcha majeur de Kubernetes concerne les ConfigMaps montés avec subPath. Lorsqu'un fichier de configuration est monté via subPath (pour ne pas écraser le répertoire entier), ce fichier est figé à la création du pod. Contrairement aux montages de volumes complets, kubelet ne met PAS à jour automatiquement les fichiers subPath quand le ConfigMap change.
Conséquence pratique : après modification d'un ConfigMap utilisé avec subPath, un kubectl rollout restart deployment/<name> est obligatoire pour que les pods chargent la nouvelle configuration. C'est particulièrement piégeux car :
- Le ConfigMap apparaît bien mis à jour dans
kubectl describe configmap - Mais le fichier à l'intérieur du pod conserve l'ancienne version
- Aucune erreur n'est remontée — le pod tourne avec une config stale silencieusement
Gotcha : imagePullPolicy et cache d'images
Le runner Kubernetes utilise par défaut imagePullPolicy: IfNotPresent. Cela signifie que si une image avec un tag donné existe déjà dans le cache local du nœud, Kubernetes ne la re-télécharge PAS, même si une version plus récente a été poussée sur le registry avec le même tag.
Symptôme : vous déployez une correction d'image, le pod redémarre, mais l'ancien comportement persiste car le nœud utilise l'image en cache.
Solution : toujours bumper le tag de l'image lors d'un rebuild (ex : 1.30.2 → 1.30.3). Ne jamais réutiliser un tag existant pour une image modifiée. Mettre à jour le tag dans les manifestes Kubernetes en même temps que le push de la nouvelle image.
Conclusion
K3s démontre qu'un cluster Kubernetes production-ready ne nécessite ni des dizaines de nœuds ni une infrastructure complexe. Un seul nœud bien configuré, avec une isolation par namespaces, un stockage fiable (Longhorn), un ingress moderne (Traefik + Gateway API) et des images hardened, suffit à héberger un ensemble complet de services web avec les mêmes garanties de fiabilité et de sécurité qu'un cluster multi-nœuds. La clé réside dans la rigueur des ResourceQuotas, la stratégie de déploiement zero-downtime et la compréhension des subtilités comme le comportement des ConfigMaps subPath et le cache d'images.
Laisser un commentaire