درس 4 — Exploitation en production

Sondes, ressources et sécurité

8 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

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

SondeQuestionEn cas d'échecRéglage ici
startuple processus a-t-il fini de démarrer ?les deux autres sondes attendent2 s × 30 = 60 s maximum
livenessest-il bloqué ?redémarrage du conteneurtoutes les 10 s, 3 échecs
readinesspeut-il servir maintenant ?retrait du Service, sans redémarragetoutes 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

files/chart/inference/templates/deployment.yaml
      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

files/chart/inference/templates/deployment.yaml
          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.

files/chart/inference/templates/deployment.yaml
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway

NetworkPolicy

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.

terminal
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-worker

Ce qui casse en production

SymptômeCauseParade
kubectl drain bloqué indéfiniment : Cannot evict pod as it would violate the pod's disruption budgetPDB 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 picsliveness timeoutSeconds: 1 sur un pod bridé par sa limite CPUtimeout de 2 s, sonde sans travail, limite CPU réaliste
OOMKilled après quelques heureslimite mémoire fixée au jugémesurer (kubectl top pods) sous charge, ajouter 30 % de marge
Read-only file system au démarrageune bibliothèque écrit un cache dans $HOME ou à côté du codemonter 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 sortierè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 ?

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 ?

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
terminal
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
done

Le 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-worker2

Le 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 /tmp inscriptible
  • Protéger un drain avec un PDB et vérifier une NetworkPolicy par un test positif et négatif
التالي