ADR 025 â Gouvernance des ressources : requests/limits mesurĂ©s, QoS, LimitRange et ResourceQuota (2026-06-24)
Statut
AcceptĂ© (2026-06-24). Right-sizing (DĂ©cision 1/2) + LimitRange au bootstrap (DĂ©cision 3) validĂ©s live from-scratch le 2026-06-25 (no BestEffort dans les 8 namespaces possĂ©dĂ©s). ResourceQuota reportĂ© Sprint 6. La validation a rĂ©vĂ©lĂ© que des requests honnĂȘtes sur-souscrivent le nĆud unique (requests mĂ©moire Ă 99 %, prometheus non planifiable) â #114 (capacitĂ©) devient un prĂ©requis dur ; #112 reste ouvert et sera clos avec #114 (voir « Validation live »). Issue #112, nĂ© de l'incident INC-060. ADR compagnon : ADR 026 (segmentation des node groups + Spot, #114) traite le volet « capacitĂ© matĂ©rielle » ; le prĂ©sent ADR traite le volet « discipline des ressources ».
Contexte
L'incident INC-060 a coupĂ© tout l'ingress (~10 min) sur le nĆud unique t3.medium : un redĂ©ploiement de Grafana a dĂ©clenchĂ© un pic mĂ©moire qui a fait basculer un nĆud sur-engagĂ© (limits.memory cumulĂ©es = 168 % de la capacitĂ©). Le dĂ©clencheur (prĂ©install des plugins Grafana) est traitĂ© Ă part. La cause racine est l'overcommit sans garde-fou : les pods se planifient sur des requests sous-Ă©valuĂ©es, mais la pression au runtime vient des limits, et rien n'empĂȘche leur somme de dĂ©passer la capacitĂ© physique.
Ă l'approche de Tempo (#108, gourmand) et d'un 2e workload (frontend #109), continuer sans gouvernance rejouerait l'incident.
DĂ©cision 1 â Dimensionner sur la mesure, pas sur l'intuition
On relÚve la consommation réelle par pod (régime stable et pic) avec l'outillage déjà en place (Prometheus + kube-state-metrics + node-exporter) :
- Mémoire :
container_memory_working_set_bytes(c'est la métrique qui déclenche l'OOMKill, pas le RSS). - CPU :
rate(container_cpu_usage_seconds_total[5m]). - Aide Ă la recommandation : VPA en mode
recommender(Off) ou Goldilocks (dashboard de reco par namespace), sans appliquer automatiquement.
DĂ©cision 2 â RĂ©gler mĂ©moire et CPU diffĂ©remment (compressible vs non)
- MĂ©moire (non compressible â dĂ©passement = OOMKill) :
request= working set en rĂ©gime + marge ;limitcouvre le pic mesurĂ© (+ ~15-20 %). Sur les composants critiques,request = limitâ classe QoS Guaranteed (les derniers Ă©vincĂ©s sous pression). - CPU (compressible â dĂ©passement = throttling, pas de kill) :
request= usage p95 ;limitlarge ou absente sur le latency-sensitive (le throttling CPU fait souvent plus de mal qu'un léger dépassement). Arbitré selon ce que le check kube-linter exige en CI.
DĂ©cision 3 â Border par namespace : LimitRange + ResourceQuota
LimitRange(defaults par namespace) : tout pod sans requests/limits hĂ©rite de valeurs par dĂ©faut â plus de podBestEffortqui se faufile. Placement (corrigĂ© en live, voir plus bas) : un LimitRange n'agit qu'Ă l'admission du pod (defaults injectĂ©s Ă la crĂ©ation, jamais rĂ©troactivement). Il doit donc exister avant les pods â il est créé au bootstrap (Ansible), avant tout composant, et non par une Application GitOps qui se synchronise trop tard. Poulet/Ćuf assumĂ© : une gouvernance qui doit prĂ©cĂ©der ArgoCD ne peut pas ĂȘtre gĂ©rĂ©e par ce mĂȘme ArgoCD.ResourceQuotaplafonnant la somme deslimits.memorypar namespace (reportĂ© Sprint 6) â c'est le garde-fou qui rendrait l'overcommit mĂ©moire structurellement impossible Ă l'admission. Nuance acquise en live : la somme des limits n'est pas la cause directe d'INC-060 (les limits ne rĂ©servent rien) ; le quota reste utile comme plafond dur, mais le vrai levier anti-incident immĂ©diat a Ă©tĂ© le dĂ©clencheur (prĂ©install Grafana, !195) et la capacitĂ© (#114).
Conséquences
- L'incident INC-060 ne peut plus se reproduire par overcommit : un dĂ©ploiement qui ferait dĂ©passer le quota est refusĂ© Ă l'admission, au lieu de tuer le nĆud au runtime.
- Les valeurs (requests/limits, plafond de quota) se calent et se valident en live (MR3) : un quota trop serré bloquerait les déploiements, donc il se vérifie sur le cluster réel, pas seulement par
helm template. - Classe QoS explicite sur le critique â comportement d'Ă©viction prĂ©visible sous pression.
- Alternatives Ă©cartĂ©es : monter un nĆud plus gros sans gouvernance (dĂ©place le mur sans le supprimer) ; Karpenter tout de suite (ne corrige pas l'overcommit runtime, il rĂ©agit au
Pendingâ donc inutile sans requests honnĂȘtes ; reportĂ© Sprint 6).
Validation live (2026-06-25)
Boot from-scratch, mesures par pod via Prometheus (metrics-server réparé en parallÚle, #115/INC-061).
- DĂ©clencheur (!195) â
: cluster monté propre,
api200, aucune cascade NodeNotReady au redĂ©ploiement Grafana (prĂ©install plugins dĂ©sactivĂ©). C'est le gain anti-incident immĂ©diat. - Right-sizing (!196) â : valeurs mesurĂ©es bien dĂ©ployĂ©es (fastapi req 100m/128Mi limit 256Mi ; argocd-application-controller req 512Mi limit 1Gi â le runaway BestEffort Ă ~800Mi est dĂ©sormais capĂ© ; grafana et prometheus req 384Mi limit 512Mi).
- Nuance honnĂȘte sur l'overcommit : la somme des
limits.memorydu nĆud est montĂ©e (168 % â 217 %), pas baissĂ©e. Ce n'est pas un Ă©chec : c'est l'effet mĂ©canique d'avoir donnĂ© une limite explicite (1Gi) Ă un gros pod jusque-lĂ non bornĂ© (BestEffort = 0 dans la somme). Borner un runaway fait monter la somme des limits tout en rĂ©duisant le risque rĂ©el. La mĂ©trique qui compte est ailleurs :requestsmĂ©moire Ă 91 % â nĆud tendu, ce qui confirme que le vrai fix capacitĂ© est #114, pas le right-sizing seul. - LimitRange (!197 â !199) â trou trouvĂ© puis corrigĂ©, reprouvĂ© from-scratch â
: livrĂ© en GitOps (sync-wave 2), il arrivait aprĂšs les composants dĂ©jĂ dĂ©marrĂ©s â ~14 pods plateforme (argocd, cert-manager, ESO, external-dns) restaient BestEffort (un LimitRange n'agit qu'Ă l'admission). CorrigĂ© (!199) : LimitRange dĂ©placĂ© au bootstrap Ansible, créé avant tout composant. 2e boot from-scratch (2026-06-25) : plus AUCUN BestEffort dans les 8 namespaces possĂ©dĂ©s,
fastapinérestricted(PSA) une seule fois, LimitRange présent partout avant les pods.
La gouvernance honnĂȘte rĂ©vĂšle la sur-souscription (le vrai enseignement)
ConsĂ©quence directe et assumĂ©e du LimitRange efficace : chaque pod jusque-lĂ BestEffort rĂ©serve dĂ©sormais le defaultRequest (64Mi). Sur le boot from-scratch, le nĆud unique t3.medium est montĂ© Ă 99 % de requests mĂ©moire (3286Mi / ~3319 allouables) â prometheus-kps-prometheus-0 (request 384Mi) ne peut plus ĂȘtre planifiĂ© (FailedScheduling: Insufficient memory). Ă noter : MemoryPressure: False â c'est de la sur-rĂ©servation (somme des requests), pas un manque de RAM rĂ©elle (usage ~69 %) ; les pods ne consomment pas ces 64Mi, ils bloquent juste le scheduler.
DĂ©cision (assumĂ©e avec Youssef) : ne PAS band-aider en baissant le defaultRequest Ă un plancher-jeton (16Mi) qu'il faudrait remonter ensuite. On garde des requests honnĂȘtes (64Mi) : c'est future-correct (dĂšs que la capacitĂ© existe, tout se planifie sans rien retoucher). La gouvernance par rĂ©servations et la capacitĂ© sont couplĂ©es : on ne peut pas rĂ©server un plancher pour chaque pod sur un nĆud dĂ©jĂ plein.
â #114 (ADR 026) passe de « souhaitable » Ă prĂ©requis dur, prouvĂ© par la mesure (requests Ă 99 %). #112 reste ouvert : la gouvernance est livrĂ©e et validĂ©e (no BestEffort, runaways capĂ©s), mais la stack ne planifie entiĂšrement qu'aprĂšs #114 (capacitĂ©). On clĂŽt #112 avec #114.
Ăvolution possible
- ADR 026 (#114) : segmenter en node groups core (on-demand) / observability (Spot) pour isoler les workloads gourmands du trafic.
- Karpenter (Sprint 6) : une fois les requests fiables, l'autoscaling des nĆuds devient pertinent (il se dĂ©clenche sur le
Pending, qui dépend des requests).
Date : 2026-06-24 Sprint : 5 Issue : #112 (INC-060)