🛠 Outils IT
🛠 102 outils 📚 53 docs
🤖 Assistant ☀

Kubernetes - Exploitation en production

1. Architecture d'un cluster

Kubernetes est un système de réconciliation : l'utilisateur déclare un état désiré dans l'API, des contrôleurs comparent en boucle cet état à l'état observé et agissent pour faire converger les deux. Comprendre cette mécanique est la clé du dépannage : un objet qui « ne fait rien » est presque toujours un contrôleur qui n'arrive pas à converger, et la raison se trouve dans les events ou dans les logs du contrôleur concerné.

1.1 Control plane

ComposantRôlePoints d'attention en production
kube-apiserverPoint d'entrée unique, authentification, autorisation (RBAC), admission, persistance dans etcdSans état, scalable horizontalement derrière un répartiteur TCP 6443. Seul composant qui parle à etcd.
etcdBase clé-valeur distribuée (Raft) contenant tout l'état du clusterNombre impair de membres (3 ou 5). Très sensible à la latence disque (fsync) : SSD/NVMe obligatoires. Sauvegarde = sauvegarde du cluster.
kube-schedulerAffecte les Pods non planifiés à un nœud (filtrage puis scoring)Actif/passif par élection de leader (Lease dans kube-system).
kube-controller-managerBoucles de contrôle : Deployment, ReplicaSet, Node, Job, EndpointSlice, ServiceAccount...Actif/passif par élection de leader.
cloud-controller-managerIntégration fournisseur (LoadBalancer, routes, nœuds)Absent sur du bare-metal : utiliser MetalLB, kube-vip ou le LB du CNI (Cilium).

1.2 Nœuds de travail

  • kubelet : agent du nœud, reçoit les PodSpecs, pilote le runtime via la CRI, exécute les probes, remonte l'état et gère les évictions sous pression (mémoire, disque, PID).
  • Runtime de conteneurs (CRI) : containerd ou CRI-O. Le dockershim a été supprimé en 1.24 : Docker Engine n'est plus un runtime supporté nativement (cri-dockerd existe mais n'a pas d'intérêt sur un nouveau cluster). Les images construites avec Docker restent parfaitement compatibles (format OCI).
  • kube-proxy : programme les règles de Services (iptables, IPVS ou nftables). Peut être remplacé par Cilium en mode eBPF.
  • CNI (Container Network Interface) : plugin qui attribue une IP à chaque Pod et assure la connectivité inter-nœuds (Calico, Cilium, Flannel...).
  • CSI (Container Storage Interface) : pilotes de stockage hors arbre (Longhorn, Ceph-CSI, pilotes des clouds). Les anciens pilotes « in-tree » ont été migrés vers CSI.

1.3 Flux d'une création de Pod

  1. kubectl apply envoie un Deployment à l'API server : authentification, RBAC, puis chaîne d'admission (mutating puis validating webhooks, Pod Security Admission, ValidatingAdmissionPolicy).
  2. L'objet est persisté dans etcd. Le contrôleur Deployment crée un ReplicaSet, qui crée les Pods (sans nodeName).
  3. Le scheduler choisit un nœud (ressources demandées, affinités, taints/tolerations, topologie) et écrit le binding.
  4. Le kubelet du nœud voit le Pod, demande au CNI une interface réseau, au CSI le montage des volumes, au runtime le pull et le démarrage.
  5. Les probes passent : le Pod devient Ready, son IP est ajoutée aux EndpointSlices des Services correspondants.
EndpointSlices : l'API Endpoints historique est dépréciée depuis 1.33 au profit de EndpointSlice. Pour diagnostiquer un Service sans backend, utiliser kubectl get endpointslices -l kubernetes.io/service-name=mon-service.

2. Choisir une distribution

Le choix dépend moins des fonctionnalités (toutes sont conformes CNCF) que du modèle d'exploitation : qui patche l'OS, qui gère etcd, comment se font les mises à jour, et quelle surface d'attaque on accepte sur les nœuds.

DistributionProfilForcesLimites
kubeadmUpstream « brut », on-premiseRéférence officielle, aucun ajout propriétaire, compréhension complèteTout est à faire : OS, LB d'API, CNI, upgrades nœud par nœud, rotation des certificats
k3sEdge, labs, petits clustersBinaire unique, très léger, etcd embarqué ou SQLite/MySQL/PostgreSQL (kine), Traefik et ServiceLB inclusComposants embarqués à désactiver en prod sérieuse ; moins d'options de durcissement par défaut
RKE2On-premise exigeant (secteur public, conformité)Orienté sécurité (profil CIS, FIPS possible), etcd embarqué, upgrades simples, intégration RancherÉcosystème SUSE/Rancher ; Ingress NGINX fourni par défaut historiquement (à revoir, cf. section 7)
Talos LinuxNœuds immuables pilotés par APIPas de SSH ni de shell, OS minimal en lecture seule, configuration déclarative (talosctl), upgrades atomiquesChangement de paradigme pour les équipes « Linux classique » ; débogage via talosctl et pods de debug
AKS / EKS / GKECloud managéControl plane et etcd gérés, SLA, intégration IAM (Workload Identity), autoscaling de nœudsCoût du LB/egress, contraintes du fournisseur, cadence d'upgrade imposée (support étendu payant)
Règle pratique : en cloud, prendre le service managé sauf contrainte forte. On-premise sur Proxmox ou bare-metal : Talos ou RKE2 pour la production, k3s pour l'edge et les labs, kubeadm quand on veut maîtriser chaque brique (ou pour apprendre).

2.1 Dimensionnement minimal d'un cluster de production

  • 3 nœuds control plane (etcd empilé) : 2 à 4 vCPU, 8 Go RAM, disque SSD avec latence fsync p99 < 10 ms. 5 membres seulement si on doit tolérer 2 pannes simultanées (au prix d'écritures plus lentes).
  • Au moins 3 workers répartis sur des hôtes physiques / zones différents, pour que topologySpreadConstraints et les PodDisruptionBudgets aient un sens.
  • Un point d'entrée API hautement disponible : VIP keepalived + HAProxy, kube-vip, ou LB externe.
  • Des plages réseau qui ne chevauchent aucun réseau existant (VPN, sites distants) : par exemple Pods 10.244.0.0/16, Services 10.96.0.0/12.

3. Installation kubeadm en haute disponibilité

Résumé d'une installation avec etcd empilé (« stacked »), 3 control planes derrière l'adresse k8s-api.example.internal:6443 (VIP 192.0.2.10). Les commandes sont pour Debian/Ubuntu.

3.1 Préparation de chaque nœud

# Modules noyau et sysctl requis par le réseau des Pods
cat <<'EOF' | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay && sudo modprobe br_netfilter

cat <<'EOF' | sudo tee /etc/sysctl.d/99-k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sudo sysctl --system

# Swap : désactiver (sauf support du swap explicitement configuré dans le kubelet)
sudo swapoff -a && sudo sed -i '/\sswap\s/s/^/#/' /etc/fstab

# containerd avec le driver de cgroup systemd (indispensable avec cgroup v2)
sudo apt-get install -y containerd
containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
# Passer SystemdCgroup = true dans la section runc (le chemin de la section
# diffère entre containerd 1.7 et 2.x : vérifier avec grep)
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
grep -n SystemdCgroup /etc/containerd/config.toml
sudo systemctl restart containerd

# Dépôt officiel pkgs.k8s.io (un dépôt PAR version mineure)
K8S_MINOR=v1.34
curl -fsSL https://pkgs.k8s.io/core:/stable:/${K8S_MINOR}/deb/Release.key \
  | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/${K8S_MINOR}/deb/ /" \
  | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update && sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
Driver de cgroup : kubelet et containerd doivent utiliser le même driver (systemd). Une incohérence produit des nœuds instables, des Pods redémarrés sans raison apparente et des erreurs cgroup dans journalctl -u kubelet.

3.2 Initialisation du premier control plane

# kubeadm-config.yaml (API v1beta4, kubeadm 1.31+)
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.34.0          # adapter au patch courant
controlPlaneEndpoint: "k8s-api.example.internal:6443"
networking:
  podSubnet: "10.244.0.0/16"
  serviceSubnet: "10.96.0.0/12"
apiServer:
  certSANs:
    - "k8s-api.example.internal"
    - "192.0.2.10"
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
serverTLSBootstrap: true             # certificats de service du kubelet signés par le cluster
sudo kubeadm init --config kubeadm-config.yaml --upload-certs
mkdir -p ~/.kube && sudo cp /etc/kubernetes/admin.conf ~/.kube/config && sudo chown $(id -u):$(id -g) ~/.kube/config

# Les commandes « kubeadm join » (control plane et worker) sont affichées en fin d'init.
# Pour les régénérer plus tard :
kubeadm token create --print-join-command
sudo kubeadm init phase upload-certs --upload-certs   # nouvelle --certificate-key (valable 2 h)

# Sur les 2 autres control planes :
sudo kubeadm join k8s-api.example.internal:6443 --token <token> \
  --discovery-token-ca-cert-hash sha256:<hash> \
  --control-plane --certificate-key <cle>

# Avec serverTLSBootstrap, approuver les CSR des kubelets (ou déployer un approbateur automatique)
kubectl get csr
kubectl certificate approve <csr-name>

3.3 CNI puis vérifications

# Exemple Cilium via sa CLI (version à épingler)
cilium install --set ipam.mode=kubernetes
cilium status --wait

kubectl get nodes -o wide
kubectl -n kube-system get pods
kubectl get --raw='/readyz?verbose'
sudo kubeadm certs check-expiration
Certificats kubeadm : les certificats clients et serveurs du control plane expirent au bout d'un an. Ils sont renouvelés automatiquement à chaque kubeadm upgrade apply. Un cluster jamais mis à jour cesse de fonctionner au bout de 12 mois : kubeadm certs renew all puis redémarrage des pods statiques. La CA, elle, dure 10 ans.

4. Objets de charge de travail

ObjetUsageParticularités
DeploymentApplications sans étatRolling update via ReplicaSets, rollback (kubectl rollout undo), Pods interchangeables
StatefulSetBases de données, files, clusters à quorumIdentité stable (app-0, app-1), un PVC par réplique (volumeClaimTemplates), démarrage ordonné, Service headless
DaemonSetAgents par nœud (logs, métriques, CNI, CSI)Un Pod par nœud éligible ; tolérer les taints des control planes si nécessaire
JobTraitement ponctuelbackoffLimit, activeDeadlineSeconds, ttlSecondsAfterFinished, mode indexé
CronJobTâches planifiéesconcurrencyPolicy: Forbid, timeZone, startingDeadlineSeconds
ServiceAdresse stable + répartitionClusterIP, NodePort, LoadBalancer, Headless (clusterIP: None), ExternalName
ConfigMap / SecretConfiguration et données sensiblesSecrets en base64 (pas chiffrés !), montés en volume (mis à jour à chaud) ou en variables (figées au démarrage)

4.1 Deployment de référence

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: shop
  labels: { app.kubernetes.io/name: web }
spec:
  replicas: 3
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }
  selector:
    matchLabels: { app.kubernetes.io/name: web }
  template:
    metadata:
      labels: { app.kubernetes.io/name: web }
    spec:
      serviceAccountName: web
      automountServiceAccountToken: false
      terminationGracePeriodSeconds: 30
      securityContext:
        runAsNonRoot: true
        seccompProfile: { type: RuntimeDefault }
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: { app.kubernetes.io/name: web }
      containers:
        - name: web
          image: registry.example.com/shop/web:1.8.2   # jamais :latest
          ports: [{ name: http, containerPort: 8080 }]
          envFrom:
            - configMapRef: { name: web-config }
          resources:
            requests: { cpu: 100m, memory: 256Mi }
            limits:   { memory: 256Mi }
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }
          startupProbe:
            httpGet: { path: /healthz, port: http }
            failureThreshold: 30
            periodSeconds: 2
          readinessProbe:
            httpGet: { path: /ready, port: http }
            periodSeconds: 5
          livenessProbe:
            httpGet: { path: /healthz, port: http }
            periodSeconds: 10
            failureThreshold: 3
          lifecycle:
            preStop:
              sleep: { seconds: 5 }   # laisse le temps aux EndpointSlices de se mettre à jour
          volumeMounts:
            - { name: tmp, mountPath: /tmp }
      volumes:
        - name: tmp
          emptyDir: {}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: web, namespace: shop }
spec:
  minAvailable: 2
  selector:
    matchLabels: { app.kubernetes.io/name: web }
Arrêt propre : à la suppression d'un Pod, le retrait des EndpointSlices et l'envoi du SIGTERM sont concurrents. Sans preStop (ou sans gestion du SIGTERM qui continue à servir quelques secondes), on perd des requêtes à chaque déploiement. L'action preStop.sleep native évite d'avoir besoin d'un binaire sleep dans l'image (utile pour les images distroless).

4.2 CronJob robuste

apiVersion: batch/v1
kind: CronJob
metadata: { name: purge-sessions, namespace: shop }
spec:
  schedule: "15 3 * * *"
  timeZone: "Europe/Paris"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 600
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 1800
      ttlSecondsAfterFinished: 86400
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: purge
              image: registry.example.com/shop/tools:1.8.2
              args: ["purge-sessions", "--older-than=30d"]
              resources:
                requests: { cpu: 50m, memory: 64Mi }
                limits:   { memory: 128Mi }

4.3 Sidecars natifs

Depuis 1.33 (GA), un conteneur déclaré dans initContainers avec restartPolicy: Always est un sidecar natif : il démarre avant les conteneurs applicatifs, reste actif pendant toute la vie du Pod et s'arrête après eux. Cela règle les problèmes historiques des Jobs qui ne se terminaient jamais à cause d'un proxy ou d'un agent de logs encore vivant.

5. Probes, ressources et QoS

5.1 Les trois probes

ProbeQuestion poséeEffet en cas d'échecErreur classique
startupProbeL'application a-t-elle fini de démarrer ?Redémarrage du conteneur après failureThreshold x periodSecondsAbsente sur une appli lente : la liveness la tue pendant son démarrage
readinessProbePeut-elle recevoir du trafic maintenant ?Retrait des EndpointSlices (pas de redémarrage)Vérifier la base de données dans la readiness : une panne SGBD vide tous les backends d'un coup
livenessProbeLe processus est-il bloqué irrémédiablement ?Redémarrage du conteneurTester des dépendances externes : redémarrages en cascade de toute la flotte
Liveness = local uniquement : une liveness ne doit jamais dépendre d'un service externe. Elle doit répondre à « mon processus est-il mort-vivant ? » (deadlock, boucle d'événements bloquée). Dans le doute, mieux vaut ne pas mettre de liveness qu'en mettre une mauvaise.

5.2 Requests, limits et classes QoS

  • requests : ce que le scheduler réserve. Sert au placement et au partage CPU proportionnel (poids cgroup).
  • limits CPU : plafond appliqué par throttling CFS. Provoque des latences invisibles dans les métriques d'utilisation moyenne. Beaucoup d'équipes ne posent pas de limite CPU sur les services sensibles à la latence et s'appuient sur des requests justes.
  • limits mémoire : plafond dur. Dépassement = le noyau tue le processus : OOMKilled (code de sortie 137).
Classe QoSConditionOrdre d'éviction sous pression
Guaranteedrequests = limits pour CPU et mémoire, sur tous les conteneursEn dernier
BurstableAu moins une request ou limit, sans être GuaranteedSelon la consommation au-delà des requests
BestEffortAucune request ni limitEn premier
# Identifier un OOMKilled
kubectl get pod web-7c9f -o jsonpath='{.status.containerStatuses[*].lastState.terminated}'
kubectl describe pod web-7c9f | grep -A5 "Last State"

# Consommation réelle (metrics-server requis)
kubectl top pods -n shop --containers
kubectl top nodes

# Allocation par nœud : ce qui est réservé, pas ce qui est consommé
kubectl describe node worker-1 | sed -n '/Allocated resources/,/Events/p'
Runtimes et mémoire : JVM, Node.js et .NET dimensionnent leur tas d'après la mémoire visible. Les versions récentes lisent la limite du cgroup, mais réglez explicitement -XX:MaxRAMPercentage=75 (Java) ou --max-old-space-size (Node) pour garder une marge hors tas (threads, buffers natifs). Sans cela, OOMKilled sans aucune trace dans les logs applicatifs.

5.3 Garde-fous par namespace

apiVersion: v1
kind: LimitRange
metadata: { name: defaults, namespace: shop }
spec:
  limits:
    - type: Container
      defaultRequest: { cpu: 50m, memory: 128Mi }
      default:        { memory: 256Mi }
---
apiVersion: v1
kind: ResourceQuota
metadata: { name: quota, namespace: shop }
spec:
  hard:
    requests.cpu: "8"
    requests.memory: 16Gi
    limits.memory: 24Gi
    persistentvolumeclaims: "10"
    services.loadbalancers: "0"

5.4 Autoscaling

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: web, namespace: shop }
spec:
  scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: web }
  minReplicas: 3
  maxReplicas: 15
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }   # % des REQUESTS
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300

Le HPA calcule l'utilisation en pourcentage des requests : sans requests, il ne fonctionne pas. Ne pas fixer spec.replicas dans les manifests gérés en GitOps quand un HPA est actif, sinon chaque synchronisation remet le nombre de réplicas à la valeur du dépôt.

6. Réseau : CNI, Services, NetworkPolicy

6.1 Choix du CNI

CNIData planeNetworkPolicyÀ retenir
Calicoiptables, nftables ou eBPF ; routage BGP ou encapsulation VXLAN/IPIPOui + politiques globales étenduesTrès éprouvé, BGP natif pour annoncer les réseaux de Pods au cœur de réseau
CiliumeBPFOui + politiques L7 (HTTP, DNS/FQDN)Peut remplacer kube-proxy, implémente Gateway API, observabilité réseau avec Hubble, chiffrement WireGuard
FlannelVXLANNonSimple mais sans NetworkPolicy : à éviter en production multi-équipes
NetworkPolicy silencieuse : un objet NetworkPolicy est accepté par l'API même si le CNI ne l'applique pas. Avec Flannel seul, vos politiques existent mais ne filtrent rien. Testez toujours l'effet réel (cf. 6.3).

6.2 Default deny par namespace

# 1) Tout interdire (entrée et sortie)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: shop }
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
---
# 2) Autoriser la résolution DNS vers CoreDNS (sinon tout casse)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns, namespace: shop }
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: kube-system }
          podSelector:
            matchLabels: { k8s-app: kube-dns }
      ports:
        - { protocol: UDP, port: 53 }
        - { protocol: TCP, port: 53 }
---
# 3) Le web reçoit depuis la passerelle et parle à la base
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: web, namespace: shop }
spec:
  podSelector:
    matchLabels: { app.kubernetes.io/name: web }
  policyTypes: [Ingress, Egress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: gateway-system }
      ports: [{ protocol: TCP, port: 8080 }]
  egress:
    - to:
        - podSelector:
            matchLabels: { app.kubernetes.io/name: postgres }
      ports: [{ protocol: TCP, port: 5432 }]
Sémantique : les NetworkPolicies sont additives (union des autorisations). Dans un même élément from, namespaceSelector et podSelector combinés = ET ; deux éléments séparés par un tiret = OU. Une indentation erronée transforme une règle restrictive en règle très permissive.

6.3 Vérifier le filtrage

kubectl -n shop run nettest --rm -it --image=busybox:1.36 --restart=Never -- \
  sh -c 'nslookup postgres.shop.svc.cluster.local; nc -zv -w2 postgres 5432; wget -qO- -T2 http://198.51.100.20 || echo BLOQUE'

# Avec Cilium : voir les paquets refusés
hubble observe --namespace shop --verdict DROPPED

6.4 Services et DNS

  • Nom complet : <service>.<namespace>.svc.cluster.local. Les Pods ont ndots:5 par défaut : un nom externe court comme api.example.com déclenche d'abord plusieurs recherches dans les domaines du cluster. Pour les appels externes intensifs, utiliser le FQDN avec point final (api.example.com.) ou abaisser ndots via dnsConfig.
  • externalTrafficPolicy: Local sur un Service LoadBalancer/NodePort conserve l'IP source du client et évite un rebond inter-nœuds, mais seuls les nœuds hébergeant un Pod reçoivent du trafic.
  • Service headless (clusterIP: None) : le DNS renvoie les IP des Pods ; indispensable aux StatefulSets (postgres-0.postgres.shop.svc).
  • Bare-metal : MetalLB (L2/ARP ou BGP) ou les fonctions LB-IPAM + annonces L2/BGP de Cilium pour donner une IP externe aux Services LoadBalancer.

7. Exposition : Gateway API vs Ingress

L'API Ingress est figée (pas de nouvelles fonctionnalités) et toutes les options avancées passaient par des annotations propres à chaque contrôleur. Gateway API (gateway.networking.k8s.io, GA depuis la v1.0) la remplace avec un modèle orienté rôles :

RessourcePropriétaireRôle
GatewayClassFournisseur d'infrastructureQuelle implémentation (Cilium, Envoy Gateway, Istio, NGINX Gateway Fabric, Traefik, Kong...)
GatewayÉquipe plateformeÉcouteurs (ports, protocoles, certificats), namespaces autorisés à s'y attacher
HTTPRoute / GRPCRoute / TLSRouteÉquipe applicativeRègles de routage : hôtes, chemins, en-têtes, poids (canary), redirections, réécritures
ReferenceGrantPropriétaire de la cibleAutorise explicitement une référence inter-namespace (Secret, Service)
Ingress NGINX communautaire en fin de vie : le projet Kubernetes a annoncé la retraite du contrôleur kubernetes/ingress-nginx, avec une maintenance « best effort » jusqu'en mars 2026, puis plus aucune version ni correctif de sécurité. Tout cluster qui l'utilise encore doit planifier sa migration (Gateway API de préférence, ou un autre contrôleur Ingress maintenu). Ne pas confondre avec le contrôleur NGINX de F5/NGINX Inc., qui est un produit distinct. Les annotations nginx.ingress.kubernetes.io/* (notamment les snippets de configuration, à l'origine de vulnérabilités critiques en 2025) n'ont pas d'équivalent direct : les inventorier avant migration.

7.1 Gateway partagée + route applicative

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: public
  namespace: gateway-system
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  gatewayClassName: cilium            # selon l'implémentation installée
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      allowedRoutes:
        namespaces: { from: All }
    - name: https-shop
      protocol: HTTPS
      port: 443
      hostname: shop.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - { kind: Secret, name: shop-example-com-tls }
      allowedRoutes:
        namespaces:
          from: Selector
          selector:
            matchLabels: { gateway-access: public }
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: web, namespace: shop }
spec:
  parentRefs:
    - { name: public, namespace: gateway-system, sectionName: https-shop }
  hostnames: ["shop.example.com"]
  rules:
    - matches:
        - path: { type: PathPrefix, value: /api }
      backendRefs:
        - { name: api, port: 8080, weight: 90 }
        - { name: api-canary, port: 8080, weight: 10 }
    - backendRefs:
        - { name: web, port: 8080 }
---
# Redirection HTTP -> HTTPS
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata: { name: redirect-https, namespace: gateway-system }
spec:
  parentRefs:
    - { name: public, sectionName: http }
  rules:
    - filters:
        - type: RequestRedirect
          requestRedirect: { scheme: https, statusCode: 301 }
# État d'attachement : toujours lire les conditions
kubectl get gateway -A
kubectl -n shop describe httproute web    # Accepted / ResolvedRefs dans status.parents
Migration : l'outil ingress2gateway du projet Kubernetes SIGs convertit des Ingress existants en ressources Gateway API. Le résultat est un point de départ : relire chaque annotation non traduite.

8. TLS automatique avec cert-manager

cert-manager gère le cycle de vie des certificats X.509 (ACME, CA interne, Vault, Venafi) et les stocke dans des Secrets kubernetes.io/tls. Il renouvelle par défaut aux deux tiers de la durée de vie. Pour les principes ACME, voir Let's Encrypt et pour la PKI interne PKI et certificats.

helm repo add jetstack https://charts.jetstack.io
helm upgrade --install cert-manager jetstack/cert-manager \
  -n cert-manager --create-namespace \
  --set crds.enabled=true \
  --set config.enableGatewayAPI=true   # support des solveurs HTTP-01 via Gateway API
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: letsencrypt-prod }
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: pki@example.com
    privateKeySecretRef: { name: letsencrypt-prod-account }
    solvers:
      # DNS-01 pour les wildcards et les services non exposés
      - selector:
          dnsZones: ["example.com"]
        dns01:
          cloudflare:
            apiTokenSecretRef: { name: cloudflare-api-token, key: api-token }
      # HTTP-01 via la Gateway publique
      - http01:
          gatewayHTTPRoute:
            parentRefs:
              - { name: public, namespace: gateway-system, kind: Gateway }

Avec l'annotation cert-manager.io/cluster-issuer sur la Gateway, cert-manager crée automatiquement un Certificate pour chaque écouteur HTTPS ayant un hostname. On peut aussi déclarer le Certificate explicitement :

apiVersion: cert-manager.io/v1
kind: Certificate
metadata: { name: wildcard-example-com, namespace: gateway-system }
spec:
  secretName: wildcard-example-com-tls
  issuerRef: { kind: ClusterIssuer, name: letsencrypt-prod }
  dnsNames: ["example.com", "*.example.com"]
  privateKey: { algorithm: ECDSA, size: 256, rotationPolicy: Always }
# Dépannage : la chaîne Certificate -> CertificateRequest -> Order -> Challenge
kubectl get certificate,certificaterequest,order,challenge -A
kubectl describe challenge -n gateway-system
cmctl status certificate wildcard-example-com -n gateway-system
cmctl renew wildcard-example-com -n gateway-system
Limites de débit ACME : testez d'abord avec l'environnement staging (https://acme-staging-v02.api.letsencrypt.org/directory). Une boucle d'échecs en production épuise rapidement les quotas par domaine et bloque les émissions plusieurs heures.

9. Stockage persistant

  • PersistentVolume (PV) : ressource de stockage du cluster. PersistentVolumeClaim (PVC) : demande d'un Pod. StorageClass : modèle de provisionnement dynamique via un pilote CSI.
  • Modes d'accès : ReadWriteOnce (un nœud), ReadWriteOncePod (un seul Pod, idéal pour les bases), ReadWriteMany (plusieurs nœuds : NFS, CephFS, Longhorn RWX), ReadOnlyMany.
  • reclaimPolicy : Delete (défaut des classes dynamiques : supprimer le PVC détruit les données !) ou Retain.
  • volumeBindingMode: WaitForFirstConsumer : provisionne le volume dans la zone / sur le nœud où le Pod est planifié. Indispensable avec des volumes locaux ou multi-zones.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-retain
  annotations:
    storageclass.kubernetes.io/is-default-class: "false"
provisioner: driver.longhorn.io
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
parameters:
  numberOfReplicas: "3"
  dataLocality: "best-effort"
SolutionTypeQuand l'utiliser
LonghornBloc répliqué sur les disques des workersClusters petits à moyens sans SAN ; simple, snapshots et sauvegardes S3 intégrés. Réseau 10 Gb/s recommandé.
Ceph-CSI (RBD / CephFS)Bloc et fichier sur cluster Ceph (externe ou Rook)Volumétrie importante, RWX performant, cluster Ceph déjà existant (par exemple celui de Proxmox)
local-path / Local PVDisque local du nœudBases répliquées au niveau applicatif (Postgres avec opérateur, Kafka) : performance maximale, pas de réplication stockage
NFS (csi-driver-nfs)FichierRWX simple ; à éviter pour les bases de données (verrous, fsync)
CSI cloudDisques managés (Azure Disk, EBS, PD)Clusters managés ; attention à l'attachement limité à une zone
Bases de données : sur Kubernetes, préférer un opérateur (CloudNativePG pour PostgreSQL, par exemple) à un StatefulSet écrit à la main : il gère réplication, bascule, sauvegardes continues vers un stockage objet et mises à jour mineures.
# Agrandir un volume (StorageClass avec allowVolumeExpansion)
kubectl -n shop patch pvc data-postgres-0 -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'
kubectl -n shop get pvc data-postgres-0 -w

# Snapshot CSI (CRD VolumeSnapshot + snapshot-controller requis)
cat <<'EOF' | kubectl apply -f -
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata: { name: pg-avant-migration, namespace: shop }
spec:
  volumeSnapshotClassName: longhorn-snapshot
  source: { persistentVolumeClaimName: data-postgres-0 }
EOF

10. RBAC, ServiceAccounts et Pod Security

10.1 RBAC

Le RBAC est uniquement permissif (pas de règle « deny »). Role/RoleBinding agissent dans un namespace ; ClusterRole/ClusterRoleBinding sur tout le cluster. Un ClusterRole peut être lié par un RoleBinding pour être réutilisé dans un seul namespace.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: deployer, namespace: shop }
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: deployer-ci, namespace: shop }
subjects:
  - { kind: ServiceAccount, name: ci-deployer, namespace: shop }
  - { kind: Group, name: "oidc:equipe-shop", apiGroup: rbac.authorization.k8s.io }
roleRef: { kind: Role, name: deployer, apiGroup: rbac.authorization.k8s.io }
# Auditer les droits
kubectl auth can-i --list --as=system:serviceaccount:shop:ci-deployer -n shop
kubectl auth can-i create pods/exec --as=jdupont -n shop
kubectl get clusterrolebindings -o wide | grep -E 'cluster-admin'
Droits équivalents à cluster-admin : create pods dans un namespace permet de monter n'importe quel Secret de ce namespace ; pods/exec, escalate, bind, impersonate, et l'écriture sur les webhooks d'admission ou les nodes/proxy permettent une élévation de privilèges. Les verbes list/watch sur secrets exposent leur contenu. Ne jamais distribuer admin.conf : utiliser une authentification OIDC (Entra ID, Keycloak...) avec des groupes.

10.2 ServiceAccounts

  • Les jetons projetés sont à durée limitée et liés au Pod ; les Secrets de jeton « longue durée » ne sont plus créés automatiquement depuis 1.24.
  • automountServiceAccountToken: false sur tout Pod qui n'appelle pas l'API.
  • Pour accéder au cloud depuis un Pod sans secret : Workload Identity (AKS avec Entra ID, IRSA / Pod Identity sur EKS, Workload Identity sur GKE), qui fédère le jeton du ServiceAccount via OIDC.

10.3 Pod Security Admission

La PodSecurityPolicy a été supprimée en 1.25. Elle est remplacée par Pod Security Admission (intégré, activé par défaut), qui applique les profils Pod Security Standards par labels de namespace :

ProfilContenuUsage
privilegedAucune restrictionNamespaces système (CNI, CSI, agents de supervision)
baselineInterdit hostNetwork, hostPID, privileged, hostPath, capacités dangereusesMinimum pour tout namespace applicatif
restrictedbaseline + runAsNonRoot, drop ALL, seccomp RuntimeDefault, pas d'escaladeCible pour les applications
kubectl label ns shop \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/audit=restricted

# Simuler l'impact AVANT d'appliquer (dry-run côté serveur)
kubectl label --dry-run=server --overwrite ns --all pod-security.kubernetes.io/enforce=baseline
Au-delà de PSA : pour des règles spécifiques (registres autorisés, labels obligatoires, interdiction de :latest), utiliser ValidatingAdmissionPolicy (CEL, natif et GA depuis 1.30) ou un moteur de politiques (Kyverno, OPA Gatekeeper). Le chiffrement des Secrets au repos dans etcd s'active via EncryptionConfiguration sur l'API server (fournisseur KMS v2 de préférence).

11. Gestion des secrets

Un Secret Kubernetes n'est qu'un encodage base64. Le problème à résoudre est double : ne pas stocker de secret en clair dans Git et centraliser la rotation.

ApprochePrincipeAvantagesInconvénients
Sealed SecretsChiffrement asymétrique côté poste (kubeseal), déchiffrement par un contrôleur dans le clusterSimple, 100 % GitOps, aucune dépendance externeClé liée au cluster (à sauvegarder !), rotation manuelle, pas de source de vérité centrale
External Secrets OperatorSynchronise depuis un coffre (Vault, Azure Key Vault, AWS Secrets Manager, GCP, 1Password...)Source unique, rotation centralisée, audit du coffreDépendance au coffre, authentification du contrôleur à concevoir
SOPS (+ age/KMS)Fichiers chiffrés dans Git, déchiffrés par Flux ou un plugin Argo CDLisible en diff (clés en clair, valeurs chiffrées)Gestion des clés de déchiffrement
CSI Secrets StoreMonte les secrets du coffre comme fichiersPas forcément de Secret Kubernetes crééDisponible seulement au montage du volume

11.1 Sealed Secrets

kubectl -n shop create secret generic db-credentials \
  --from-literal=username=app --from-literal=password='CHANGEME' \
  --dry-run=client -o yaml \
  | kubeseal --controller-namespace kube-system --format yaml > db-credentials.sealed.yaml
# On commite uniquement le fichier .sealed.yaml

# Sauvegarder la clé privée du contrôleur (sinon tout est perdu en cas de reconstruction)
kubectl -n kube-system get secret -l sealedsecrets.bitnami.com/sealed-secrets-key -o yaml > sealed-secrets-keys.yaml
# ... à stocker dans un coffre hors cluster, JAMAIS dans Git

11.2 External Secrets Operator

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata: { name: azure-kv }
spec:
  provider:
    azurekv:
      vaultUrl: "https://kv-exemple.vault.azure.net"
      authType: WorkloadIdentity
      serviceAccountRef: { name: external-secrets, namespace: external-secrets }
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: db-credentials, namespace: shop }
spec:
  refreshInterval: 1h
  secretStoreRef: { kind: ClusterSecretStore, name: azure-kv }
  target:
    name: db-credentials
    creationPolicy: Owner
  data:
    - secretKey: password
      remoteRef: { key: shop-db-password }
Rotation effective : un Secret monté en variable d'environnement n'est relu qu'au redémarrage du Pod. Monter en volume (fichier mis à jour par le kubelet) ou utiliser un outil comme Reloader qui déclenche un rollout à chaque changement de Secret/ConfigMap.

12. Helm, Kustomize et GitOps

12.1 Helm vs Kustomize

CritèreHelmKustomize
ModèleTemplates Go + values.yaml, packagé en chart versionnéPatchs déclaratifs sur du YAML pur (base + overlays)
DistributionDépôts HTTP ou registres OCIRépertoires Git
Cycle de vieReleases, historique, rollback, hooksAucun : simple rendu
Idéal pourLogiciels tiers, applications paramétrablesVariations d'environnement de vos propres manifests
# Helm : toujours rendre et comparer avant d'appliquer
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm show values prometheus-community/kube-prometheus-stack > defaults.yaml
helm template kps prometheus-community/kube-prometheus-stack -f values-prod.yaml | kubectl diff -f -  # (ou plugin helm-diff)
helm upgrade --install kps prometheus-community/kube-prometheus-stack \
  -n monitoring --create-namespace -f values-prod.yaml --version <version-epinglee> --wait
helm history kps -n monitoring
helm rollback kps 3 -n monitoring

# Chart en registre OCI
helm push mon-chart-1.4.0.tgz oci://registry.example.com/charts
Helm 4 : Helm 4 est sorti fin 2025. Il conserve le format des charts v2, mais certains drapeaux ont été renommés (par exemple --atomic devient --rollback-on-failure) et l'application côté serveur (server-side apply) est utilisée par défaut pour les nouvelles releases. Relire les notes de migration avant de mettre à jour les pipelines.
# Kustomize : base + overlay
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: shop
resources:
  - ../../base
images:
  - name: registry.example.com/shop/web
    newTag: "1.8.2"
patches:
  - target: { kind: Deployment, name: web }
    patch: |-
      - op: replace
        path: /spec/replicas
        value: 5
configMapGenerator:
  - name: web-config
    literals: ["APP_ENV=production", "LOG_LEVEL=info"]
kubectl kustomize overlays/prod | less
kubectl diff -k overlays/prod
kubectl apply -k overlays/prod

12.2 GitOps avec Argo CD ou Flux

Le dépôt Git devient la source de vérité ; un agent dans le cluster tire et réconcilie. Avantages : pas d'identifiants cluster-admin dans la CI, détection de dérive, audit par l'historique Git, reconstruction d'un cluster par simple bootstrap. La CI (cf. CI/CD) se contente de construire l'image et de mettre à jour le tag dans le dépôt de configuration.

Argo CDFlux
InterfaceUI web riche, visualisation de l'arbre d'objetsCLI / CRD, UI tierces
Multi-clusterUne instance centrale pilote plusieurs clustersUn Flux par cluster (modèle « pull » pur)
Multi-tenantAppProjects, SSO, RBAC propreRBAC Kubernetes natif, impersonation
SecretsPlugins (SOPS, ESO recommandé)SOPS natif
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: shop-prod
  namespace: argocd
spec:
  project: shop
  source:
    repoURL: https://git.example.com/platform/deploy.git
    targetRevision: main
    path: apps/shop/overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: shop
  syncPolicy:
    automated: { prune: true, selfHeal: true }
    syncOptions: ["CreateNamespace=false", "ServerSideApply=true"]
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers: ["/spec/replicas"]     # géré par le HPA
prune: true : supprimer un fichier du dépôt supprime l'objet dans le cluster, y compris un PVC. Protéger les ressources critiques avec l'annotation argocd.argoproj.io/sync-options: Prune=false et utiliser des StorageClasses en Retain.

13. Observabilité

13.1 Métriques : kube-prometheus-stack

Le chart kube-prometheus-stack installe Prometheus Operator, Prometheus, Alertmanager, Grafana, node-exporter, kube-state-metrics et un jeu de règles et tableaux de bord éprouvés. Les cibles se déclarent par CRD (ServiceMonitor, PodMonitor, PrometheusRule). Voir aussi Monitoring.

# values-prod.yaml (extrait)
prometheus:
  prometheusSpec:
    retention: 15d
    retentionSize: 45GB
    serviceMonitorSelectorNilUsesHelmValues: false   # découvrir TOUS les ServiceMonitor
    ruleSelectorNilUsesHelmValues: false
    storageSpec:
      volumeClaimTemplate:
        spec:
          storageClassName: longhorn-retain
          resources: { requests: { storage: 50Gi } }
alertmanager:
  config:
    route:
      receiver: equipe-ops
      group_by: ["alertname", "namespace"]
    receivers:
      - name: equipe-ops
        webhook_configs:
          - url: "http://alert-relay.monitoring.svc:8080/hook"
# Sur kubeadm, etcd / scheduler / controller-manager écoutent sur 127.0.0.1 :
# exposer leurs métriques (bind-address) ou désactiver ces cibles pour éviter des alertes permanentes.
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata: { name: shop, namespace: shop }
spec:
  groups:
    - name: shop.rules
      rules:
        - alert: PodRedemarrages
          expr: increase(kube_pod_container_status_restarts_total{namespace="shop"}[30m]) > 3
          for: 5m
          labels: { severity: warning }
          annotations:
            summary: "{{ $labels.pod }} redémarre en boucle"
        - alert: ThrottlingCPU
          expr: |
            sum by (pod) (rate(container_cpu_cfs_throttled_periods_total{namespace="shop"}[5m]))
              / sum by (pod) (rate(container_cpu_cfs_periods_total{namespace="shop"}[5m])) > 0.25
          for: 15m
          labels: { severity: info }

13.2 Alertes indispensables

  • etcd : perte de leader, etcd_disk_wal_fsync_duration_seconds p99 > 10 ms, base proche du quota (2 Go par défaut, 8 Go max recommandé).
  • Certificats : expiration des certificats du control plane et des Secrets TLS (métrique exportée par cert-manager).
  • Nœuds : NotReady, pression mémoire/disque, inodes, horloge désynchronisée.
  • Charges : CrashLoopBackOff, réplicas indisponibles, PVC presque plein (kubelet_volume_stats_available_bytes), Jobs en échec.
  • Plateforme : API server en erreur 5xx ou latence élevée, webhooks d'admission en échec (un webhook en panne peut bloquer tout déploiement).

13.3 Logs : Loki

Loki indexe uniquement les labels (namespace, pod, conteneur), pas le contenu : peu coûteux, stockage objet (S3/MinIO), requêtes LogQL. La collecte se fait par un DaemonSet ; Promtail est déprécié au profit de Grafana Alloy (fin de support annoncée pour 2026). Fluent Bit et Vector restent des alternatives compatibles.

# LogQL : erreurs 5xx de l'API sur 15 minutes
sum by (pod) (count_over_time({namespace="shop", container="api"} | json | status >= 500 [15m]))
Cardinalité : ne jamais transformer en label Loki ou Prometheus une valeur à forte cardinalité (ID utilisateur, ID de requête, URL complète). C'est la première cause d'explosion mémoire de ces deux systèmes.

13.4 Audit de l'API

Activer les journaux d'audit de l'API server (--audit-policy-file, --audit-log-path) avec une politique qui enregistre au niveau Metadata les accès aux Secrets (jamais RequestResponse, qui écrirait les valeurs) et les opérations exec/attach/portforward.

14. Sauvegarde et restauration

Deux niveaux complémentaires (voir aussi Stratégie de sauvegarde) :

  • etcd : état complet du cluster, restauration « tout ou rien » en cas de perte du control plane.
  • Velero (ou équivalent) : sauvegarde des objets par namespace + des volumes, restauration sélective, migration entre clusters.

14.1 Snapshot etcd (kubeadm)

sudo ETCDCTL_API=3 etcdctl \
  --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/server.crt \
  --key=/etc/kubernetes/pki/etcd/server.key \
  snapshot save /var/backups/etcd-$(date +%F-%H%M).db

# Vérification (etcdutl remplace les sous-commandes hors ligne d'etcdctl depuis etcd 3.5/3.6)
sudo etcdutl snapshot status /var/backups/etcd-2026-10-19-0300.db -w table
Le snapshot ne suffit pas : sauvegardez aussi /etc/kubernetes/pki (CA du cluster et d'etcd). Le snapshot contient les Secrets en clair (sauf chiffrement au repos) : chiffrez-le et stockez-le hors du cluster. Testez la restauration en lab au moins une fois par trimestre.
# Restauration sur un control plane unique (les autres membres sont ensuite réintégrés)
sudo mv /etc/kubernetes/manifests/etcd.yaml /root/        # arrête le pod statique etcd
sudo mv /var/lib/etcd /var/lib/etcd.old
sudo etcdutl snapshot restore /var/backups/etcd-2026-10-19-0300.db \
  --data-dir /var/lib/etcd \
  --name cp-1 \
  --initial-cluster cp-1=https://192.0.2.11:2380 \
  --initial-advertise-peer-urls https://192.0.2.11:2380
sudo mv /root/etcd.yaml /etc/kubernetes/manifests/
# puis : retirer les anciens membres, réinitialiser cp-2/cp-3 (kubeadm reset) et les rejoindre

14.2 Velero

velero install \
  --provider aws \
  --plugins velero/velero-plugin-for-aws:<version> \
  --bucket velero-backups \
  --secret-file ./credentials-velero \
  --backup-location-config region=minio,s3ForcePathStyle=true,s3Url=https://s3.example.internal \
  --use-node-agent --default-volumes-to-fs-backup

velero schedule create quotidien --schedule="0 2 * * *" --ttl 720h \
  --include-namespaces shop,monitoring

velero backup describe quotidien-20261019020000 --details
velero restore create --from-backup quotidien-20261019020000 \
  --include-namespaces shop --namespace-mappings shop:shop-restore
Cohérence applicative : une copie de volume à chaud d'une base de données n'est que « crash-consistent ». Utiliser les hooks Velero (pre.hook.backup.velero.io/command) pour geler l'écriture, ou mieux, la sauvegarde native de la base (pg_basebackup / WAL vers S3 via l'opérateur).

15. Mises à jour de version

Kubernetes publie environ trois versions mineures par an ; chacune est maintenue environ 14 mois. Seules les trois dernières mineures reçoivent des correctifs : un cluster non mis à jour pendant un an sort du support. Vérifier la page officielle des releases pour connaître les branches maintenues au moment de l'opération.

15.1 Règles de version skew

  • On monte une version mineure à la fois (1.33 -> 1.34 -> 1.35), jamais de saut.
  • Le control plane d'abord, les nœuds ensuite. Le kubelet peut avoir jusqu'à trois mineures de retard sur l'API server, jamais d'avance.
  • kubectl : une mineure d'écart maximum (en plus ou en moins).

15.2 Avant de commencer

# APIs dépréciées encore utilisées (métrique de l'API server)
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis

# Analyse des manifests et releases Helm (outils tiers : pluto, kubent)
pluto detect-helm -o wide
pluto detect-files -d ./manifests

# PDB bloquants (ALLOWED DISRUPTIONS = 0 empêchera le drain)
kubectl get pdb -A

# Snapshot etcd + vérifier la compatibilité du CNI, du CSI, de cert-manager,
# de l'implémentation Gateway API et des opérateurs avec la version cible

15.3 Procédure kubeadm

# 1) Premier control plane : changer de dépôt (pkgs.k8s.io est par mineure)
sudo sed -i 's/v1.34/v1.35/g' /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-mark unhold kubeadm && sudo apt-get install -y kubeadm='1.35.*' && sudo apt-mark hold kubeadm
sudo kubeadm upgrade plan
sudo kubeadm upgrade apply v1.35.x

# 2) Autres control planes
sudo kubeadm upgrade node

# 3) Chaque nœud (control planes puis workers), un par un
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data --timeout=10m
# sur le nœud :
sudo apt-mark unhold kubelet kubectl && sudo apt-get install -y kubelet='1.35.*' kubectl='1.35.*' && sudo apt-mark hold kubelet kubectl
sudo kubeadm upgrade node        # sur un worker : met à jour la config kubelet
sudo systemctl daemon-reload && sudo systemctl restart kubelet
kubectl uncordon worker-1
kubectl get nodes
Nœuds immuables : en cloud ou avec Talos, plutôt que de mettre à jour sur place, on ajoute un pool de nœuds à la nouvelle version puis on draine et supprime l'ancien. Plus rapide, et le retour arrière consiste simplement à garder l'ancien pool.

16. Dépannage et checklist de production

16.1 Méthode

kubectl get pods -n shop -o wide
kubectl describe pod <pod> -n shop                     # section Events en bas
kubectl get events -n shop --sort-by=.lastTimestamp
kubectl events -n shop --for pod/<pod> --watch
kubectl logs <pod> -c <conteneur> --previous            # logs de l'instance qui a planté
kubectl get pod <pod> -o yaml | less                    # status.conditions, containerStatuses

# Conteneur éphémère de debug (image distroless sans shell)
kubectl debug -it <pod> --image=busybox:1.36 --target=<conteneur>
# Copie du pod avec une commande modifiée (pour un pod en crash)
kubectl debug <pod> -it --copy-to=debug-web --container=web -- sh
# Shell sur le nœud (système de fichiers de l'hôte monté sous /host)
kubectl debug node/worker-1 -it --image=busybox:1.36 --profile=sysadmin

# Côté nœud
sudo journalctl -u kubelet -f
sudo crictl ps -a
sudo crictl logs <id-conteneur>
sudo crictl pull registry.example.com/shop/web:1.8.2

16.2 Symptômes fréquents

SymptômeCause probableCorrection
CrashLoopBackOffLe processus sort (erreur de config, variable manquante, migration échouée) ou liveness qui tue l'applilogs --previous, vérifier le code de sortie (137 = OOM/SIGKILL, 1 = erreur appli), ajouter une startupProbe
ImagePullBackOff / ErrImagePullTag inexistant, registre privé sans imagePullSecrets, quota de pull (Docker Hub), DNS ou proxy du nœuddescribe pod (message exact), crictl pull depuis le nœud, miroir de registre interne
PendingRessources insuffisantes (requests), taint non tolérée, affinité impossible, PVC non liéLire l'event FailedScheduling (« 0/6 nodes are available: 3 Insufficient memory... »), get pvc
OOMKilledLimite mémoire trop basse ou fuiteMesurer (kubectl top, Grafana), régler le tas du runtime, augmenter la limite
CreateContainerConfigErrorConfigMap ou Secret référencé absent, clé manquantedescribe pod, créer l'objet ou marquer la référence optional
Pod Running mais 0/1 ReadyreadinessProbe en échecTester l'URL depuis le pod (kubectl exec ou debug), vérifier port et chemin
Service sans réponseSélecteur qui ne correspond à aucun Pod, mauvais targetPort, NetworkPolicyget endpointslices -l kubernetes.io/service-name=x ; vide = problème de labels
Résolution DNS lente ou intermittenteCoreDNS saturé, ndots:5, conntrack UDP, boucle avec le resolv.conf du nœud (systemd-resolved)kubectl -n kube-system logs -l k8s-app=kube-dns, NodeLocal DNSCache, FQDN avec point final
Namespace bloqué en TerminatingFinalizers d'objets dont le contrôleur a disparu, APIService indisponiblekubectl get apiservice | grep False, retirer le contrôleur fautif ou les finalizers en connaissance de cause
Déploiements refusés « failed calling webhook »Webhook d'admission indisponible avec failurePolicy: FailRéparer le service du webhook ; en urgence, supprimer la ValidatingWebhookConfiguration concernée
Nœud NotReadykubelet arrêté, runtime en panne, CNI absent, disque plein, certificat kubelet expiréjournalctl -u kubelet, df -h /var/lib, crictl info
Évictions massivesPression disque (imagefs, logs) ou mémoire sur le nœudNettoyage des images, rotation des logs, requests réalistes, seuils d'éviction du kubelet

16.3 Diagnostic DNS rapide

kubectl run dnstest --rm -it --image=busybox:1.36 --restart=Never -- \
  sh -c 'cat /etc/resolv.conf; nslookup kubernetes.default; nslookup example.com'
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl -n kube-system get configmap coredns -o yaml
# Activer temporairement le plugin « log » dans le Corefile pour tracer les requêtes

16.4 Pièges classiques

  • Utiliser :latest ou un tag mutable : impossible de savoir ce qui tourne, rollback illusoire. Épingler par tag immuable, idéalement par digest.
  • Oublier les requests : le scheduler surcharge les nœuds, le HPA ne fonctionne pas, les Pods sont BestEffort.
  • Un seul réplica + PDB minAvailable: 1 : le drain des nœuds est bloqué indéfiniment.
  • Mettre des secrets dans des ConfigMaps ou en clair dans des valeurs Helm commitées.
  • Exposer l'API server sur Internet sans restriction d'IP : la limiter au VPN / bastion.
  • Ignorer l'expiration des certificats kubeadm, ou la dérive d'horloge (NTP) qui casse etcd et les jetons.
  • Mettre etcd sur des disques partagés lents ou sur un stockage réseau : élections de leader à répétition.

16.5 Checklist de production

DomaineContrôle
Control plane3 membres etcd sur SSD, API derrière VIP/LB, accès API restreint au réseau d'administration, certificats surveillés
SauvegardeSnapshot etcd automatisé + PKI, Velero planifié, restauration testée et chronométrée, clé Sealed Secrets sauvegardée
IdentitésOIDC pour les humains, aucun admin.conf distribué, RBAC minimal, ServiceAccounts dédiés, audit API actif
ChargesRequests partout, limites mémoire, 3 probes pertinentes, PDB, topologySpreadConstraints, images épinglées
Sécurité PodPSA restricted (ou baseline minimum) sur tous les namespaces applicatifs, runAsNonRoot, readOnlyRootFilesystem, drop ALL
RéseauCNI appliquant les NetworkPolicies, default-deny par namespace, egress contrôlé, plus d'Ingress NGINX communautaire
SecretsAucun secret en clair dans Git, chiffrement au repos etcd, rotation outillée
Supervisionkube-prometheus-stack, alertes etcd/certificats/PVC/CrashLoop, logs centralisés avec rétention définie
Cycle de vieVersion dans le support upstream, calendrier d'upgrade trimestriel, détection des APIs dépréciées en CI
LivraisonGitOps (Argo CD / Flux), aucun kubectl apply manuel en prod, images scannées et signées