Kubernetes - Exploitation en production
- 1. Architecture d'un cluster
- 2. Choisir une distribution
- 3. Installation kubeadm en haute disponibilité
- 4. Objets de charge de travail
- 5. Probes, ressources et QoS
- 6. Réseau : CNI, Services, NetworkPolicy
- 7. Exposition : Gateway API vs Ingress
- 8. TLS automatique avec cert-manager
- 9. Stockage persistant
- 10. RBAC, ServiceAccounts et Pod Security
- 11. Gestion des secrets
- 12. Helm, Kustomize et GitOps
- 13. Observabilité
- 14. Sauvegarde et restauration
- 15. Mises à jour de version
- 16. Dépannage et checklist de 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
| Composant | Rôle | Points d'attention en production |
|---|---|---|
kube-apiserver | Point d'entrée unique, authentification, autorisation (RBAC), admission, persistance dans etcd | Sans état, scalable horizontalement derrière un répartiteur TCP 6443. Seul composant qui parle à etcd. |
etcd | Base clé-valeur distribuée (Raft) contenant tout l'état du cluster | Nombre impair de membres (3 ou 5). Très sensible à la latence disque (fsync) : SSD/NVMe obligatoires. Sauvegarde = sauvegarde du cluster. |
kube-scheduler | Affecte les Pods non planifiés à un nœud (filtrage puis scoring) | Actif/passif par élection de leader (Lease dans kube-system). |
kube-controller-manager | Boucles de contrôle : Deployment, ReplicaSet, Node, Job, EndpointSlice, ServiceAccount... | Actif/passif par élection de leader. |
cloud-controller-manager | Inté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) :
containerdouCRI-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
kubectl applyenvoie un Deployment à l'API server : authentification, RBAC, puis chaîne d'admission (mutating puis validating webhooks, Pod Security Admission, ValidatingAdmissionPolicy).- L'objet est persisté dans etcd. Le contrôleur Deployment crée un ReplicaSet, qui crée les Pods (sans
nodeName). - Le scheduler choisit un nœud (ressources demandées, affinités, taints/tolerations, topologie) et écrit le binding.
- 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.
- Les probes passent : le Pod devient
Ready, son IP est ajoutée aux EndpointSlices des Services correspondants.
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.
| Distribution | Profil | Forces | Limites |
|---|---|---|---|
| kubeadm | Upstream « brut », on-premise | Référence officielle, aucun ajout propriétaire, compréhension complète | Tout est à faire : OS, LB d'API, CNI, upgrades nœud par nœud, rotation des certificats |
| k3s | Edge, labs, petits clusters | Binaire unique, très léger, etcd embarqué ou SQLite/MySQL/PostgreSQL (kine), Traefik et ServiceLB inclus | Composants embarqués à désactiver en prod sérieuse ; moins d'options de durcissement par défaut |
| RKE2 | On-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 Linux | Nœuds immuables pilotés par API | Pas de SSH ni de shell, OS minimal en lecture seule, configuration déclarative (talosctl), upgrades atomiques | Changement de paradigme pour les équipes « Linux classique » ; débogage via talosctl et pods de debug |
| AKS / EKS / GKE | Cloud managé | Control plane et etcd gérés, SLA, intégration IAM (Workload Identity), autoscaling de nœuds | Coût du LB/egress, contraintes du fournisseur, cadence d'upgrade imposée (support étendu payant) |
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
topologySpreadConstraintset 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, Services10.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
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
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
| Objet | Usage | Particularités |
|---|---|---|
| Deployment | Applications sans état | Rolling update via ReplicaSets, rollback (kubectl rollout undo), Pods interchangeables |
| StatefulSet | Bases de données, files, clusters à quorum | Identité stable (app-0, app-1), un PVC par réplique (volumeClaimTemplates), démarrage ordonné, Service headless |
| DaemonSet | Agents par nœud (logs, métriques, CNI, CSI) | Un Pod par nœud éligible ; tolérer les taints des control planes si nécessaire |
| Job | Traitement ponctuel | backoffLimit, activeDeadlineSeconds, ttlSecondsAfterFinished, mode indexé |
| CronJob | Tâches planifiées | concurrencyPolicy: Forbid, timeZone, startingDeadlineSeconds |
| Service | Adresse stable + répartition | ClusterIP, NodePort, LoadBalancer, Headless (clusterIP: None), ExternalName |
| ConfigMap / Secret | Configuration et données sensibles | Secrets 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 }
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
| Probe | Question posée | Effet en cas d'échec | Erreur classique |
|---|---|---|---|
| startupProbe | L'application a-t-elle fini de démarrer ? | Redémarrage du conteneur après failureThreshold x periodSeconds | Absente sur une appli lente : la liveness la tue pendant son démarrage |
| readinessProbe | Peut-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 |
| livenessProbe | Le processus est-il bloqué irrémédiablement ? | Redémarrage du conteneur | Tester des dépendances externes : redémarrages en cascade de toute la flotte |
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 QoS | Condition | Ordre d'éviction sous pression |
|---|---|---|
| Guaranteed | requests = limits pour CPU et mémoire, sur tous les conteneurs | En dernier |
| Burstable | Au moins une request ou limit, sans être Guaranteed | Selon la consommation au-delà des requests |
| BestEffort | Aucune request ni limit | En 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'
-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
| CNI | Data plane | NetworkPolicy | À retenir |
|---|---|---|---|
| Calico | iptables, nftables ou eBPF ; routage BGP ou encapsulation VXLAN/IPIP | Oui + politiques globales étendues | Très éprouvé, BGP natif pour annoncer les réseaux de Pods au cœur de réseau |
| Cilium | eBPF | Oui + politiques L7 (HTTP, DNS/FQDN) | Peut remplacer kube-proxy, implémente Gateway API, observabilité réseau avec Hubble, chiffrement WireGuard |
| Flannel | VXLAN | Non | Simple mais sans NetworkPolicy : à éviter en production multi-équipes |
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 }]
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 ontndots:5par défaut : un nom externe court commeapi.example.comdé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 abaisserndotsviadnsConfig. externalTrafficPolicy: Localsur 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 :
| Ressource | Propriétaire | Rôle |
|---|---|---|
GatewayClass | Fournisseur d'infrastructure | Quelle 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 applicative | Règles de routage : hôtes, chemins, en-têtes, poids (canary), redirections, réécritures |
ReferenceGrant | Propriétaire de la cible | Autorise explicitement une référence inter-namespace (Secret, Service) |
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
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
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 !) ouRetain. - 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"
| Solution | Type | Quand l'utiliser |
|---|---|---|
| Longhorn | Bloc répliqué sur les disques des workers | Clusters 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 PV | Disque local du nœud | Bases répliquées au niveau applicatif (Postgres avec opérateur, Kafka) : performance maximale, pas de réplication stockage |
| NFS (csi-driver-nfs) | Fichier | RWX simple ; à éviter pour les bases de données (verrous, fsync) |
| CSI cloud | Disques managés (Azure Disk, EBS, PD) | Clusters managés ; attention à l'attachement limité à une zone |
# 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'
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: falsesur 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 :
| Profil | Contenu | Usage |
|---|---|---|
privileged | Aucune restriction | Namespaces système (CNI, CSI, agents de supervision) |
baseline | Interdit hostNetwork, hostPID, privileged, hostPath, capacités dangereuses | Minimum pour tout namespace applicatif |
restricted | baseline + runAsNonRoot, drop ALL, seccomp RuntimeDefault, pas d'escalade | Cible 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
: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.
| Approche | Principe | Avantages | Inconvénients |
|---|---|---|---|
| Sealed Secrets | Chiffrement asymétrique côté poste (kubeseal), déchiffrement par un contrôleur dans le cluster | Simple, 100 % GitOps, aucune dépendance externe | Clé liée au cluster (à sauvegarder !), rotation manuelle, pas de source de vérité centrale |
| External Secrets Operator | Synchronise depuis un coffre (Vault, Azure Key Vault, AWS Secrets Manager, GCP, 1Password...) | Source unique, rotation centralisée, audit du coffre | Dé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 CD | Lisible en diff (clés en clair, valeurs chiffrées) | Gestion des clés de déchiffrement |
| CSI Secrets Store | Monte les secrets du coffre comme fichiers | Pas 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 }
12. Helm, Kustomize et GitOps
12.1 Helm vs Kustomize
| Critère | Helm | Kustomize |
|---|---|---|
| Modèle | Templates Go + values.yaml, packagé en chart versionné | Patchs déclaratifs sur du YAML pur (base + overlays) |
| Distribution | Dépôts HTTP ou registres OCI | Répertoires Git |
| Cycle de vie | Releases, historique, rollback, hooks | Aucun : simple rendu |
| Idéal pour | Logiciels tiers, applications paramétrables | Variations 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
--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 CD | Flux | |
|---|---|---|
| Interface | UI web riche, visualisation de l'arbre d'objets | CLI / CRD, UI tierces |
| Multi-cluster | Une instance centrale pilote plusieurs clusters | Un Flux par cluster (modèle « pull » pur) |
| Multi-tenant | AppProjects, SSO, RBAC propre | RBAC Kubernetes natif, impersonation |
| Secrets | Plugins (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
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_secondsp99 > 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]))
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
/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
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
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ôme | Cause probable | Correction |
|---|---|---|
CrashLoopBackOff | Le processus sort (erreur de config, variable manquante, migration échouée) ou liveness qui tue l'appli | logs --previous, vérifier le code de sortie (137 = OOM/SIGKILL, 1 = erreur appli), ajouter une startupProbe |
ImagePullBackOff / ErrImagePull | Tag inexistant, registre privé sans imagePullSecrets, quota de pull (Docker Hub), DNS ou proxy du nœud | describe pod (message exact), crictl pull depuis le nœud, miroir de registre interne |
Pending | Ressources 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 |
OOMKilled | Limite mémoire trop basse ou fuite | Mesurer (kubectl top, Grafana), régler le tas du runtime, augmenter la limite |
CreateContainerConfigError | ConfigMap ou Secret référencé absent, clé manquante | describe pod, créer l'objet ou marquer la référence optional |
Pod Running mais 0/1 Ready | readinessProbe en échec | Tester l'URL depuis le pod (kubectl exec ou debug), vérifier port et chemin |
| Service sans réponse | Sélecteur qui ne correspond à aucun Pod, mauvais targetPort, NetworkPolicy | get endpointslices -l kubernetes.io/service-name=x ; vide = problème de labels |
| Résolution DNS lente ou intermittente | CoreDNS 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 Terminating | Finalizers d'objets dont le contrôleur a disparu, APIService indisponible | kubectl 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: Fail | Réparer le service du webhook ; en urgence, supprimer la ValidatingWebhookConfiguration concernée |
Nœud NotReady | kubelet arrêté, runtime en panne, CNI absent, disque plein, certificat kubelet expiré | journalctl -u kubelet, df -h /var/lib, crictl info |
| Évictions massives | Pression disque (imagefs, logs) ou mémoire sur le nœud | Nettoyage 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
:latestou 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
| Domaine | Contrôle |
|---|---|
| Control plane | 3 membres etcd sur SSD, API derrière VIP/LB, accès API restreint au réseau d'administration, certificats surveillés |
| Sauvegarde | Snapshot etcd automatisé + PKI, Velero planifié, restauration testée et chronométrée, clé Sealed Secrets sauvegardée |
| Identités | OIDC pour les humains, aucun admin.conf distribué, RBAC minimal, ServiceAccounts dédiés, audit API actif |
| Charges | Requests partout, limites mémoire, 3 probes pertinentes, PDB, topologySpreadConstraints, images épinglées |
| Sécurité Pod | PSA restricted (ou baseline minimum) sur tous les namespaces applicatifs, runAsNonRoot, readOnlyRootFilesystem, drop ALL |
| Réseau | CNI appliquant les NetworkPolicies, default-deny par namespace, egress contrôlé, plus d'Ingress NGINX communautaire |
| Secrets | Aucun secret en clair dans Git, chiffrement au repos etcd, rotation outillée |
| Supervision | kube-prometheus-stack, alertes etcd/certificats/PVC/CrashLoop, logs centralisés avec rétention définie |
| Cycle de vie | Version dans le support upstream, calendrier d'upgrade trimestriel, détection des APIs dépréciées en CI |
| Livraison | GitOps (Argo CD / Flux), aucun kubectl apply manuel en prod, images scannées et signées |