Mettre à jour et restaurer des applications

KubernetesBeginner
Pratiquer maintenant

Introduction

Vous savez désormais déployer, exposer et mettre à l’échelle une application. La question opérationnelle suivante est donc la suivante : comment remplacer la version de l’application sans interrompre complètement le service ?

Kubernetes répond à cette question avec une mise à jour progressive. Lorsque le modèle de Pods est modifié, le Deployment crée un nouveau ReplicaSet, démarre progressivement de nouveaux Pods et supprime les anciens uniquement lorsque leurs remplaçants sont disponibles. Pendant toute l’opération, le Service conserve la même adresse stable.

Dans cet exercice, vous ferez passer une application NGINX d’une image explicitement versionnée à une autre, vous relierez les révisions du Deployment aux ReplicaSets et aux Pods, vous provoquerez volontairement une mise en production défectueuse, puis vous restaurerez la dernière révision saine. Vous veillerez également à maintenir le manifeste enregistré cohérent avec l’état réel restauré, une habitude essentielle pour éviter qu’une future commande kubectl apply ne réintroduise l’échec.

Lire l’environnement de départ

Démarrage de l’environnement : Ce lab démarre pour vous un cluster Kubernetes complet. La configuration du plan de contrôle, du nœud et des composants réseau prend généralement 2 à 3 minutes. Veuillez patienter jusqu’à la fin du chargement de l’environnement avant de commencer.

Dans cette étape, vous vérifierez la connexion au cluster et préparerez un client HTTP exécuté dans le cluster.

L’environnement de ce cours contient déjà un cluster Kubernetes opérationnel ; vous n’avez donc pas besoin d’en créer un. Commencez par vérifier où kubectl enverra les commandes. config current-context indique la connexion active, get node consulte l’état du nœud et version affiche les versions du client local et du serveur d’API accessible :

kubectl config current-context
kubectl get node
kubectl version

Le contexte et le nœud portent le nom labex-v135, le nœud est Ready et le serveur indique Kubernetes v1.35.x. Un contexte correspond à la sélection dans le kubeconfig qui relie kubectl à un cluster, un utilisateur et un espace de noms par défaut donnés.

Créez maintenant un petit Pod client. Vous l’utiliserez plus tard pour accéder à l’application via son Service. kubectl run crée un Pod ; --image sélectionne BusyBox, --restart=Never le conserve comme Pod autonome et le séparateur -- introduit sa commande longue durée sleep 3600. kubectl wait attend ensuite au maximum 30 secondes que la condition Ready soit satisfaite :

kubectl run release-client --image=busybox:1.36 --image-pull-policy=IfNotPresent \
  --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/release-client --timeout=30s

Cette méthode sépare le client des Pods web, ce qui correspond davantage à la façon dont une charge de travail appelle une autre charge de travail au sein d’un cluster.

Déployer une base stable

Dans cette étape, vous établirez une révision connue comme saine avant d’effectuer des modifications.

Avant de mettre en pratique une mise à jour, établissez une révision fiable. Créez un manifeste unique contenant un Deployment à trois réplicas et un Service ClusterIP stable.

cd permet d’accéder à l’espace de travail préparé. La syntaxe here-document cat <<'EOF' > release-web.yaml écrit dans le fichier tout le contenu jusqu’au EOF de fermeture. La ligne --- sépare deux objets Kubernetes au sein d’un même fichier YAML :

cd /home/labex/project/update-lab
cat <<'EOF' > release-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: release-web
  annotations:
    kubernetes.io/change-cause: "Initial release: nginx 1.26"
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: release-web
  template:
    metadata:
      labels:
        app: release-web
    spec:
      containers:
        - name: nginx
          image: nginx:1.26-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: release-web
spec:
  selector:
    app: release-web
  ports:
    - name: http
      port: 80
      targetPort: http
EOF
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment,service,pods -l app=release-web

Le Deployment gère les Pods, tandis que le Service les sélectionne indépendamment grâce à leur étiquette. La mise à jour du Deployment ne modifiera pas l’adresse du Service.

Vérifiez la base depuis le client. Dans kubectl exec POD -- COMMAND, -- sépare les arguments de kubectl de la commande exécutée dans le conteneur. wget -qO- récupère le contenu sans affichage superflu vers la sortie standard, puis le tube transmet la réponse à head afin de n’en afficher que le début :

kubectl exec release-client -- wget -qO- http://release-web | head

Publier une nouvelle image de manière déclarative

Dans cette étape, vous modifierez le modèle de Pods enregistré et laisserez le Deployment effectuer une mise à jour progressive.

Un Deployment lance une mise à jour lorsque son modèle de Pods est modifié. L’image faisant partie de ce modèle, sa modification crée une nouvelle révision et un nouveau ReplicaSet.

Mettez à jour à la fois l’image et le motif de changement lisible dans le manifeste enregistré. Chaque substitution sed -i 's/old/new/' file modifie directement le fichier. grep -nE affiche ensuite les numéros de ligne correspondant à l’un ou l’autre des termes de l’expression régulière étendue (change-cause ou image:), ce qui permet de vérifier les deux modifications avant l’application :

cd /home/labex/project/update-lab
sed -i 's/Initial release: nginx 1.26/Release nginx 1.27/' release-web.yaml
sed -i 's/nginx:1.26-alpine/nginx:1.27-alpine/' release-web.yaml
grep -nE 'change-cause|image:' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s

kubectl apply modifie l’état souhaité. Le contrôleur du Deployment agit ensuite de manière asynchrone jusqu’à ce que les trois réplicas mis à jour soient disponibles. Pour obtenir un tableau ciblé des Pods, -l sélectionne l’étiquette de l’application et -o custom-columns associe des en-têtes à des chemins de champs des objets :

kubectl get deployment release-web
kubectl get pods -l app=release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready'

Les trois réplicas actuels utilisent la nouvelle image. Après la réussite de la mise à jour, il est également possible d’apercevoir brièvement un Pod utilisant l’ancienne image et en état Terminating. Ce Pod ne fait plus partie des réplicas souhaités ; Kubernetes termine simplement son arrêt progressif en arrière-plan.

Relier les révisions, les ReplicaSets et les Pods

Dans cette étape, vous inspecterez les objets associés à la mise à jour réussie et vérifierez que le Service est resté disponible.

La mise à jour est terminée, mais comprendre ce qui a changé est plus utile que de voir uniquement le message « réussite ». kubectl rollout history lit les révisions enregistrées du Deployment. La commande get replicasets suivante utilise -l pour conserver les objets associés et custom-columns pour comparer leurs nombres de réplicas et leurs images :

kubectl rollout history deployment/release-web
kubectl get replicasets -l app=release-web \
  -o custom-columns='NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas,IMAGE:.spec.template.spec.containers[0].image'

Vous devriez voir deux ReplicaSets. Le nouveau gère trois Pods ; l’ancien reste à zéro réplica afin que son modèle de Pods demeure disponible pour une restauration. Le nom d’un ReplicaSet contient un hachage dérivé du modèle de Pods, ce qui explique pourquoi les noms des Pods ont changé lorsque l’image a été modifiée.

Vérifiez l’image réellement utilisée et le nombre de backends du Service. Les deux expressions JSONPath utilisent range pour répéter un modèle de sortie sur une liste. Les espaces littéraux et {"\n"} produisent une ligne lisible par Pod ou par point de terminaison :

kubectl get pods -l app=release-web \
  -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.spec.containers[0].image}{"\n"}{end}'
kubectl get endpointslices -l kubernetes.io/service-name=release-web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
kubectl exec release-client -- wget -qO- http://release-web | head

La révision a changé, mais le Service possède toujours trois backends prêts et conserve le même nom stable.

Diagnostiquer une mise en production défectueuse

Dans cette étape, vous introduirez une erreur contrôlée d’image et utiliserez les événements d’un Pod pour déterminer pourquoi la mise à jour ne peut pas aboutir.

Simulez maintenant une erreur courante lors d’une mise en production : une balise d’image de conteneur qui n’existe pas. Conservez cet échec sur le même Deployment afin que le mécanisme de mise à jour puisse démontrer une propriété de sécurité importante. Les deux commandes sed -i modifient directement l’annotation et le texte de l’image. La mise à jour devrait normalement se terminer par un statut d’échec après le délai imparti ; || true permet volontairement à l’exercice de continuer :

cd /home/labex/project/update-lab
sed -i 's/Release nginx 1.27/Broken release: missing image/' release-web.yaml
sed -i 's/nginx:1.27-alpine/nginx:does-not-exist-course/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=20s || true
kubectl get deployment release-web
kubectl get pods -l app=release-web

La mise à jour n’aboutit pas, car un nouveau Pod ne parvient pas à récupérer son image. || true permet à l’exercice de continuer après le délai d’attente prévu.

Trouvez le Pod défaillant et examinez ses événements. $(...) enregistre la sortie de la commande dans BAD_POD. Le premier tube transmet les données JSON de Kubernetes à jq ; select(...) conserve le Pod utilisant l’image défectueuse et -r renvoie son nom sous forme de texte brut. Le second tube vers head -n1 ne conserve qu’un seul nom. Enfin, sed -n '/Events:/,$p' affiche la sortie de describe depuis Events: jusqu’à la fin :

BAD_POD=$(kubectl get pods -l app=release-web \
  -o json | jq -r '.items[] | select(.spec.containers[0].image == "nginx:does-not-exist-course") | .metadata.name' | head -n1)
echo "$BAD_POD"
kubectl describe pod "$BAD_POD" | sed -n '/Events:/,$p'
kubectl get replicasets -l app=release-web

Recherchez ErrImagePull ou ImagePullBackOff. Remarquez que l’ancien ReplicaSet sain possède toujours des Pods disponibles ; le Service peut donc continuer à répondre :

kubectl exec release-client -- wget -qO- http://release-web | head

Cela prouve que le service reste disponible pendant une mise à jour échouée, et non que la nouvelle version fonctionne.

Restaurer la version précédente et réconcilier le manifeste

Dans cette étape, vous restaurerez la dernière révision saine, puis corrigerez le manifeste enregistré.

Le Deployment actif conserve l’historique de ses révisions ; Kubernetes peut donc restaurer le modèle de Pods précédent. rollout undo demande au contrôleur du Deployment de réutiliser la révision précédente ; rollout status attend la fin de la récupération et custom-columns affiche l’image restaurée ainsi que le nombre de réplicas prêts :

kubectl rollout history deployment/release-web
kubectl rollout undo deployment/release-web
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.template.spec.containers[0].image,READY:.status.readyReplicas'

L’image fonctionne de nouveau et trois réplicas sont prêts. rollout undo crée une nouvelle révision à partir d’un ancien modèle de Pods ; il ne rembobine pas le compteur des révisions.

Il reste une tâche importante. Le fichier YAML contient toujours l’image défectueuse ; une future commande kubectl apply casserait donc de nouveau le Deployment. Réconciliez l’état souhaité enregistré avec l’état réel restauré. Les substitutions corrigent le fichier, apply le synchronise et la dernière commande d’historique confirme la nouvelle suite de révisions :

cd /home/labex/project/update-lab
sed -i 's/Broken release: missing image/Rollback to nginx 1.27/' release-web.yaml
sed -i 's/nginx:does-not-exist-course/nginx:1.27-alpine/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
grep -nE 'change-cause|image:' release-web.yaml
kubectl rollout history deployment/release-web

Désormais, le cluster et le fichier décrivent tous deux la même version saine.

Choisir un budget de mise à jour plus sûr

Dans cette étape, vous rendrez explicites la capacité de mise à jour du Deployment et ses limites de disponibilité.

Les paramètres de la stratégie du Deployment contrôlent la capacité temporaire autorisée lors des futures mises à jour :

  • maxUnavailable indique le nombre de réplicas souhaités qui peuvent être indisponibles pendant une mise à jour.
  • maxSurge indique le nombre de Pods supplémentaires pouvant exister temporairement au-delà du nombre de réplicas souhaité.

Pour cette petite application à trois réplicas, exigez que les trois réplicas souhaités restent disponibles et autorisez un Pod supplémentaire.

La commande sed utilise une adresse, /type: RollingUpdate/, pour trouver la ligne de stratégie. Son action a\ ajoute le YAML suivant, réparti sur plusieurs lignes et correctement indenté. Après l’application, JSONPath extrait les deux valeurs de stratégie afin de vérifier la modification sans parcourir l’objet complet :

cd /home/labex/project/update-lab
sed -i '/type: RollingUpdate/a\    rollingUpdate:\n      maxUnavailable: 0\n      maxSurge: 1' release-web.yaml
kubectl apply -f release-web.yaml
kubectl get deployment release-web \
  -o jsonpath='maxUnavailable={.spec.strategy.rollingUpdate.maxUnavailable}{"\n"}maxSurge={.spec.strategy.rollingUpdate.maxSurge}{"\n"}'
kubectl get deployment release-web

La modification des seuls paramètres de stratégie ne remplace pas les Pods, car le modèle de Pods n’a pas changé. Lors d’une future mise à jour de l’image, Kubernetes pourra créer un Pod supplémentaire et ne devrait pas réduire volontairement le nombre de réplicas disponibles en dessous de trois.

Ce réglage privilégie la disponibilité, mais il nécessite une capacité suffisante dans le cluster. Il n’existe pas de valeur universellement idéale : les applications plus importantes utilisent souvent des pourcentages, et le choix réel dépend de la capacité disponible, du temps de démarrage et du niveau d’interruption acceptable.

Résumé

Vous avez suivi une mise en production applicative pendant tout son cycle opérationnel :

  • établi une base saine avec un Deployment et un Service stable ;
  • modifié une image explicitement versionnée de manière déclarative et attendu la mise à jour ;
  • relié les révisions du Deployment aux anciens et nouveaux ReplicaSets ;
  • diagnostiqué un échec de récupération d’image tandis que la version précédente continuait de répondre ;
  • restauré le dernier modèle de Pods sain ;
  • réconcilié le manifeste afin qu’une future application ne puisse pas réintroduire la mauvaise image ; et
  • configuré un budget explicite de disponibilité et de surcharge pour les prochaines mises à jour.

L’idée essentielle est qu’un Deployment gère l’état souhaité dans le temps. Une mise à jour ne se résume pas à modifier une image : il s’agit d’une transition contrôlée entre ReplicaSets, que vous devez observer, vérifier et être prêt à annuler.