Chaos Engineering : je casse mon cluster exprès


Mon homelab marchait bien. Trop bien. Et c’est exactement ça le problème.

Quand tout fonctionne, tu ne sais pas ce qui se passe quand ça casse. Tu ne sais pas si tes alertes marchent, si tes dashboards affichent les bonnes métriques, si ton agent IA sait vraiment réparer les trucs.

Alors j’ai décidé de tout casser. Volontairement. Méthodiquement.

Le principe du Chaos Engineering

Netflix a popularisé le concept avec Chaos Monkey : un programme qui kill des instances de production au hasard. L’idée : si ton système survit à des pannes aléatoires, il survivra aux vraies pannes.

Les principes :

  1. Définir l’état normal (les métriques de base)
  2. Formuler une hypothèse (“si je kill ce pod, le service reste disponible”)
  3. Injecter la panne
  4. Observer ce qui se passe
  5. Analyser les résultats
  6. Améliorer ce qui a cassé

Sur un homelab single-node, on ne peut pas tester la perte d’un node. Mais on peut tester beaucoup d’autres choses.

Expérience 1 : Kill un pod au hasard

Hypothèse

Kubernetes devrait redémarrer automatiquement n’importe quel pod killé, grâce aux Deployments et leur replicas spec.

L’injection

# Lister les pods running
kubectl get pods -A --field-selector=status.phase=Running -o json | \
  jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name)"' | \
  shuf -n 1

Résultat : monitoring/prometheus-server-6d4b8c9f5-x7k2p

OK, pas le plus facile pour commencer. On kill Prometheus.

kubectl delete pod prometheus-server-6d4b8c9f5-x7k2p -n monitoring --grace-period=0

Observation

Ce qui s’est passé :

  • T+0s : Pod terminé
  • T+1s : Kubernetes crée un nouveau pod (prometheus-server-6d4b8c9f5-m3n9q)
  • T+3s : Nouveau pod en ContainerCreating
  • T+8s : Nouveau pod en Running
  • T+15s : Prometheus scrappe de nouveau les métriques

Trou dans les métriques : ~15 secondes sans collecte. Grafana affiche un gap dans les graphes.

Alerte déclenchée ? Non. AlertManager n’a pas eu le temps de réagir (le for: 1m dans mes alertes exige 1 minute de problème continu).

Verdict

Kubernetes fait le job. Mais 15 secondes de trou, ça veut dire que si ça arrive pendant un incident réel, on perd des données de monitoring. Pour un service critique, il faudrait 2 replicas minimum.

Expérience 2 : Saturer la mémoire d’un namespace

Hypothèse

Les ResourceQuotas et LimitRanges devraient empêcher un pod gourmand de crasher les autres.

L’injection

Un pod qui consomme toute la RAM :

apiVersion: v1
kind: Pod
metadata:
  name: memory-bomb
  namespace: test-chaos
spec:
  containers:
  - name: stress
    image: progrium/stress
    args: ["--vm", "1", "--vm-bytes", "2G", "--timeout", "60s"]
    resources:
      limits:
        memory: "512Mi"  # Limite à 512Mi
      requests:
        memory: "256Mi"

Le container demande 2GB mais est limité à 512Mi.

kubectl apply -f memory-bomb.yaml

Observation

Ce qui s’est passé :

  • T+0s : Pod démarré
  • T+2s : Le processus stress commence à allouer de la mémoire
  • T+3s : Le container atteint la limite de 512Mi
  • T+3s : OOMKilled — Kubernetes kill le container immédiatement
  • T+4s : Le pod redémarre (RestartPolicy: Always)
  • T+5s : Re-OOMKilled
  • T+15s : CrashLoopBackOff

Impact sur les autres pods ? Aucun. Les resource limits ont parfaitement fonctionné. Les autres pods du namespace n’ont rien vu.

Dans Grafana : Le dashboard montre un spike de mémoire suivi d’un crash, puis la boucle de restart. Exactement ce qu’on attend.

Et sans resource limits ?

J’ai retesté sans le champ resources.limits :

kubectl run memory-bomb-unlimited --image=progrium/stress \
  --namespace=test-chaos -- --vm 1 --vm-bytes 4G --timeout 60s

Résultat : Le container a consommé 4GB de RAM. D’autres pods du même node ont commencé à ralentir. Kubernetes a fini par OOMKill le pod le plus gourmand (le nôtre), mais ça a pris plus de temps et ça a impacté les voisins.

Verdict

Toujours mettre des resource limits. C’est pas optionnel. Sans ça, un pod peut affecter tout le node.

Expérience 3 : Corrompre un PVC

Hypothèse

Si les données d’un PersistentVolumeClaim sont corrompues, l’application devrait échouer proprement et Velero devrait pouvoir restaurer.

L’injection

Écrire des données corrompues dans le PVC de Redis :

# Trouver le pod Redis
kubectl get pods -n console -l app=redis

# Écrire du garbage dans le fichier de dump
kubectl exec -n console redis-0 -- sh -c \
  'echo "CORRUPTED_DATA" > /data/dump.rdb'

# Forcer un restart
kubectl delete pod redis-0 -n console

Observation

Ce qui s’est passé :

  • T+0s : Redis redémarre
  • T+1s : Redis tente de charger dump.rdb
  • T+1s : Bad file format reading the append only file: make a backup
  • T+2s : Redis refuse de démarrer → CrashLoopBackOff

Impact : Toute la console web est inaccessible (pas de session, pas de cache, pas de quiz).

Restauration Velero :

# Lister les backups
velero backup get

# Restaurer juste le PVC de Redis
velero restore create redis-restore \
  --from-backup daily-full-20260218 \
  --include-namespaces console \
  --include-resources persistentvolumeclaims,persistentvolumes \
  --selector app=redis

Temps de restauration : ~2 minutes. Redis a redémarré proprement avec les données d’avant la corruption.

Verdict

Velero fonctionne. Mais 2 minutes de downtime pour Redis = 2 minutes de console inaccessible. Pour un vrai environnement de prod, il faudrait un Redis Sentinel ou un cluster Redis.

Expérience 4 : Supprimer un Deployment entier

Hypothèse

Si quelqu’un fait un kubectl delete deployment accidentel, ArgoCD ou un GitOps devrait restaurer automatiquement. Mais moi, j’ai pas encore de GitOps…

L’injection

# Supprimer le deployment de la console frontend
kubectl delete deployment console-ui -n console

Observation

Ce qui s’est passé :

  • T+0s : Deployment supprimé, pods terminés
  • T+5s : La console web est inaccessible
  • T+5s : Nginx Proxy Manager retourne 502 Bad Gateway

Restauration ? Pas de GitOps, donc pas de restauration automatique.

Il faut redéployer manuellement :

# Rebuild et redéployer
cd web-console && ./build-and-deploy.sh --frontend-only

Temps de restauration : ~3 minutes (build + push + deploy).

Ou bien restaurer avec Velero :

velero restore create console-restore \
  --from-backup daily-full-20260218 \
  --include-namespaces console \
  --include-resources deployments \
  --selector app=console-ui

Temps : ~30 secondes. Beaucoup plus rapide.

Verdict

C’est ici que le manque de GitOps se fait sentir. Avec ArgoCD, le Deployment serait recréé automatiquement en quelques secondes. Sans ça, on dépend des backups Velero ou d’un redéploiement manuel.

Expérience 5 : Couper le réseau d’un pod

Hypothèse

Si un pod perd la connectivité réseau, les health checks devraient détecter le problème et Kubernetes devrait le redémarrer.

L’injection

On ne peut pas couper le réseau d’un pod directement sans Network Policies. Mais on peut simuler en bloquant le trafic sortant :

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: block-egress
  namespace: console
spec:
  podSelector:
    matchLabels:
      app: console-api
  policyTypes:
  - Egress
  egress: []  # Aucun trafic sortant autorisé
kubectl apply -f block-egress.yaml

Observation

Ce qui s’est passé :

  • T+0s : NetworkPolicy appliquée
  • T+1s : Le backend console ne peut plus joindre Redis, ni l’API K8s
  • T+5s : Les requêtes HTTP échouent (timeout Redis)
  • T+10s : La liveness probe échoue (si elle dépend d’une connexion Redis)
  • T+30s : Kubernetes redémarre le pod
  • T+31s : Le pod redémarre… mais ne peut toujours pas joindre Redis (la NetworkPolicy est encore là)
  • T+60s : CrashLoopBackOff

Intéressant : le pod redémarre en boucle parce que la cause est externe (réseau bloqué). Kubernetes ne peut pas résoudre ça tout seul.

# Supprimer la NetworkPolicy pour restaurer
kubectl delete networkpolicy block-egress -n console

Le pod a redémarré normalement après ça.

Verdict

Les liveness probes fonctionnent, mais elles ne peuvent pas distinguer “le code a crashé” de “le réseau est coupé”. Dans les deux cas, Kubernetes redémarre le pod, ce qui ne résout rien si le problème est réseau.

Expérience 6 : Tester l’agent IA en chaos

Hypothèse

Mon agent IA devrait être capable de diagnostiquer et réparer les pannes créées par les expériences précédentes.

L’injection

Je kill un pod de la console et je demande à l’agent :

"Le pod console-api crash, répare-le"

Observation

L’agent a :

  1. Utilisé kubectl_get pour voir les pods → trouvé le pod en CrashLoopBackOff
  2. Utilisé analyze_pod_failure → identifié “Back-off restarting failed container”
  3. Utilisé get_pod_logs → lu les logs du dernier crash
  4. Identifié le problème (connexion Redis refusée, à cause de ma NetworkPolicy encore active)
  5. Utilisé kubectl_get pour lister les NetworkPolicies
  6. Identifié block-egress comme la cause
  7. Recommandé de supprimer la NetworkPolicy

Temps total : 25 secondes. 6 tool calls, 3 itérations Claude.

Il n’a pas supprimé la NetworkPolicy lui-même (action destructive → demande confirmation), mais il a diagnostiqué correctement et proposé la solution.

Verdict

L’agent fonctionne comme prévu pour le troubleshooting. Il suit le pattern analyse → diagnostic → proposition de fix. Le garde-fou “demander confirmation pour les suppressions” a bien fonctionné.

Le tableau de bord chaos

Après toutes ces expériences, voici le récapitulatif :

ExpérienceHypothèseRésultatAction nécessaire
Kill pod randomK8s redémarreOK — 15s de downtimeAjouter des replicas pour les services critiques
Saturer la RAMLes limits protègentOK — OOMKill immédiatToujours mettre des limits
Corrompre PVCVelero restaureOK — 2 min de restoreBackup plus fréquent pour Redis
Supprimer DeploymentRestauration autoFAIL — Pas de GitOpsImplémenter ArgoCD
Couper le réseauHealth checks détectentPARTIEL — Redémarre en boucleAjouter des readiness probes réseau
Agent IA en chaosDiagnostic autoOK — 25s de diagnosticRien, ça marche

Ce que j’ai amélioré après

1. Resource Limits partout

J’ai audité tous mes Deployments. Ceux qui n’avaient pas de limits en ont maintenant :

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

2. Liveness et Readiness probes

Ajouté des probes sur tous les services qui n’en avaient pas :

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 15
readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

La distinction est importante :

  • Liveness : “est-ce que le process tourne ?” → si non, redémarrer
  • Readiness : “est-ce que le service est prêt à recevoir du trafic ?” → si non, retirer du Service

3. Alertes plus rapides

J’ai réduit le for de certaines alertes critiques :

# Avant
- alert: PodCrashLooping
  expr: rate(kube_pod_container_status_restarts_total[5m]) > 0
  for: 5m   # Trop long !

# Après
- alert: PodCrashLooping
  expr: rate(kube_pod_container_status_restarts_total[5m]) > 0
  for: 1m   # Réagir plus vite

4. La liste “à faire”

Les expériences ont révélé des manques :

  • ArgoCD pour le GitOps (restauration automatique des Deployments)
  • Redis Sentinel pour la haute disponibilité de Redis
  • NetworkPolicies par défaut pour isoler les namespaces
  • Backup Redis toutes les heures au lieu de toutes les 6h

Comment reproduire chez vous

Si vous avez un homelab K8s, voici un script simple pour commencer :

#!/bin/bash
# chaos-monkey-lite.sh - Kill un pod random toutes les X minutes

INTERVAL=${1:-300}  # 5 minutes par défaut
EXCLUDE_NS="kube-system|kube-public|velero"

while true; do
    # Trouver un pod random (hors namespaces critiques)
    VICTIM=$(kubectl get pods -A --field-selector=status.phase=Running -o json | \
        jq -r --arg exclude "$EXCLUDE_NS" \
        '.items[] | select(.metadata.namespace | test($exclude) | not) | "\(.metadata.namespace) \(.metadata.name)"' | \
        shuf -n 1)

    if [ -n "$VICTIM" ]; then
        NS=$(echo $VICTIM | awk '{print $1}')
        POD=$(echo $VICTIM | awk '{print $2}')
        echo "$(date): Killing pod $POD in namespace $NS"
        kubectl delete pod "$POD" -n "$NS" --grace-period=0
    fi

    sleep $INTERVAL
done

Lancez-le, ouvrez Grafana, et observez. C’est fascinant de voir comment le cluster réagit.

Ce que j’ai appris

1. On ne sait jamais si ça marche tant qu’on a pas testé. Mes alertes avaient l’air bien sur le papier. En pratique, certaines ne se déclenchaient jamais parce que le for était trop long.

2. Les resource limits ne sont pas optionnels. Un pod sans limits, c’est une bombe à retardement.

3. Le chaos engineering, c’est pas que pour Netflix. Même sur un homelab single-node, ça révèle des problèmes qu’on n’aurait jamais trouvés autrement.

4. GitOps manque cruellement. C’est le plus gros trou dans mon setup. Sans source de vérité déclarative, une suppression accidentelle demande une intervention manuelle.

5. L’observabilité rend le chaos utile. Sans Prometheus + Grafana + Loki, le chaos engineering c’est juste casser des trucs. Avec l’observabilité, c’est une méthode scientifique.


Le chaos engineering m’a rendu beaucoup plus confiant dans mon homelab. Pas parce que tout est parfait, mais parce que je sais exactement ce qui casse et comment le réparer.

Et le prochain gros chantier est clair : ArgoCD pour le GitOps. Quand ton cluster peut se reconstruire tout seul à partir d’un repo Git, tu dors vraiment tranquille.