Aller au contenu
English
Cloud medium

Monter un cluster Kapsule de version, sous trafic, et mesurer ce que ça coûte

Mesuré live le ·fr-par·scw 2.62.0

75 min de lecture

logo Scaleway

Monter un cluster Kapsule de version prend un peu plus de sept minutes et coûte deux requêtes perdues sur 249. Cette leçon monte un cluster de 1.36.4 à 1.37.0 pendant qu'une application reçoit du trafic en continu, et mesure chaque étape. Vous verrez que le control plane et les nœuds se montent séparément, que le pool remplace ses machines au lieu de les mettre à jour, et que le PodDisruptionBudget cadence l'opération plutôt que de la bloquer. Vous saurez aussi écrire une attente qui ne déclare pas la montée terminée trop tôt.

  • Monter le control plane puis les nœuds, en sachant pourquoi c'est séparé.
  • Mesurer la disponibilité réelle de votre application pendant l'opération.
  • Reconnaître le remplacement roulant, et ce qu'il implique pour la capacité.
  • Écrire une attente qui ne se trompe pas de critère.
  • Un cluster Kapsule sur une version antérieure à la dernière disponible.
  • kubectl, la CLI scw 2.62.0, jq et curl.
  • La lecture de créer un cluster Kapsule, dont ce lab prolonge le cluster.

Jusqu'où peut-on monter, et quand faut-il le faire ?

Section intitulée « Jusqu'où peut-on monter, et quand faut-il le faire ? »

On ne saute pas une version mineure. La cible doit être un correctif de la version courante, ou la mineure immédiatement suivante. Passer de 1.35 à 1.37 exige donc deux montées successives.

Fenêtre de terminal
CLUSTER_ID=$(scw k8s cluster list region=fr-par -o json | jq -r '.[0].id')
scw k8s cluster get "${CLUSTER_ID}" region=fr-par -o json \
| jq -r '"cluster : \(.version) (\(.status))"'
scw k8s version list region=fr-par -o json \
| jq -r '.[] | "\(.name) dépréciée \(.deprecated_at[0:10]) fin \(.end_of_life_at[0:10])"'

La première sortie doit afficher votre version et ready, la seconde une ligne par version encore proposée, avec ses deux échéances.

Les dates décident du calendrier à votre place. Scaleway supporte chaque mineure 14 mois, puis monte lui-même les clusters restants vers la mineure suivante, dans les 30 jours, en prévenant par ticket. Choisir de ne rien faire, c'est donc choisir que la montée se produise à un moment que vous ne maîtrisez pas.

L'argument upgrade-pools=true enchaîne le control plane et les nœuds. Lisez ce tableau par la colonne du milieu : elle dit dans quelle situation chaque stratégie est la bonne.

StratégieQuand la choisirCe que vous acceptez
upgrade-pools=true, tout d'un coupcluster de développement, fenêtre de maintenance courteune seule opération longue, sans point de contrôle intermédiaire
Control plane seul, puis pools plus tardproduction : valider les API avant de toucher aux nœudsvivre quelques jours avec un écart de version, ce que Kubernetes autorise
Pool neuf à la version cible, puis basculecharges sensibles, retour arrière souhaitépayer deux pools en parallèle le temps de la migration

La troisième mérite d'être connue, parce qu'elle restaure le retour arrière que la montée en place supprime. Scaleway la documente : au lieu de monter le pool existant, on en crée un neuf directement à la version cible, on y déplace les charges, puis on supprime l'ancien. Tant que les deux pools coexistent, revenir en arrière revient à replanifier les pods sur l'ancien.

Sans sonde continue, vous ne saurez jamais ce que la montée a coûté à vos utilisateurs. Déployez une application à plusieurs répliques, protégée par un budget de disruption.

Fenêtre de terminal
kubectl create deployment web \
--image=nginx:1.29.1@sha256:8adbdcb969e2676478ee2c7ad333956f0c8e0e4c5a7463f4611d7a2e7a7ff5dc \
--replicas=4
kubectl create poddisruptionbudget web-pdb --selector=app=web --min-available=2
kubectl expose deployment web --port=80 --target-port=80 --type=LoadBalancer
kubectl rollout status deployment/web --timeout=300s

La sortie doit finir par deployment "web" successfully rolled out.

Attendez ensuite une vraie réponse, et non l'attribution d'une adresse : le Load Balancer reçoit son adresse avant que ses backends n'aient passé leurs contrôles de santé.

Fenêtre de terminal
ADRESSE=$(kubectl get svc web \
-o jsonpath='{.status.loadBalancer.ingress[0].ip}{.status.loadBalancer.ingress[0].hostname}')
until [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "http://${ADRESSE}/")" = "200" ]; do
printf '.'; sleep 5
done
echo " service joignable"

La sortie doit finir par service joignable. Lancez alors la sonde, qui tournera pendant toute l'opération :

Fenêtre de terminal
( while :; do
curl -s -o /dev/null -w '%{http_code}\n' --max-time 3 "http://${ADRESSE}/" >> codes-http.txt
sleep 1
done ) &
PID_SONDE=$!

Elle ne touche pas à vos nœuds, et c'est le premier fait à comprendre.

Fenêtre de terminal
scw k8s cluster upgrade "${CLUSTER_ID}" region=fr-par version=1.37.0 -o json | jq -r '.status'
until [ "$(scw k8s cluster get "${CLUSTER_ID}" region=fr-par -o json | jq -r '.status')" = "ready" ]; do
printf '.'; sleep 15
done
echo " control plane monté"

La première sortie doit afficher updating, la dernière control plane monté. Mesuré le 2026-09-12 : 77 secondes.

Regardez maintenant l'écart que cette opération vient de créer :

Fenêtre de terminal
scw k8s cluster get "${CLUSTER_ID}" region=fr-par -o json | jq -r '"control plane : \(.version)"'
kubectl get nodes -o 'custom-columns=NOM:.metadata.name,KUBELET:.status.nodeInfo.kubeletVersion'

La sortie doit afficher un control plane en 1.37.0 et des nœuds encore en v1.36.4.

Cet écart est normal et supporté. Kubernetes autorise un control plane en avance sur ses kubelets, ce qui rend la montée en deux temps possible : vous pouvez monter le control plane un jour et les nœuds un autre, en connaissance de cause.

Ils ne le sont pas : ils sont remplacés, un par un. C'est le comportement à connaître avant de lancer l'opération, parce qu'il décide de la capacité disponible pendant la montée.

Fenêtre de terminal
POOL_ID=$(scw k8s pool list cluster-id="${CLUSTER_ID}" region=fr-par -o json \
| jq -r '.[0].id')
scw k8s pool upgrade "${POOL_ID}" region=fr-par version=1.37.0 -o json | jq -r '.status'

La sortie doit afficher upgrading. Observé le 2026-09-12 sur un pool de deux nœuds : un nœud neuf apparaît, l'ancien passe en deleting, et le second remplacement ne démarre qu'après le premier. Pendant l'opération, les quatre pods se sont retrouvés sur l'unique nœud disponible.

Le remplacement roulant explique à lui seul les trois contraintes officielles de cette leçon, et il vaut de s'y arrêter. Puisque chaque nœud est détruit puis recréé, tout ce qui vivait sur son disque local disparaît avec lui, d'où l'avertissement de Scaleway sur la perte de données locales. Puisque le pool pilote lui-même le nombre de machines pendant l'opération, il refuse qu'on le redimensionne en parallèle, sous peine de conflit. Et puisque le cluster fonctionne avec une machine de moins à chaque étape, la capacité disponible pendant la montée est celle de N-1 nœuds, pas celle de N. Dimensionner au plus juste revient donc à accepter une saturation garantie à chaque maintenance, ce qui transforme une opération de routine en incident.

Compter les nœuds à la bonne version ne suffit pas, et c'est une erreur que j'ai commise avant de la corriger.

Fenêtre de terminal
TAILLE=$(scw k8s pool get "${POOL_ID}" region=fr-par -o json | jq -r '.size')
until prets=$(kubectl get nodes --no-headers | awk '$2=="Ready"' | wc -l) &&
vieux=$(kubectl get nodes --no-headers -o 'custom-columns=V:.status.nodeInfo.kubeletVersion' \
| grep -cv 'v1.37.0') &&
transit=$(scw k8s node list cluster-id="${CLUSTER_ID}" region=fr-par -o json \
| jq -r '[.[] | select(.status != "ready")] | length') &&
[ "${prets}" = "${TAILLE}" ] && [ "${vieux}" = "0" ] && [ "${transit}" = "0" ]; do
printf '.'; sleep 15
done
echo " montée réellement terminée"

La sortie doit finir par montée réellement terminée. Mesuré : 369 secondes pour deux nœuds.

Deux requêtes perdues sur 249, soit 99,2 % de disponibilité.

Fenêtre de terminal
kill "${PID_SONDE}"
sort codes-http.txt | uniq -c | sort -rn
echo "total : $(wc -l < codes-http.txt) requêtes"

La sortie doit afficher la répartition des codes. Mesuré le 2026-09-12 :

247 200
2 000
total : 249 requêtes

Un code 000 signale une requête que curl n'a pas pu mener à terme, le plus souvent une connexion coupée pendant que le Load Balancer retirait un backend.

Il cadence la montée, il ne la bloque pas. Observé en direct pendant le drain du premier nœud :

Fenêtre de terminal
kubectl get pdb web-pdb -o 'custom-columns=MIN:.spec.minAvailable,SAINS:.status.currentHealthy,EXIGE:.status.desiredHealthy,EVICTIONS:.status.disruptionsAllowed'

La sortie affichait 2 2 2 0 : zéro éviction autorisée, puisque le nombre de pods sains égalait exactement le minimum exigé. Le drain a repris de lui-même à mesure que les pods recréés devenaient sains.

Kapsule n'a jamais eu à forcer. C'est important, parce qu'il en aurait le pouvoir : max-termination-grace-period, réglable à la création du pool, vaut 15 minutes par défaut et passe outre votre budget au-delà. Un budget trop strict ne protège donc pas indéfiniment, il retarde.

Vérifiez enfin que le stockage a traversé l'épreuve :

Fenêtre de terminal
kubectl get nodes -o 'custom-columns=NOM:.metadata.name,ETAT:.status.conditions[-1].type,KUBELET:.status.nodeInfo.kubeletVersion'
kubectl get pvc -o 'custom-columns=NOM:.metadata.name,ETAT:.status.phase'

La sortie doit afficher tous les nœuds en v1.37.0 et les PersistentVolumeClaim toujours Bound. Mesuré : le volume a survécu au remplacement complet des deux nœuds.

PlafondValeurSource
Saut de version autoriséune mineure, ou un correctifaide de scw k8s cluster upgrade
Retour arrièreimpossibledoc de montée de version
Redimensionnement pendant la montéeinterditdoc de montée de version
Fenêtre de support d'une mineure14 moispolitique de support des versions
Montée imposée après la fin de supportdans les 30 jourspolitique de support des versions
Délai avant drain forcé15 min par défaut, 1 h au maximumaide de scw k8s pool create

Les deux premiers symptômes portent la valeur exacte renvoyée par l'API.

SymptômeCauseSolution
Le cluster répond updating alors que version affiche déjà la ciblele champ de version bascule avant la fin de l'opérationattendre sur status, jamais sur version
Le pool répond upgrading et un nœud manque dans kubectl get nodesremplacement roulant : l'ancien est parti, le neuf n'est pas enregistrécombiner nombre, version et status != "ready" côté API
Le drain d'un nœud n'avance plusle PodDisruptionBudget autorise zéro évictionaugmenter les répliques, ou assouplir min-available
Des données ont disparu après la montée des nœudselles étaient sur le disque local d'un nœud remplacépasser par un PersistentVolumeClaim

Ces quatre erreurs traitent la montée comme une mise à jour en place. C'en est une destruction suivie d'une reconstruction.

AntipatternConséquenceDiscipline
Monter sans sonde continueon ne saura jamais ce que l'opération a coûté aux utilisateursmesurer les codes HTTP pendant toute la durée
Dimensionner le cluster au plus justesaturation garantie à chaque maintenance, puisque la capacité tombe à N-1prévoir la marge d'un nœud
Poser un budget de disruption très strict et l'oublierle drain traîne, puis Kapsule force après 15 minutesaccorder min-available au nombre réel de répliques
Attendre la fin du support pour monterScaleway monte à votre place, au moment qu'il choisitrelever deprecated_at et planifier

La montée de version sous l'angle Well-Architected

Section intitulée « La montée de version sous l'angle Well-Architected »

Quelle capacité reste-t-il pendant la maintenance ? Celle de N-1 nœuds, puisque le pool détruit une machine avant d'en fournir une autre. Mesuré : les quatre répliques se sont retrouvées sur un seul nœud.

Discipline : dimensionner pour N-1, poser un PodDisruptionBudget cohérent avec le nombre de répliques, et placer les données sur des volumes persistants plutôt que sur le disque local.

Votre attente mesure-t-elle la fin de l'opération, ou un état transitoire qui lui ressemble ? Un critère à une seule condition a rendu ici un chiffre inférieur de 46 % à la réalité.

Discipline : combiner plusieurs conditions dont au moins une côté fournisseur, et se méfier d'une mesure qui tombe plus courte qu'attendu.

Le volet Kubernetes se clôt sur l'opération la plus redoutée, et elle est mesurable.

Ce qui a été prouvéMesure
Le control plane monte seul, et vite77 secondes
Les nœuds ne suivent pas : ils sont remplacés369 secondes pour deux nœuds
Le service survit à l'opération247 codes 200 sur 249, soit 99,2 %
Le volume persistant ne bouge pasBound de bout en bout

Ce qui a résisté, et c'est le plus instructif : la mesure elle-même. Une attente qui ne compte que les nœuds à la version cible conclut à 198 secondes, soit 46 % de moins que la réalité. Pendant le remplacement roulant, un seul nœud peut être enregistré, déjà à la bonne version : le compte de vieux nœuds tombe à zéro et la boucle croit avoir fini.

Ce qui dément l'intuition : le champ version du cluster bascule avant la fin de l'opération. C'est status qui fait foi, et se fier au premier fait annoncer une montée terminée qui ne l'est pas.

Dépendances de destruction : le cluster emporte ses nœuds, mais pas les répartiteurs créés par ses services, ni les volumes dont la classe de stockage retient le volume. Les deux se vérifient séparément après la suppression du cluster.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Le control plane et les nœuds se montent séparément : après le premier, les kubelets restent en arrière, et c'est supporté.
  • Mesuré le 2026-09-12 : control plane en 77 s, remplacement roulant de deux nœuds en 369 s.
  • Le pool remplace ses nœuds, il ne les met pas à jour : tout ce qui vit sur un disque local disparaît.
  • La capacité tombe à N-1 pendant toute l'opération.
  • 247 codes 200 sur 249 : l'application reste disponible, à deux requêtes près.
  • Le PodDisruptionBudget cadence le drain, il ne le bloque pas, mais Kapsule peut forcer après 15 minutes.
  • Le champ version bascule avant la fin : c'est status qui fait foi.
  • Une attente doit combiner nombre, version et nœuds en transit, sinon elle conclut trop tôt.
  • On ne saute pas une mineure, et aucun retour arrière n'est possible.
  • Les pools ne se redimensionnent pas pendant la montée : prévoir la capacité avant.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn