Lab 9.1 : Probes¶
| Durée | 60 à 75 min |
| Niveau | Guidé |
| Prérequis | Cours du chapitre 9 lu, notions de ConfigMap (chapitre 4), de Service (chapitre 3) et de rolling update (chapitre 2), cluster Ready |
| Acquis visés | AA5 |
!!! abstract "Objectifs" - Constater qu'un Pod sans sonde peut recevoir du trafic alors qu'il ne peut pas servir - Configurer une sonde readiness et observer le retrait du Pod des endpoints - Configurer une sonde liveness et observer le redémarrage du conteneur - Observer qu'une sonde readiness bloque une mise à jour défectueuse - Utiliser une sonde startup pour une application lente à démarrer - Utiliser les mécanismes tcpSocket et exec, et définir les sondes d'une base PostgreSQL
Contexte et schéma¶
Vous déployez une petite application dont vous pouvez provoquer les pannes en supprimant des fichiers « interrupteurs » dans le conteneur. Deux interrupteurs : l'un décide si l'application peut servir des requêtes, l'autre si elle est vivante.
flowchart LR
C["Client<br/>(Pod de test)"] --> S["Service app"]
S -- "Pods prêts seulement" --> P["Pod app"]
K["kubelet"] -- "/ready" --> P
K -- "/health" --> P
P --- F["/flags/ready.flag<br/>/flags/health.flag"]
| Chemin | Comportement |
|---|---|
/ et /ready |
200 si /flags/ready.flag existe, sinon 503 |
/health |
200 si /flags/health.flag existe, sinon 500 |
Étapes¶
Étape 1 : Préparer le namespace et la configuration¶
k create namespace tp-k8s
k config set-context --current --namespace=tp-k8s
cat <<'EOF' > probe-conf.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: probe-conf
data:
default.conf: |
server {
listen 80;
default_type text/plain;
location = / {
root /flags;
try_files /ready.flag =503;
}
location = /ready {
root /flags;
try_files /ready.flag =503;
}
location = /health {
root /flags;
try_files /health.flag =500;
}
}
EOF
k apply -f probe-conf.yaml
!!! success "Point de contrôle" - [ ] Le ConfigMap probe-conf existe
Étape 2 : Une application sans sonde¶
Déployez l'application (2 réplicas) et un Service, sans aucune sonde :
cat <<'EOF' > app-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 2
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: web
image: nginx:1.26-alpine
env:
- name: PRET
value: "oui"
command: ["sh", "-c", "mkdir -p /flags; [ \"$PRET\" = oui ] && echo 'application en service' > /flags/ready.flag; echo ok > /flags/health.flag; exec nginx -g 'daemon off;'"]
volumeMounts:
- name: conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
volumes:
- name: conf
configMap:
name: probe-conf
---
apiVersion: v1
kind: Service
metadata:
name: app
spec:
selector:
app: app
ports:
- port: 80
EOF
k apply -f app-v1.yaml
k rollout status deployment/app
k get pods -o wide
k get endpoints app
Mesurez les réponses du Service avec dix requêtes :
k run test --rm -it --image=busybox:1.36 --restart=Never -- \
sh -c 'for i in 1 2 3 4 5 6 7 8 9 10; do wget -q -T 2 -O /dev/null http://app && echo OK || echo ERREUR; done'
Tout est OK. Faites maintenant « tomber » l'application sur un Pod, en supprimant son interrupteur, puis recommencez :
POD=$(k get pods -l app=app -o jsonpath='{.items[0].metadata.name}')
echo $POD
k exec $POD -- rm /flags/ready.flag
k exec $POD -- wget -q -T 2 -O- http://localhost; echo
k get pods
k get endpoints app
k run test --rm -it --image=busybox:1.36 --restart=Never -- \
sh -c 'for i in 1 2 3 4 5 6 7 8 9 10; do wget -q -T 2 -O /dev/null http://app && echo OK || echo ERREUR; done'
Le Pod défaillant reste 1/1 et dans les endpoints : environ la moitié des requêtes échouent.
!!! success "Point de contrôle" - [ ] Avec deux Pods sains, les dix requêtes réussissent - [ ] Le Pod défaillant reste READY 1/1 et présent dans k get endpoints app - [ ] Une partie des requêtes (ERREUR) échouent
Étape 3 : Ajouter une sonde readiness¶
Ajoutez une sonde readiness sur /ready :
sed 's| volumeMounts:| readinessProbe:\n httpGet:\n path: /ready\n port: 80\n periodSeconds: 5\n failureThreshold: 2\n volumeMounts:|' app-v1.yaml > app-v2.yaml
grep -A7 readinessProbe app-v2.yaml
k apply -f app-v2.yaml
k rollout status deployment/app
k get pods
Dans un second terminal, surveillez : k get pods -w. Dans le premier, faites tomber un Pod, puis observez :
POD=$(k get pods -l app=app -o jsonpath='{.items[0].metadata.name}')
k exec $POD -- rm /flags/ready.flag
sleep 12
k get pods
k get endpoints app
k describe pod $POD | grep -E "Readiness|Warning" | tail -n 3
k run test --rm -it --image=busybox:1.36 --restart=Never -- \
sh -c 'for i in 1 2 3 4 5 6 7 8 9 10; do wget -q -T 2 -O /dev/null http://app && echo OK || echo ERREUR; done'
Le Pod passe à 0/1 (sans redémarrer), disparaît des endpoints, et toutes les requêtes réussissent. Rétablissez l'application :
k exec $POD -- sh -c "echo 'application en service' > /flags/ready.flag"
sleep 8
k get pods
k get endpoints app
!!! success "Point de contrôle" - [ ] Le Pod défaillant est passé à READY 0/1 avec RESTARTS à 0 - [ ] Il a disparu des endpoints du Service et les dix requêtes ont réussi - [ ] Les événements mentionnent Readiness probe failed avec le code 503 - [ ] Après la remise du fichier, le Pod est revenu à 1/1 et dans les endpoints
Étape 4 : Ajouter une sonde liveness¶
Ajoutez une sonde liveness sur /health :
sed 's| volumeMounts:| livenessProbe:\n httpGet:\n path: /health\n port: 80\n periodSeconds: 5\n failureThreshold: 3\n volumeMounts:|' app-v2.yaml > app-v3.yaml
k apply -f app-v3.yaml
k rollout status deployment/app
Dans le second terminal : k get pods -w. Dans le premier, supprimez l'interrupteur de vie d'un Pod :
POD=$(k get pods -l app=app -o jsonpath='{.items[0].metadata.name}')
k exec $POD -- rm /flags/health.flag
sleep 25
k get pods
k describe pod $POD | grep -E "Liveness|Killing|Started" | tail -n 5
Après environ 15 secondes (trois échecs espacés de 5 secondes), le conteneur est redémarré : la colonne RESTARTS passe à 1. Comme les fichiers sont recréés au démarrage du conteneur, l'application redevient saine.
!!! success "Point de contrôle" - [ ] RESTARTS est passé à 1 pour le Pod concerné - [ ] Les événements mentionnent un échec de la sonde liveness et le redémarrage du conteneur - [ ] Le Pod est de nouveau 1/1
Étape 5 : La readiness protège une mise à jour¶
Simulez une nouvelle version défectueuse : l'application démarre mais n'est jamais prête (la variable PRET à non empêche la création de l'interrupteur) :
sed 's|value: "oui"|value: "non"|' app-v3.yaml > app-v4.yaml
k apply -f app-v4.yaml
sleep 30
k rollout status deployment/app --timeout=20s
k get pods
k get endpoints app
k get replicaset
La mise à jour reste bloquée : le nouveau Pod est 0/1 et les anciens Pods restent disponibles et dans les endpoints. Vérifiez que le service répond toujours, puis revenez en arrière :
k run test --rm -it --image=busybox:1.36 --restart=Never -- \
sh -c 'for i in 1 2 3 4 5; do wget -q -T 2 -O /dev/null http://app && echo OK || echo ERREUR; done'
k rollout undo deployment/app
k rollout status deployment/app
k get pods
!!! success "Point de contrôle" - [ ] Le nouveau Pod est resté 0/1 et la mise à jour ne s'est pas terminée - [ ] Les anciens Pods ont continué à répondre (aucun ERREUR) - [ ] Après rollout undo, les deux Pods sont 1/1
Étape 6 : Une application lente à démarrer¶
Ce Pod attend 40 secondes avant de lancer nginx, et sa sonde liveness est la même que précédemment :
cat <<'EOF' > lent-v1.yaml
apiVersion: v1
kind: Pod
metadata:
name: lent-v1
spec:
containers:
- name: web
image: nginx:1.26-alpine
command: ["sh", "-c", "sleep 40; mkdir -p /flags; echo ok > /flags/ready.flag; echo ok > /flags/health.flag; exec nginx -g 'daemon off;'"]
livenessProbe:
httpGet:
path: /health
port: 80
periodSeconds: 5
failureThreshold: 3
volumeMounts:
- name: conf
mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
volumes:
- name: conf
configMap:
name: probe-conf
EOF
k apply -f lent-v1.yaml
sleep 60
k get pod lent-v1
k describe pod lent-v1 | grep -E "Liveness|Killing" | tail -n 3
La sonde liveness échoue avant la fin du démarrage : le conteneur est tué avant d'être opérationnel, et RESTARTS augmente sans fin. Ajoutez une sonde startup qui laisse jusqu'à 60 secondes au démarrage :
k delete pod lent-v1
sed -e 's|name: lent-v1|name: lent-v2|' -e 's| livenessProbe:| startupProbe:\n httpGet:\n path: /health\n port: 80\n periodSeconds: 5\n failureThreshold: 12\n livenessProbe:|' lent-v1.yaml > lent-v2.yaml
k apply -f lent-v2.yaml
sleep 60
k get pod lent-v2
!!! success "Point de contrôle" - [ ] lent-v1 a redémarré plusieurs fois et n'est jamais devenu prêt - [ ] lent-v2 est 1/1 avec RESTARTS à 0 - [ ] Vous pouvez expliquer le rôle de failureThreshold × periodSeconds dans la sonde startup
Étape 7 : Les mécanismes tcpSocket et exec, et une base PostgreSQL¶
Jusqu'ici, vous avez utilisé httpGet. Deux Pods nginx testent les deux autres mécanismes, avec des sondes volontairement erronées :
cat <<'EOF' > mecanismes.yaml
apiVersion: v1
kind: Pod
metadata:
name: tcp-ko
spec:
containers:
- name: web
image: nginx:1.26-alpine
readinessProbe:
tcpSocket:
port: 81
periodSeconds: 5
---
apiVersion: v1
kind: Pod
metadata:
name: exec-ko
spec:
containers:
- name: web
image: nginx:1.26-alpine
readinessProbe:
exec:
command: ["cat", "/tmp/absent"]
periodSeconds: 5
EOF
k apply -f mecanismes.yaml
sleep 20
k get pods tcp-ko exec-ko
k describe pod tcp-ko | grep -E "Unhealthy" | tail -n 1
k describe pod exec-ko | grep -E "Unhealthy" | tail -n 1
k delete -f mecanismes.yaml
Les deux Pods restent 0/1 avec RESTARTS à 0 : la sonde tcpSocket échoue (connection refused, le port 81 n'est pas ouvert) et la sonde exec échoue (code de sortie non nul). Passez à une base de données : sa readiness utilise pg_isready, sa liveness un simple test de port.
cat <<'EOF' > postgres.yaml
apiVersion: v1
kind: Pod
metadata:
name: pg
spec:
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_USER
value: appuser
- name: POSTGRES_PASSWORD
value: ChangeMe-123
- name: POSTGRES_DB
value: appdb
readinessProbe:
exec:
command: ["pg_isready", "-h", "127.0.0.1", "-U", "appuser", "-d", "appdb"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
tcpSocket:
port: 5432
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 3
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 512Mi
EOF
k apply -f postgres.yaml
k get pod pg -w
Observez le passage de 0/1 à 1/1, puis quittez avec Ctrl+C et nettoyez :
Pourquoi -h 127.0.0.1 ?
Au premier démarrage, PostgreSQL lance d'abord un serveur temporaire qui n'écoute que sur un socket local, pour initialiser la base. Sans -h, pg_isready interroge ce socket et peut répondre « prêt » trop tôt. Avec -h 127.0.0.1, la sonde ne réussit qu'une fois le serveur définitif à l'écoute en TCP.
!!! success "Point de contrôle" - [ ] tcp-ko et exec-ko sont 0/1, sans redémarrage, avec un message d'échec différent pour chacun - [ ] Le Pod pg passe à 1/1 une fois la base prête - [ ] Vous pouvez expliquer pourquoi la liveness de la base est un tcpSocket, et sa readiness un pg_isready
Questions de réflexion¶
Pourquoi le Pod sans sonde recevait-il du trafic alors qu'il ne pouvait pas servir ?
Sans sonde readiness, Kubernetes considère le Pod prêt dès que son conteneur tourne. Il reste donc dans les endpoints du Service, qui lui envoie des requêtes.
Pourquoi le Pod n'a-t-il pas redémarré quand la sonde readiness a échoué ?
Une sonde readiness agit sur le trafic, pas sur le cycle de vie du conteneur : elle retire le Pod des endpoints. Seule la sonde liveness provoque un redémarrage.
Pourquoi les anciens Pods sont-ils restés disponibles pendant la mise à jour défectueuse ?
Le Deployment n'avance que lorsque le nouveau Pod est prêt. Sa sonde readiness échouait : l'ancien ReplicaSet a donc conservé ses Pods, et le Service n'a envoyé du trafic qu'à eux.
Pourquoi lent-v1 redémarrait-il sans fin, et comment la sonde startup règle-t-elle le problème ?
La liveness commençait à tester dès le démarrage du conteneur et échouait avant la fin des 40 secondes d'attente. La sonde startup suspend la liveness (et la readiness) jusqu'à son premier succès, en tolérant jusqu'à failureThreshold × periodSeconds secondes.
Quelle sonde choisiriez-vous pour vérifier la connexion à la base de données, et pourquoi pas l'autre ?
La readiness : sans base, le Pod cesse de recevoir du trafic, sans être redémarré. Une liveness dépendante de la base redémarrerait toutes les instances inutilement.
Dépannage¶
| Symptôme | Pistes |
|---|---|
Pod CrashLoopBackOff dès l'étape 2 |
k logs <pod> : erreur dans la configuration nginx (ConfigMap) |
READY 0/1 après ajout de la readiness |
k describe pod : chemin ou port de la sonde incorrect, ready.flag absent |
sed n'a rien modifié |
Indentation différente : ouvrir le fichier app-v2.yaml et vérifier la présence du bloc readinessProbe |
| La requête du Pod de test échoue immédiatement | Service sans endpoints : k get endpoints app ; les deux Pods sont-ils prêts ? |
Le Pod ne redémarre pas après la suppression de health.flag |
Attendre 15 à 20 secondes ; vérifier que la liveness est bien présente dans le Pod (k describe pod) |
rollout status ne s'interrompt pas |
Ajouter --timeout=20s ; la mise à jour défectueuse est bloquée par conception |
k exec $POD : Pod introuvable |
Un Pod a été remplacé : relire POD=$(k get pods ...) |
lent-v2 redémarre |
Délai de démarrage plus long que failureThreshold × periodSeconds : augmenter failureThreshold |
pg en 0/1 pendant plusieurs dizaines de secondes |
Initialisation de la base en cours : patienter ; sinon k logs pg |
pg en CrashLoopBackOff |
k logs pg : variables d'environnement manquantes ou incorrectes |
exec : executable file not found |
La commande de la sonde n'existe pas dans l'image : vérifier avec k exec <pod> -- which <commande> |