Articles

Un socle de production pour les workloads Kubernetes

Requests, probes, disruption budgets, arrêt propre et sécurité des pods : les réglages qu'un Deployment de production devrait avoir avant son premier incident.

Les exemples sont génériques et illustratifs. Ils ne proviennent d'aucun environnement client : adaptez seuils et réglages à votre propre système.

Pourquoi un socle

Kubernetes exécute presque n'importe quoi avec presque aucune configuration, et c'est bien le problème : les valeurs par défaut sont pensées pour démarrer, pas pour tenir. Les réglages ci-dessous coûtent peu le premier jour et cher lorsqu'on les découvre pendant une panne. Ils concernent les Deployments sans état ; les workloads avec état en demandent davantage.

Requests et limits

Le scheduler place les pods selon leurs requests, et l'Horizontal Pod Autoscaler mesure l'utilisation par rapport à elles. Un pod sans requests est placé à l'aveugle et ne peut pas être mis à l'échelle sur le CPU.

La mémoire n'est pas compressible : un conteneur qui dépasse sa limite mémoire est tué. Fixez la limite mémoire égale ou proche de la request, pour qu'un pod ne soit jamais planifié sur de la mémoire qu'il ne peut pas garder. Le CPU est différent — un conteneur au-delà de sa limite CPU est bridé (throttling), ce qui se traduit par de la latence. Beaucoup d'équipes définissent des requests CPU et omettent la limite CPU pour les services sensibles à la latence ; quel que soit votre choix, mesurez le throttling.

Trois probes, trois rôles

  • La readiness décide si le pod reçoit du trafic. Attention aux dépendances partagées : si tous les pods échouent à la readiness en même temps, le service n'a plus aucun endpoint.
  • La liveness décide si le conteneur est redémarré. Limitez-la au processus lui-même : si elle vérifie la base de données, une panne de la base redémarre tous les pods d'un coup.
  • La startup retient les autres probes pendant le démarrage d'une application lente, au lieu de gonfler le délai de la liveness.

Survivre aux drains de nœuds et aux pannes de zone

Les mises à jour de nœuds et l'autoscaling évincent des pods en permanence. Un PodDisruptionBudget limite le nombre de réplicas qu'une interruption volontaire peut arrêter à la fois, et les topology spread constraints répartissent les réplicas sur plusieurs zones, pour que la panne d'une zone n'emporte pas tout le service.

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api
spec:
  maxUnavailable: 1
  selector:
    matchLabels: { app: api }

Arrêt propre

Quand un pod est supprimé, son retrait des endpoints du service et l'arrêt de ses conteneurs (le hook preStop, puis SIGTERM) commencent en même temps. Une application qui s'arrête immédiatement perd les requêtes qu'on lui route encore. Ajoutez une courte pause en preStop pour que les load balancers se mettent à jour, traitez SIGTERM en terminant le travail en cours, et gardez terminationGracePeriodSeconds supérieur à la pause plus le drainage.

Contexte de sécurité et images

Exécutez avec un utilisateur non root, retirez toutes les capabilities Linux, bloquez l'escalade de privilèges et montez le système de fichiers racine en lecture seule ; le Pod Security Standard restricted impose l'essentiel de cela par namespace. Référencez les images par digest plutôt que par un tag mutable, pour que ce qui a été testé soit exactement ce qui tourne.

Tout assembler

Un Deployment avec le socle appliqué. Les valeurs sont indicatives : dimensionnez les requests à partir de l'usage mesuré, pas de cet exemple.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels: { app: api }
  template:
    metadata:
      labels: { app: api }
    spec:
      terminationGracePeriodSeconds: 45
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: { app: api }
      securityContext:
        runAsNonRoot: true
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: api
          image: registry.example.com/api@sha256:<digest>
          resources:
            requests: { cpu: 250m, memory: 256Mi }
            limits: { memory: 256Mi }
          readinessProbe:
            httpGet: { path: /ready, port: 8080 }
            periodSeconds: 5
          livenessProbe:
            httpGet: { path: /live, port: 8080 }
            periodSeconds: 10
            failureThreshold: 3
          startupProbe:
            httpGet: { path: /live, port: 8080 }
            periodSeconds: 5
            failureThreshold: 30
          lifecycle:
            preStop:
              sleep: { seconds: 5 }   # native sleep action, Kubernetes 1.30+
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }
Contact

Besoin d'un second avis sur votre installation ?

Envoyez une courte description de votre système. Un ingénieur senior répond par écrit sous un jour ouvré.

Parlons de votre infrastructure