Mise à jour progressive et rollback
La coupure de quatre minutes du prototype venait d'un docker stop suivi d'un docker run. Sur Kubernetes, une mise à jour ne coupe rien si trois conditions tiennent ensemble : la stratégie ne retire jamais un pod prêt avant que son remplaçant le soit, chaque pod qui part finit ses requêtes, et une version cassée ne remplace jamais la version qui marche. Cette leçon le vérifie sous charge, avec une mise à jour réussie, une version cassée et un retour arrière, puis supprime le cluster.
Le mécanisme
stateDiagram-v2 [*] --> R5: "helm upgrade 0.2.0 (révision 5)" R5 --> Surge: "maxSurge 1 : un pod 0.2.0 en plus" Surge --> Ready: "readiness OK" Ready --> Remove: "un pod 0.1.0 retiré (preStop 5 s puis SIGTERM)" Remove --> Surge: "pod suivant" Remove --> Deployed: "tous les pods en 0.2.0" Deployed --> R6: "helm upgrade 0.3.0 (image absente)" R6 --> Stuck: "pod 0.3.0 en ImagePullBackOff, jamais prêt" Stuck --> Failed: "--wait --timeout 45s dépassé : révision 6 failed" Failed --> R7: "helm rollback : révision 7 = copie de la 5" R7 --> [*]
Avec maxUnavailable: 0, le Deployment ne supprime un ancien pod qu'après qu'un nouveau a passé sa readiness. Un nouveau pod qui ne devient jamais prêt bloque donc la mise à jour au premier pod : les anciens continuent de servir. C'est le comportement voulu, mais il ne prévient personne. Le --wait --timeout de Helm transforme ce blocage silencieux en échec explicite.
La mise en œuvre
Construire la nouvelle version et la déployer :
make image deploy TAG=0.2.0
kubectl --context kind-lab-k8s-ia -n assistant rollout status deploy/inference
helm --kube-context kind-lab-k8s-ia -n assistant history inferenceSimuler l'erreur la plus fréquente, une étiquette d'image qui n'existe pas (faute de frappe, image pas encore poussée par la CI) :
make deploy TAG=0.3.0 # échoue après 3 minutes : Error: UPGRADE FAILED: context deadline exceeded
kubectl --context kind-lab-k8s-ia -n assistant get pods
helm --kube-context kind-lab-k8s-ia -n assistant rollback inference --waithelm rollback sans numéro revient à la révision précédente ; il crée une nouvelle révision (7), copie de la 5. L'historique reste lisible : qui a déployé quoi, ce qui a échoué, d'où l'on est revenu.
Pendant toute la séquence, loadtest.py envoyait 8 clients en continu (backend fake, 5 ms de CPU par requête) pour compter chaque erreur.
Nettoyage
make down # kind delete cluster + vérification
make clean # idem si le cluster existe encore, + caches et images localesmake down échoue (code de retour non nul) si le cluster existe encore après suppression.
Ce qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
helm upgrade bloqué puis context deadline exceeded, service toujours en ancienne version | nouveau pod jamais prêt (image absente, Secret manquant, sonde qui échoue) | kubectl describe pod sur le pod récent, puis helm rollback |
| 502 pendant chaque déploiement | pas de preStop : SIGTERM arrive avant le retrait de l'Ingress | preStop.sleep de 5 s, terminationGracePeriodSeconds > durée max d'une requête |
| Rollback qui « ne marche pas » : la version cassée revient au déploiement suivant | la CI redéploie l'étiquette cassée depuis la branche principale | corriger la source (revert du commit) ; le rollback n'est qu'un pansement |
Rollback impossible : release has no 5 version | --history-max trop bas, ou release réinstallée | garder au moins 5 révisions (défaut Helm : 10) |
| Mise à jour lente avec 6 pods | maxSurge: 1 remplace un pod à la fois | maxSurge: 25% si le cluster a la place ; jamais maxUnavailable > 0 pour un service sans marge |
La règle de décision
maxUnavailable: 0pour tout service dont la capacité est juste ;maxSurgeselon la place libre du cluster.--wait --timeouttoujours ;--atomicen déploiement automatique.- Le rollback Helm rend le service en quelques secondes ; le correctif durable est un revert dans Git. Le projet suivant (GitOps) fait de Git la seule source.
- Le rolling update remplace tout le monde dès que les sondes passent. Une version qui répond 200 mais répond faux passera : c'est la limite que corrige le canary avec analyse automatique (projet suivant).
Pratique
Un helm upgrade vers 0.3.0 échoue après le délai. kubectl get pods montre 4 pods 0.2.0 Running et 3 pods 0.3.0 en ImagePullBackOff. Les utilisateurs se plaignent-ils ?
Un pod qui n'a pas passé sa readiness ne reçoit aucun trafic. Avec maxUnavailable: 0, aucun pod 0.2.0 n'a été retiré tant qu'aucun 0.3.0 n'était prêt. La mesure du jour le confirme : 0 erreur sur 68 993 requêtes. Il faut quand même agir (rollback), car la capacité de surge est occupée par des pods inutiles.
Vous faites helm rollback vers la révision 5 un vendredi soir. Lundi, la CI redéploie depuis main et la version cassée revient. Quelle était la bonne suite au rollback ?
Le rollback modifie le cluster, pas la source de vérité. La CI rejoue la source et réintroduit la version cassée. Corriger la source est la seule réparation durable ; c'est précisément ce que formalise le GitOps. Supprimer une révision efface la trace de l'incident, désactiver la CI bloque toute l'équipe.
Exercice
Mesurez vous-même la disponibilité pendant une mise à jour : lancez le test de charge en arrière-plan, déployez une nouvelle étiquette, et comptez les erreurs. Puis recommencez après avoir mis preStopSleepSeconds: 0 et maxUnavailable: 1. Que constatez-vous ?
Voir la solution
make deploy VALUES="--set config.LLM_BACKEND=fake --set config.FAKE_CPU_MS=5"
.venv/bin/python scripts/loadtest.py --url http://assistant.127.0.0.1.nip.io:8081 --concurrency 8 --duration 90 &
sleep 15
make image deploy TAG=0.2.1 VALUES="--set config.LLM_BACKEND=fake --set config.FAKE_CPU_MS=5"
waitAvec les réglages du chart, le compteur doit rester à zéro erreur. Avec preStopSleepSeconds: 0, quelques requêtes peuvent échouer à chaque pod retiré, selon la vitesse de propagation des Endpoints vers NGINX (le nombre varie d'un essai à l'autre, d'où l'intérêt de répéter la mesure). Avec maxUnavailable: 1 sur 2 pods, la capacité est divisée par deux pendant la mise à jour : la latence p95 monte, et le délestage peut renvoyer des 503 si la charge est forte.
Résultat obtenu
Exécution du 2026-10-15, sous charge continue (8 clients) :
== 20:54:46 upgrade 0.2.0
STATUS: deployed
REVISION: 5
durée : 9 s
== 20:55:05 upgrade 0.3.0 (image absente)
Error: UPGRADE FAILED: context deadline exceeded
NAME READY STATUS RESTARTS AGE
inference-5795897865-g689b 1/1 Running 0 7s
inference-5795897865-nsrvw 1/1 Running 0 22s
inference-5795897865-t6tnm 1/1 Running 0 65s
inference-5795897865-vh7m9 1/1 Running 0 60s
inference-5fc459dddd-2d8f7 0/1 ImagePullBackOff 0 46s
inference-5fc459dddd-9v2l5 0/1 ErrImagePull 0 7s
inference-5fc459dddd-c4pjs 0/1 ImagePullBackOff 0 22s
== 20:55:52 rollback
Rollback was a success! Happy Helming!
5 superseded Upgrade complete
6 failed Upgrade "inference" failed: context deadline exceeded
7 deployed Rollback to 5
requêtes : 68993 codes : {200: 68993} pods distincts : 9
latence p50 19 ms, max 4533 msL'HPA est monté de 2 à 6 pods pendant la séquence (d'où les 4 pods 0.2.0) : mise à jour, échec et rollback se sont superposés à un changement d'échelle, sans une seule erreur.
Nettoyage :
$ make down
Deleting cluster "lab-k8s-ia" ...
Deleted nodes: ["lab-k8s-ia-worker" "lab-k8s-ia-control-plane" "lab-k8s-ia-worker2"]
Cluster lab-k8s-ia supprimé.
$ make clean
Artefacts supprimés.
$ kind get clusters
No kind clusters found.
$ docker images lab-k8s-ia/inference -q | wc -l
0Je sais faire
- Régler
maxSurgeetmaxUnavailableselon la marge de capacité - Reconnaître une mise à jour bloquée et revenir en arrière avec
helm rollback - Mesurer la disponibilité pendant un déploiement plutôt que la supposer
- Supprimer le cluster et vérifier qu'il n'en reste rien