Sondes, ressources et sécurité
Un mardi matin, l'équipe plateforme met à jour les nœuds du cluster : kubectl drain sur chaque nœud, l'un après l'autre. Les deux pods de l'assistant tournaient sur le même nœud ; ils sont évincés ensemble et le service répond 503 pendant 40 secondes. La semaine suivante, un audit signale que n'importe quel pod du cluster peut appeler l'API d'inférence directement, sans passer par l'Ingress, et consommer le budget Gemini. Ces deux incidents se règlent dans le chart, par des objets que Kubernetes applique sans intervention humaine.
Le mécanisme
flowchart TB
subgraph POD["Pod API"]
SP["startupProbe : 2 s × 30 essais"] --> LP["livenessProbe : /health/live"]
SP --> RP["readinessProbe : /health/ready"]
SC["securityContext : UID 10001, FS racine en lecture seule, aucune capability"]
RES["requests 250m / 192Mi · limits 1 CPU / 384Mi"]
end
RP -->|"échec : retiré des Endpoints"| SVC["Service"]
LP -->|"échec : conteneur redémarré"| KL["kubelet"]
DR["kubectl drain"] -->|"éviction"| PDB["PDB minAvailable: 1"]
PDB -->|"refuse si 1 seul pod prêt"| DR
NP["NetworkPolicy"] -->|"entrée : namespace ingress-nginx uniquement"| POD
NP -->|"sortie : DNS + HTTPS public"| OUT["API Gemini"]
Trois sondes, trois rôles
| Sonde | Question | En cas d'échec | Réglage ici |
|---|---|---|---|
| startup | le processus a-t-il fini de démarrer ? | les deux autres sondes attendent | 2 s × 30 = 60 s maximum |
| liveness | est-il bloqué ? | redémarrage du conteneur | toutes les 10 s, 3 échecs |
| readiness | peut-il servir maintenant ? | retrait du Service, sans redémarrage | toutes les 5 s, 2 échecs |
La startup probe évite le compromis classique : une liveness avec un initialDelaySeconds long pour les démarrages lents, qui ralentit aussi la détection des blocages pendant toute la vie du pod.
Requests et limits
Le scheduler place les pods selon les requests ; l'HPA calcule son pourcentage par rapport aux requests ; le noyau applique les limits. Pour ce service :
requests.cpu: 250m: un pod qui attend Gemini consomme peu (3 % mesurés en leçon 5).limits.cpu: 1: au-delà, le conteneur est bridé (throttling), pas tué. Certaines équipes retirent toute limite CPU pour éviter ce bridage ; gardez-en une sur un cluster partagé, sinon un pod emballé prend le CPU des voisins.limits.memory: 384Mi: au-delà, le conteneur est tué (OOMKilled). Mesurez avant de fixer : ici, 53 à 56 Mio par pod au repos (kubectl top pods), la limite laisse une marge pour les pics de réponses longues.
Le securityContext
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile: { type: RuntimeDefault }
...
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
...
volumeMounts:
- { name: tmp, mountPath: /tmp }Ces réglages correspondent au profil restricted des Pod Security Standards. Le seul répertoire inscriptible est /tmp, un emptyDir limité à 64 Mo. Une faille dans une dépendance Python ne peut ni installer d'outil, ni modifier le code, ni devenir root.
Arrêt propre
lifecycle:
preStop:
sleep: { seconds: 5 }À la suppression d'un pod, le retrait des Endpoints et l'envoi de SIGTERM partent en parallèle. Sans pause, uvicorn arrête d'accepter des connexions alors que l'Ingress lui en envoie encore pendant une ou deux secondes. L'action sleep native (activée par défaut depuis Kubernetes 1.30) évite d'avoir besoin d'un binaire sleep dans l'image.
PodDisruptionBudget et répartition
minAvailable: 1 interdit à un drain d'évincer le dernier pod prêt. Il ne protège pas d'une panne de nœud : c'est le rôle de la contrainte de répartition, qui place les pods sur des nœuds différents quand c'est possible.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnywayNetworkPolicy
Dès qu'une NetworkPolicy sélectionne un pod, tout ce qu'elle n'autorise pas est refusé. La politique du chart n'accepte en entrée que le namespace ingress-nginx, et n'autorise en sortie que le DNS du cluster et le port 443 vers des adresses publiques : un pod compromis ne peut pas scanner les bases de données internes. Code : networkpolicy.yaml.
make e2e
kubectl --context kind-lab-k8s-ia drain lab-k8s-ia-worker --ignore-daemonsets --delete-emptydir-data
kubectl --context kind-lab-k8s-ia uncordon lab-k8s-ia-workerCe qui casse en production
| Symptôme | Cause | Parade |
|---|---|---|
kubectl drain bloqué indéfiniment : Cannot evict pod as it would violate the pod's disruption budget | PDB minAvailable: 1 avec un seul pod (HPA désactivé, replicas: 1) | minReplicas ≥ 2 dès qu'un PDB existe |
| Redémarrages en boucle pendant les pics | liveness timeoutSeconds: 1 sur un pod bridé par sa limite CPU | timeout de 2 s, sonde sans travail, limite CPU réaliste |
OOMKilled après quelques heures | limite mémoire fixée au jugé | mesurer (kubectl top pods) sous charge, ajouter 30 % de marge |
Read-only file system au démarrage | une bibliothèque écrit un cache dans $HOME ou à côté du code | monter un emptyDir sur le chemin, ou rediriger le cache vers /tmp |
| Après ajout de la NetworkPolicy, toutes les requêtes Gemini échouent en délai dépassé | DNS non autorisé en sortie | règle d'egress vers kube-dns en UDP et TCP 53 |
La règle de décision
- Les trois sondes sur tout service HTTP ; startup dès que le démarrage dépasse 10 secondes ou varie.
- PDB uniquement avec au moins deux répliques garanties ; sinon il bloque la maintenance au lieu de protéger le service.
- NetworkPolicy par défaut fermée pour tout service qui coûte de l'argent à chaque appel.
Pratique
Vous activez la NetworkPolicy en préproduction. Les pages /health répondent via l'Ingress, mais chaque appel /v1/ask échoue après 30 s avec « délai dépassé ». Que vérifiez-vous en premier ?
Les sondes et /health passent : l'entrée fonctionne. L'appel sortant vers Gemini, lui, a besoin de résoudre le nom (DNS) puis d'ouvrir une connexion 443. Sans règle d'egress correcte, la résolution ou la connexion expire. Un Secret absent donnerait un 503 immédiat de la readiness, pas un délai.
Un nœud est drainé alors que l'HPA a mis le service à 2 pods, tous deux sur ce nœud (topologySpreadConstraints en ScheduleAnyway). Que fait le PDB minAvailable: 1 ?
Le drain utilise l'API d'éviction, qui consulte le PDB. Le premier pod part ; le Deployment en recrée un sur un autre nœud ; quand il est prêt, le budget repasse à 1 éviction autorisée et le second pod part. Le blocage définitif arrive seulement s'il n'y a qu'une réplique. Kubernetes ne migre jamais un pod à chaud.
Exercice
Montrez, avec une commande, qu'un pod du namespace ingress-nginx atteint l'API alors qu'un pod du namespace default ne l'atteint pas.
Voir la solution
for ns in ingress-nginx default; do
kubectl --context kind-lab-k8s-ia -n $ns run np-$ns --rm -i --restart=Never \
--image lab-k8s-ia/inference:0.2.0 --image-pull-policy=IfNotPresent -- \
python -c "import urllib.request as u; print(u.urlopen('http://inference.assistant.svc/health/live', timeout=3).read())" \
2>&1 | head -1
doneLe premier affiche b'{"status":"ok"}', le second se termine par timed out. Le test de make e2e ne vérifie que le refus ; le contrôle positif prouve que le refus vient bien de la politique et pas d'un Service cassé.
Résultat obtenu
Exécution du 2026-10-15 :
$ make e2e
[OK] conteneur non-root : uid=10001
[OK] NetworkPolicy bloque un pod hors Ingress : connexion expirée après 3 s
$ kubectl -n default run np-default ... urlopen('http://inference.assistant.svc/health/live')
TimeoutError: timed out
$ kubectl -n ingress-nginx run np-ingress-nginx ... (même commande)
b'{"status":"ok"}'
$ kubectl --context kind-lab-k8s-ia -n assistant top pods
inference-6ddbf6f4ff-2b6g6 12m 56Mi
inference-6ddbf6f4ff-sz8b2 2m 53Mi
# Drain du nœud qui porte les DEUX pods de l'assistant
$ kubectl --context kind-lab-k8s-ia drain lab-k8s-ia-worker --ignore-daemonsets --delete-emptydir-data
evicting pod assistant/inference-6ddbf6f4ff-2b6g6
evicting pod assistant/inference-6ddbf6f4ff-rr8fm
error when evicting pods/"inference-6ddbf6f4ff-rr8fm" -n "assistant" (will retry after 5s):
Cannot evict pod as it would violate the pod's disruption budget.
evicting pod assistant/inference-6ddbf6f4ff-rr8fm
node/lab-k8s-ia-worker drained # 12 s au total
$ kubectl --context kind-lab-k8s-ia -n assistant get pods -o wide
inference-6ddbf6f4ff-8zqqp 1/1 lab-k8s-ia-worker2
inference-6ddbf6f4ff-c7q22 1/1 lab-k8s-ia-worker2Le PDB a retenu le second pod 5 secondes, le temps que son remplaçant soit prêt sur worker2 : le service n'est jamais descendu sous un pod prêt.
Je sais faire
- Choisir le rôle et le réglage de chacune des trois sondes
- Fixer requests et limits à partir de mesures
- Appliquer un securityContext de profil restricted avec un
/tmpinscriptible - Protéger un drain avec un PDB et vérifier une NetworkPolicy par un test positif et négatif