Lección 6 — Exploitation en production

Mise à jour progressive et rollback

7 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

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 :

terminal
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 inference

Simuler 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) :

terminal
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 --wait

helm 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

terminal
make down        # kind delete cluster + vérification
make clean       # idem si le cluster existe encore, + caches et images locales

make down échoue (code de retour non nul) si le cluster existe encore après suppression.

Ce qui casse en production

SymptômeCauseParade
helm upgrade bloqué puis context deadline exceeded, service toujours en ancienne versionnouveau 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éploiementpas de preStop : SIGTERM arrive avant le retrait de l'IngresspreStop.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 suivantla CI redéploie l'étiquette cassée depuis la branche principalecorriger 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éegarder au moins 5 révisions (défaut Helm : 10)
Mise à jour lente avec 6 podsmaxSurge: 1 remplace un pod à la foismaxSurge: 25% si le cluster a la place ; jamais maxUnavailable > 0 pour un service sans marge

La règle de décision

  • maxUnavailable: 0 pour tout service dont la capacité est juste ; maxSurge selon la place libre du cluster.
  • --wait --timeout toujours ; --atomic en 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 ?

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 ?

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
terminal
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"
wait

Avec 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 ms

L'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
0

Je sais faire

  • Régler maxSurge et maxUnavailable selon 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