Aller au contenu

Module 3 · k8s par la pratique (via k9s)

Objectifs

  • Consolider les objets k8s en les manipulant, pas en les lisant.
  • Debugger les pannes classiques : CrashLoopBackOff, ImagePullBackOff, Pending.
  • Maîtriser scale, rollout, édition à chaud, ConfigMap/Secret depuis k9s.
  • Savoir lancer une tâche qui se termine (Job, CronJob).
  • Durée : ~2 h 30 · Pré-requis : Module 1 et Module 2.

Le cycle de vie d'un pod

graph LR
    P[Pending] --> CC[ContainerCreating] --> R[Running]
    P -.image KO.-> W[ImagePullBackOff]
    R --> S[Succeeded]
    R --> F[Failed]
    R -.le process meurt.-> C[CrashLoopBackOff]
    C -.redémarre.-> R
    R --> T[Terminating] --> X[supprimé]
État affiché Ce que ça veut dire Où regarder
Pending Accepté par l'API, pas encore sur un nœud (ressources ? volume ? scheduling ?) d describe → events
ContainerCreating Nœud choisi, k8s prépare le conteneur : tirage de l'image, montage des volumes events
ImagePullBackOff La préparation coince : image introuvable ou accès refusé describe → events
Running Le(s) conteneur(s) tourne(nt) logs
CrashLoopBackOff Le conteneur démarre puis meurt en boucle ; k8s le relance (délai croissant) logs (--previous)
Succeeded / Failed Le pod s'est terminé (fini avec succès / en erreur) - normal pour un Job logs
Terminating En cours de suppression (SIGTERM, puis SIGKILL après le délai de grâce) events

Phase réelle vs statut affiché

Kubernetes ne connaît que 4 phases : Pending · Running · Succeeded · Failed.

Mais la colonne STATUS de kubectl get pods (et k9s) est plus riche : elle affiche souvent la raison du moment - ContainerCreating, ImagePullBackOff, CrashLoopBackOff, Terminating.

Ce ne sont pas des phases, mais des états de conteneur ou des raisons d'attente. D'où le réflexe : lire les events, qui racontent ce que la phase seule cache.

La règle d'or du debug k8s

describe → events → logs. Dans cet ordre. 90 % des pannes se lisent dans les events.

ImagePullBackOff a deux visages

L'event du describe fait la différence : not found = l'image n'existe pas (tag mal orthographié, le cas du TP) · unauthorized = l'image existe mais le registre est privé - il faut donner au namespace un secret d'accès et le référencer dans le pod (imagePullSecrets). Le secret est par namespace : ça marche dans dev et pas dans prod ? C'est probablement ça.

kubectl create secret docker-registry regcred \
  --docker-server ghcr.io --docker-username moi --docker-password <token>

TP de debug : chercher fait partie de l'exercice

Les TP de panne de ce module ne donnent volontairement pas la commande de correction à l'avance : tu pars du symptôme et tu remontes à la cause. Les gestes nouveaux se voient en séance, et la cheat-sheet kubectl garde la famille kubectl set sous la main.

ConfigMaps & Secrets

Un ConfigMap stocke de la config non sensible ; un Secret des données sensibles (base64).

kubectl create configmap app-config --from-literal COLOR=blue
kubectl create secret generic app-secret --from-literal API_KEY=s3cr3t

Dans k9s : :cm et :secret. Sur un secret, x peut décoder les valeurs (selon version) ; sinon y montre le base64.

Warning

Un Secret k8s n'est pas chiffré par défaut, juste encodé en base64. La « sécurité » vient du RBAC (qui peut le lire) - on en parle au module 4.

graph LR
    CM[ConfigMap<br/>config non sensible] -->|env / envFrom| C[Conteneur du Pod]
    SE[Secret<br/>données sensibles] -->|env| C
    SE -.fichier monté.-> C

L'app ne lit jamais un ConfigMap ou un Secret directement : Kubernetes en injecte le contenu dans le conteneur, en variables d'environnement ou en fichier monté. Même image, config différente selon l'environnement - on découple le code de sa configuration.

Pods multi-conteneurs : init containers & sidecars

Un pod contient le plus souvent un conteneur - mais pas toujours. Deux motifs à connaître, et surtout à ne pas confondre :

Motif Quand tourne-t-il ? Cas d'usage
Init container Avant l'app, jusqu'à terminaison (en séquence) attendre la BDD, migrer un schéma, préparer une config
Sidecar En même temps que l'app, tout du long agent de logs, proxy, rechargement de config
spec:
  initContainers:          # chacun doit réussir avant que l'app démarre
    - name: attend-bdd
      image: busybox
      command: ["sh", "-c", "until nc -z db 5432; do sleep 1; done"]
  containers:              # puis l'app (et ses éventuels sidecars)
    - name: app
      image: monapp:1.0

Le test pour les distinguer : « ce conteneur doit-il se terminer avant que l'app démarre (init), ou vivre avec elle (sidecar) ? ». Dans k9s, les conteneurs d'un pod se voient en drill-down (Entrée sur le pod) - et un statut Init:0/1 signifie « j'attends mes init containers » : encore un état à savoir lire.

graph LR
    I1[init-1] -->|finit| I2[init-2] -->|finit| A[app démarre]
    A -.tournent ensemble.- SC[sidecar]

Les init containers s'exécutent en séquence et doivent tous finir avant que l'app démarre ; le sidecar démarre avec l'app et vit tout du long. Lire le temps de gauche à droite : c'est la frise des deux motifs.

Scaling & rollout

kubectl scale deploy web --replicas 5
kubectl set image deploy/web nginx=nginx:1.27   # déclenche un rolling update
kubectl rollout status deploy/web
kubectl rollout undo deploy/web                 # rollback

Dans k9s, sur un deployment : s pour scaler, r pour restart, l pour suivre les logs pendant le déploiement.

La stratégie derrière le ballet

Par défaut (strategy: RollingUpdate), le remplacement est borné par maxSurge (combien de pods en plus pendant la bascule) et maxUnavailable (combien en moins) - 25 % chacun. L'alternative Recreate tue tout puis recrée : coupure assumée, mais nécessaire quand deux versions ne peuvent pas cohabiter (ex. un volume qu'un seul pod peut monter - module 4b).

Éditer à chaud - e dans k9s, kubectl edit au terminal

Troisième façon de modifier un objet, la plus directe : ouvrir l'objet vivant dans un éditeur, changer un champ, sauvegarder - l'API applique aussitôt.

kubectl edit deploy/web        # ouvre le YAML live ; sauvegarder = appliquer

Dans k9s : e sur n'importe quelle ressource. C'est l'outil du dépannage (corriger une image, un label, une variable, tout de suite) et de l'exploration (voir l'objet complet, avec les champs que tu n'as jamais écrits).

Puissant, mais sans trace

Ce que tu changes par edit n'est écrit nulle part ailleurs que dans le cluster : pas de fichier, pas d'historique, pas de revue. Et certains champs sont immutables (l'éditeur rouvre avec l'erreur en commentaire - lis-la). Dépanner oui, livrer non : c'est tout l'objet du module 3y, et le module 4e te montrera même un robot qui annule tes edits.

Jobs & CronJobs - les pods qui se terminent

Tout ce qu'on a vu jusqu'ici tourne en continu (un serveur web ne « finit » jamais). Mais une migration de schéma, une sauvegarde, un batch de nuit : ces tâches doivent se terminer. C'est le rôle du Job - et un pod en état Completed est alors un succès, pas une panne (c'est l'état Succeeded du schéma du cycle de vie).

Objet Rôle En une phrase
Job la tâche à finir lance un pod et le relance jusqu'au succès (backoffLimit essais)
CronJob la tâche planifiée crée un Job selon un planning cron (0 3 * * * = 3 h du matin)
kubectl create job migration --image busybox:1.36 -- sh -c "echo migration ok"
kubectl create cronjob sauvegarde --image busybox:1.36 --schedule "0 3 * * *" -- date
kubectl create job test-svg --from cronjob/sauvegarde   # déclencher SANS attendre 3 h du matin

Dans k9s : :job et :cronjob. Les logs d'un pod Completed restent lisibles (l) - c'est même tout l'intérêt : le compte rendu de la tâche.

Tip

Un Job utilise restartPolicy: Never ou OnFailure - jamais Always (sinon le pod redémarrerait après le succès… et ta « tâche finie » ne finirait jamais).

Exercices

Exercice - CrashLoopBackOff

kubectl run crash --image busybox:1.36 -- sh -c "echo 'démarrage du worker...'; sleep 2; echo 'FATAL: variable DB_HOST absente'; exit 1"
  1. Dans k9s, observe le pod boucler (RunningErrorCrashLoopBackOff).
  2. Lis les logs (l) : selon le moment, tu ne vois que « démarrage… » (le conteneur vient de renaître). Puis kubectl logs crash --previous : , la vraie cause - FATAL: variable DB_HOST absente.
  3. Explique pourquoi k8s le redémarre en boucle (restartPolicy).

Exercice - ImagePullBackOff

kubectl run fantome --image nexistepas/super-app:9.9.9
  1. d describe → trouve l'event exact.
  2. Corrige : supprime et relance avec nginx.

Exercice - Config injectée

kubectl create configmap demo --from-literal MESSAGE="Salut la sandbox"
kubectl create deployment reader --image nginx:1.27-alpine
kubectl set env deployment/reader --from configmap/demo   # injecte toutes les clés du CM
  1. kubectl exec deploy/reader -- env | grep MESSAGE : la variable injectée est là.
  2. Dans k9s : e sur le deployment reader → repère le bloc envFrom ajouté.

Exercice - Rolling update + rollback

kubectl create deployment site --image nginx:1.25 --replicas 3
kubectl set image deploy/site nginx=nginx:1.27
  1. Suis le rollout dans k9s (les pods se remplacent un à un).
  2. Casse-le volontairement : kubectl set image deploy/site nginx=nginx:nope.
  3. Observe les pods bloqués, puis kubectl rollout undo deploy/site.

Exercice - Édition à chaud

  1. Sur le deployment site, appuie sur e dans k9s.
  2. Trouve spec.replicas et passe-le à 4. Sauvegarde, regarde la liste des pods.
  3. Même geste pour revenir à 3. Tu viens de faire un kubectl edit sans le savoir.

Comment ça s'emboîte

La même app, vue d'ensemble - pas une liste d'objets, un système où chacun se branche :

graph TD
    D[Deployment] -->|maintient| RS[ReplicaSet]
    RS -->|crée / remplace| P[Pods]
    SVC[Service] -->|selector = labels| P
    SVC -->|pods prêts| EP[Endpoints]
    CM[ConfigMap] -.injecté.-> P
    SEC[Secret] -.injecté.-> P

Trois choses à voir : on pilote le Deployment (jamais un Pod nu) ; le Service trouve ses pods par labels (pas par IP) et n'envoie le trafic qu'aux pods prêts listés dans les Endpoints ; la config (ConfigMap/Secret) arrive dans le Pod par injection. Garde cette carte en tête : au module 4r on y ajoutera l'Ingress devant le Service, au 4b le stockage sous le Pod.

Et après ? Du clavier au fichier

Tout ce module est impératif (commandes à la main) : parfait pour explorer et debugger. L'étape suivante - décrire ces mêmes objets dans des manifestes YAML qu'on applique - a son module dédié : Module 3y - Le déclaratif (YAML).

Pratique

Dans le terminal de ta sandbox (à droite), les TP auto-corrigés de ce module - à dérouler une fois la lecture faite :

tp start 02-deploiement      # déploiement, scale, service
tp start 02-rollout          # mise à jour (rolling update), rollback, édition à chaud
tp start 02-batch            # jobs & cronjobs : les pods qui se terminent
tp start 02-debug-image      # debug : un déploiement qui ne démarre pas (ImagePullBackOff)
tp start 02-debug-service    # debug : un service qui ne route nulle part
tp start 02-debug-crashloop  # debug : un pod en CrashLoopBackOff (réflexe logs)

fais la manip puis tp check (ou tp watch pour l'auto-validation) · tp hint / tp solution si tu bloques · tp status pour ta progression.

Ce qu'il faut retenir

  • describe → events → logs : le triptyque du debug.
  • Init container = avant l'app (et doit réussir) · sidecar = à côté de l'app.
  • CrashLoopBackOff = problème applicatif (logs --previous).
  • ImagePullBackOff / Pending = problème plateforme (events du describe).
  • Scale / rollout / rollback : Kubernetes gère le remplacement progressif pour toi.
  • e / kubectl edit dépanne à chaud - mais sans trace : à réserver à l'urgence.
  • Un pod Completed n'est pas une panne : c'est un Job qui a fini son travail.
  • L'impératif sert à explorer ; pour livrer, on passe au déclaratif (module 3y).

Quiz éclair

Teste-toi : réponds de tête, puis déplie.

1. CrashLoopBackOff vs ImagePullBackOff : où regardes-tu d'abord ?

CrashLoopBackOff = problème applicatif → les logs (kubectl logs --previous). ImagePullBackOff = problème plateforme (image/registre) → describe → events.

2. Un pod reste en Pending depuis 5 minutes. Quelles pistes ?

describeEvents : pas de nœud avec assez de ressources, volume non disponible, ou contrainte de scheduling impossible à satisfaire. Pending = problème plateforme.

3. Une mise à jour s'est mal passée. Comment revenir en arrière ?

kubectl rollout undo deploy/<nom> - Kubernetes rebascule sur la révision précédente.

4. Un pod affiche Completed depuis une heure. Tu t'inquiètes ?

Non - Completed signifie que le conteneur a fini avec succès. C'est l'état normal d'un pod de Job (ou d'un init container). Ses logs restent lisibles : c'est le compte rendu de la tâche.

5. Corriger un champ d'un objet immédiatement, sans fichier sous la main ?

e dans k9s ou kubectl edit : l'objet vivant s'ouvre dans l'éditeur, sauvegarder = appliquer. Mais aucune trace hors du cluster - en prod, on répercute ensuite dans le manifeste (module 3y) sous peine de voir le correctif disparaître.

➡️ Suite : Module 2b - k9s approfondi (optionnel, pour gagner en vitesse) · puis Module 3y - Le déclaratif (YAML)