<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Le blog de Joseph Delassalle — platform engineering &amp; SRE</title><description>Joseph Delassalle, Platform Engineer / SRE freelance senior : migration d&apos;infrastructures legacy vers Kubernetes sans interruption de service. Lille–Bruxelles.</description><link>https://jm-dev.it/</link><language>fr</language><item><title>Instrumenter une app Prometheus : golden signals</title><link>https://jm-dev.it/blog/instrumenter-application-prometheus-golden-signals/</link><guid isPermaLink="true">https://jm-dev.it/blog/instrumenter-application-prometheus-golden-signals/</guid><description>Golden signals, méthode RED et histogramme plutôt que summary pour la latence : comment instrumenter une application Prometheus, avec les requêtes PromQL.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=fr justification : cible les requêtes FR &quot;instrumenter prometheus&quot;, &quot;golden signals&quot;, &quot;histogramme prometheus latence&quot;, &quot;histogram_quantile&quot;, et sert de relais LinkedIn francophone. Le pendant EN vise la longue traîne mondiale (même translationKey). Article socle sous l&apos;article alerting déjà publié : il donne les métriques que les alertes par symptôme consomment.&lt;/p&gt;
&lt;p&gt;Note contenu (directive JM 2026-07-24) : tutoriel de référence, générique et vérifiable de bout en bout contre le comportement documenté de Prometheus (types de métriques, histogram_quantile, format d&apos;exposition). Aucune anecdote de production par construction ; ce n&apos;est pas un REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;Votre cluster est couvert de métriques. &lt;code&gt;node_exporter&lt;/code&gt; remonte le CPU, la RAM et le remplissage des disques, &lt;code&gt;kube-state-metrics&lt;/code&gt; connaît l&apos;état de chaque pod. Puis un responsable produit demande : « le paiement est-il plus lent que ce matin ? », et personne ne sait répondre. Toutes ces métriques décrivent les machines et l&apos;orchestrateur ; aucune ne décrit ce que fait votre application. C&apos;est exactement le trou que l&apos;instrumentation comble : exposer, depuis le code, les quelques chiffres qui disent si le service rend le service.&lt;/p&gt;
&lt;h2&gt;Les quatre signaux, et par où commencer&lt;/h2&gt;
&lt;p&gt;La grille la plus utile reste celle des &lt;em&gt;golden signals&lt;/em&gt; du livre SRE de Google : la latence (combien de temps une requête prend), le trafic (combien vous en recevez), les erreurs (combien échouent) et la saturation (à quel point vos ressources sont sollicitées). La saturation est déjà largement couverte par &lt;code&gt;node_exporter&lt;/code&gt; et les métriques du cluster. Restent trois choses à instrumenter dans l&apos;application elle-même, ce que la méthode RED résume bien pour un service qui répond à des requêtes : Rate, Errors, Duration.&lt;/p&gt;
&lt;p&gt;Autrement dit, avant d&apos;ajouter la moindre métrique métier, exposez combien de requêtes arrivent, combien tournent mal, et combien de temps elles durent. Tout le reste sert au diagnostic une fois qu&apos;un de ces trois signaux vous a alerté.&lt;/p&gt;
&lt;h2&gt;Le bon type de métrique pour la bonne question&lt;/h2&gt;
&lt;p&gt;Prometheus a quatre types de métriques, et le choix n&apos;est pas cosmétique. Un &lt;em&gt;counter&lt;/em&gt; ne fait que monter et se lit avec &lt;code&gt;rate()&lt;/code&gt; : c&apos;est le type du trafic et des erreurs, un &lt;code&gt;http_requests_total&lt;/code&gt; porteur d&apos;un label &lt;code&gt;status&lt;/code&gt;. Une &lt;em&gt;gauge&lt;/em&gt; est une valeur instantanée qui monte et descend, pour ce qui se compte à un instant donné (connexions en cours, profondeur d&apos;une file). Un &lt;em&gt;histogram&lt;/em&gt; échantillonne une distribution dans des tranches prédéfinies, et c&apos;est le bon outil pour la latence. Le &lt;em&gt;summary&lt;/em&gt; ressemble à l&apos;histogramme mais calcule ses quantiles autrement, avec une conséquence lourde qu&apos;on verra plus bas.&lt;/p&gt;
&lt;h2&gt;Un handler instrumenté, et les requêtes qui vont avec&lt;/h2&gt;
&lt;p&gt;Concrètement, votre bibliothèque cliente expose au scrape un format texte de ce genre :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# TYPE http_requests_total counter
http_requests_total{service=&quot;checkout&quot;,status=&quot;200&quot;} 128934
http_requests_total{service=&quot;checkout&quot;,status=&quot;500&quot;} 271

# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{service=&quot;checkout&quot;,le=&quot;0.1&quot;} 24054
http_request_duration_seconds_bucket{service=&quot;checkout&quot;,le=&quot;0.3&quot;} 33444
http_request_duration_seconds_bucket{service=&quot;checkout&quot;,le=&quot;1&quot;}   34101
http_request_duration_seconds_bucket{service=&quot;checkout&quot;,le=&quot;+Inf&quot;} 34115
http_request_duration_seconds_sum{service=&quot;checkout&quot;}   5342.7
http_request_duration_seconds_count{service=&quot;checkout&quot;} 34115&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;L&apos;histogramme expose des compteurs cumulatifs par borne &lt;code&gt;le&lt;/code&gt; (&lt;em&gt;less than or equal&lt;/em&gt;), plus un &lt;code&gt;_sum&lt;/code&gt; et un &lt;code&gt;_count&lt;/code&gt;. À partir de là, deux requêtes couvrent l&apos;essentiel :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# taux d&apos;erreur par service
sum(rate(http_requests_total{status=~&quot;5..&quot;}[5m])) by (service)
  /
sum(rate(http_requests_total[5m])) by (service)

# latence p99 par service
histogram_quantile(
  0.99,
  sum by (service, le) (rate(http_request_duration_seconds_bucket[5m]))
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ce sont précisément les métriques que consomment les &lt;a href=&quot;/blog/regles-alerting-prometheus-symptomes-vs-causes/&quot;&gt;alertes Prometheus par symptôme&lt;/a&gt; : instrumenter d&apos;abord, alerter ensuite. Une règle qui surveille le taux d&apos;erreur ou la latence n&apos;existe que parce que l&apos;application a été instrumentée pour les exposer.&lt;/p&gt;
&lt;h2&gt;Histogramme ou summary : pourquoi le quantile se calcule côté serveur&lt;/h2&gt;
&lt;p&gt;Voici la distinction qui décide du type à choisir. Un summary calcule ses quantiles dans le client, par instance, et publie directement un &lt;code&gt;quantile=&quot;0.99&quot;&lt;/code&gt;. Le problème : on ne peut ni additionner ni moyenner des quantiles. Le p99 de dix pods n&apos;est pas la moyenne de leurs dix p99. Dès que votre service tourne en plusieurs réplicas, un summary vous donne dix chiffres impossibles à recombiner en une latence de service.&lt;/p&gt;
&lt;p&gt;L&apos;histogramme fait l&apos;inverse : il expose des comptes bruts par tranche, et &lt;code&gt;histogram_quantile()&lt;/code&gt; reconstitue le quantile côté serveur, après que vous avez agrégé les buckets avec &lt;code&gt;sum by (le)&lt;/code&gt;. Comme tout tourne en plusieurs exemplaires sur Kubernetes, l&apos;histogramme est le choix par défaut. Son coût : il faut choisir les tranches à l&apos;avance.&lt;/p&gt;
&lt;h2&gt;Les buckets, l&apos;interpolation, et les pièges&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;histogram_quantile()&lt;/code&gt; interpole linéairement à l&apos;intérieur de la tranche où tombe le quantile. La précision de votre p99 est donc la largeur du bucket qui l&apos;entoure. Les tranches par défaut (de quelques millisecondes à dix secondes) encadrent rarement un objectif réel : placez les bornes là où vous prenez des décisions. Si votre cible est 300 ms, vous voulez des bornes vers &lt;code&gt;0.25&lt;/code&gt;, &lt;code&gt;0.3&lt;/code&gt;, &lt;code&gt;0.5&lt;/code&gt;. Et si le quantile tombe dans la tranche &lt;code&gt;+Inf&lt;/code&gt;, le résultat n&apos;est plus une valeur mais une borne inférieure, signe que vos buckets sont trop grossiers. Les histogrammes natifs, plus récents, suppriment ce choix de bornes au prix d&apos;un statut encore expérimental.&lt;/p&gt;
&lt;p&gt;Trois autres pièges reviennent. La cardinalité d&apos;abord : un label par utilisateur, par URL brute ou par identifiant de requête multiplie les séries par le nombre de buckets et fait exploser le coût de stockage, un sujet que détaille l&apos;entrée de glossaire sur la &lt;a href=&quot;/glossaire/cardinalite-metriques/&quot;&gt;cardinalité des métriques&lt;/a&gt;. Gardez des labels bornés : le gabarit de route, pas le chemin brut ; la classe de statut, pas le message complet. Moyenner des quantiles ensuite, &lt;code&gt;avg&lt;/code&gt; d&apos;un p99, qui ne veut rien dire : on agrège les buckets, jamais les quantiles. Trop peu de tranches autour de l&apos;objectif enfin, qui rend le p99 illisible au moment précis où il compte.&lt;/p&gt;
&lt;h2&gt;À retenir&lt;/h2&gt;
&lt;p&gt;Instrumentez trois choses depuis le code, trafic, erreurs et durée, et laissez la saturation à &lt;code&gt;node_exporter&lt;/code&gt;. Un counter pour les taux, un histogramme pour la latence, avec ses bornes posées autour de la décision que vous prenez vraiment. Préférez l&apos;histogramme au summary dès que le service tourne en plusieurs réplicas, et tenez la cardinalité des labels, parce qu&apos;une application instrumentée qui coûte plus qu&apos;elle n&apos;observe est un incident comme un autre. Cette &lt;a href=&quot;/glossaire/observabilite/&quot;&gt;observabilité&lt;/a&gt; utile est le socle de mon offre &lt;a href=&quot;/expertise/fiabilite-observabilite/&quot;&gt;fiabilité et observabilité&lt;/a&gt; ; d&apos;autres sujets dans la &lt;a href=&quot;/blog/categorie/observabilite/&quot;&gt;catégorie observabilité&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Observabilité</category><category>prometheus</category><category>instrumentation</category><category>golden-signals</category><category>histogramme</category><category>metriques</category></item><item><title>PodDisruptionBudget : contre quoi il protège vraiment</title><link>https://jm-dev.it/blog/kubernetes-pod-disruption-budget-fr/</link><guid isPermaLink="true">https://jm-dev.it/blog/kubernetes-pod-disruption-budget-fr/</guid><description>Ce qu&apos;un PodDisruptionBudget garde : évictions volontaires, pas crashs de nœud, minAvailable contre maxUnavailable, et les erreurs qui bloquent les drains.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- justification lang=fr : version française de l&apos;article EN &quot;kubernetes-pod-disruption-budget&quot;. Contenu de référence générique, sert le public francophone (relais LinkedIn FR) et le maillage vers le glossaire français. Paire liée via translationKey.&lt;/p&gt;
&lt;p&gt;Note de contenu (directive JM 2026-07-24) : tutoriel de référence, générique et vérifiable de bout en bout contre la documentation Kubernetes et le comportement des contrôleurs. Aucune anecdote de production par construction, ce n&apos;est pas un REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;Deux symptômes mènent à la documentation du PodDisruptionBudget, et ils pointent dans des directions opposées. Le premier : &lt;code&gt;kubectl drain node-7&lt;/code&gt; reste bloqué sur « evicting pod ... » sans jamais rendre la main, l&apos;autoscaler de cluster n&apos;arrive plus à réduire la voilure, une mise à jour de node pool cale une heure sur un seul nœud. Le second : une équipe ajoute un PDB en pensant garder son application en vie quand un nœud tombe, perd quand même la moitié de ses réplicas sur une panne matérielle, et se demande pourquoi le budget n&apos;a rien fait. Les deux viennent de la même confusion sur ce à quoi sert réellement un PDB.&lt;/p&gt;
&lt;h2&gt;Perturbations volontaires et involontaires&lt;/h2&gt;
&lt;p&gt;Kubernetes range les perturbations de pods en deux familles, et un PDB n&apos;en touche qu&apos;une seule.&lt;/p&gt;
&lt;p&gt;Les &lt;strong&gt;perturbations volontaires&lt;/strong&gt; passent par l&apos;API d&apos;éviction : &lt;code&gt;kubectl drain&lt;/code&gt;, l&apos;autoscaler qui retire un nœud sous-utilisé, une mise à jour progressive d&apos;un node pool managé, ou tout appel à &lt;code&gt;POST .../pods/&amp;lt;nom&amp;gt;/eviction&lt;/code&gt;. Ce sont des actions délibérées, déclenchées par un opérateur ou un contrôleur.&lt;/p&gt;
&lt;p&gt;Les &lt;strong&gt;perturbations involontaires&lt;/strong&gt; sont tout le reste : un kernel panic, une panne matérielle, le nœud qui manque de mémoire et le OOM killer qui emporte votre pod, une partition réseau, une préemption au profit d&apos;un pod plus prioritaire. Un cas surprend souvent : &lt;code&gt;kubectl delete pod&lt;/code&gt; est une suppression directe, pas une éviction, donc le PDB ne le voit pas non plus.&lt;/p&gt;
&lt;p&gt;Un PodDisruptionBudget est un garde-barrière devant l&apos;API d&apos;éviction, rien de plus. Il ne ressuscite pas un pod parti avec son nœud et ne planifie jamais de remplaçant. Si votre crainte porte sur les crashs de nœud, les leviers sont le nombre de réplicas, les &lt;code&gt;topologySpreadConstraints&lt;/code&gt; et l&apos;anti-affinité, pas un PDB. Ce qu&apos;un budget vous achète, c&apos;est une protection contre &lt;em&gt;votre propre outillage&lt;/em&gt; qui retirerait trop de pods d&apos;un coup pendant une opération planifiée. Il plafonne les évictions volontaires simultanées pour qu&apos;un drain ou une réduction d&apos;échelle ne vous fasse pas passer sous un plancher que vous fixez.&lt;/p&gt;
&lt;h2&gt;minAvailable contre maxUnavailable&lt;/h2&gt;
&lt;p&gt;Un PDB porte exactement l&apos;un de ces deux champs, jamais les deux.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;minAvailable&lt;/code&gt; : le nombre (ou pourcentage) de pods qui doivent rester sains. Une éviction est refusée si elle ferait passer le compte sous ce plancher.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxUnavailable&lt;/code&gt; : le nombre (ou pourcentage) de pods autorisés à être indisponibles simultanément. Une éviction est refusée dès que ce seuil est atteint.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Le contrôleur de perturbation ramène l&apos;un ou l&apos;autre à une seule valeur calculée. Il lit le nombre de pods &lt;em&gt;attendus&lt;/em&gt; auprès du contrôleur propriétaire (le &lt;code&gt;spec.replicas&lt;/code&gt; d&apos;un Deployment, par exemple), compte combien de pods sont actuellement &lt;strong&gt;sains&lt;/strong&gt; (au sens Ready, ce qui explique pourquoi votre &lt;a href=&quot;/blog/readiness-liveness-startup-probes-kubernetes/&quot;&gt;readiness probe&lt;/a&gt; alimente directement ce calcul), puis en déduit &lt;code&gt;disruptionsAllowed = sainsActuels − sainsSouhaités&lt;/code&gt;. Avec &lt;code&gt;minAvailable: 4&lt;/code&gt;, &lt;code&gt;sainsSouhaités&lt;/code&gt; vaut 4. Avec &lt;code&gt;maxUnavailable: 1&lt;/code&gt; sur 6 réplicas, il vaut 5. Quand &lt;code&gt;disruptionsAllowed&lt;/code&gt; tombe à 0, l&apos;API d&apos;éviction renvoie un HTTP 429 et &lt;code&gt;kubectl drain&lt;/code&gt; patiente puis réessaie jusqu&apos;à ce que le budget se rouvre.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api
spec:
  minAvailable: 4          # ne jamais descendre sous 4 pods Ready
  selector:
    matchLabels:
      app: api
  # unhealthyPodEvictionPolicy: AlwaysAllow   # voir les pièges&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get pdb api
NAME   MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
api    4               N/A               2                     3d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ALLOWED DISRUPTIONS: 2&lt;/code&gt; signifie que deux pods peuvent être évincés à l&apos;instant présent ; drainez un nœud qui porte trois pods &lt;code&gt;api&lt;/code&gt; et la troisième éviction attendra.&lt;/p&gt;
&lt;p&gt;Privilégiez &lt;code&gt;maxUnavailable&lt;/code&gt; quand vous passez à l&apos;échelle horizontalement : il reste correct quand &lt;code&gt;replicas&lt;/code&gt; varie, là où un &lt;code&gt;minAvailable&lt;/code&gt; fixe devient silencieusement plus strict à mesure que vous réduisez, et plus laxiste à mesure que vous montez.&lt;/p&gt;
&lt;h2&gt;Interaction avec les drains et l&apos;autoscaler&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;kubectl drain&lt;/code&gt; cordonne le nœud, puis évince ses pods un par un via l&apos;API d&apos;éviction, en respectant chaque PDB concerné. Si un budget est épuisé, le drain n&apos;échoue pas : il boucle, attendant que les pods soient replanifiés ailleurs et repassent Ready pour que le budget se rouvre. Un drain qui « bloque » est presque toujours un PDB qui fait son travail pendant que les remplaçants ne montent pas (pas de place pour planifier, ou un rollout coincé).&lt;/p&gt;
&lt;p&gt;L&apos;autoscaler de cluster s&apos;appuie sur la même API pour réduire la voilure. Avant de retirer un nœud sous-utilisé, il vérifie que tous ses pods peuvent être évincés sans violer un PDB. Sinon, le nœud est marqué non supprimable et reste en place. Un PDB qui n&apos;autorise jamais aucune perturbation cloue donc des nœuds au sol et sabote la consolidation, ce qui se traduit par une facture cloud qui ne baisse jamais.&lt;/p&gt;
&lt;h2&gt;Erreurs de configuration classiques&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;maxUnavailable: 0&lt;/code&gt; ou &lt;code&gt;minAvailable: 100%&lt;/code&gt;.&lt;/strong&gt; Interdit &lt;em&gt;toute&lt;/em&gt; éviction volontaire. Chaque drain bloque indéfiniment et l&apos;autoscaler ne peut jamais récupérer le nœud. Cela ressemble à une sécurité maximale ; c&apos;est un blocage opérationnel permanent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Un seul réplica avec &lt;code&gt;minAvailable: 1&lt;/code&gt;.&lt;/strong&gt; L&apos;unique pod ne peut jamais être évincé, donc tout drain de son nœud reste bloqué. Un PDB ne rend pas un workload mono-réplica hautement disponible ; il le rend seulement indrainable. La &lt;a href=&quot;/glossaire/haute-disponibilite/&quot;&gt;haute disponibilité&lt;/a&gt; commence par plusieurs réplicas, pas par un budget.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Des pourcentages sur de petits comptes de réplicas.&lt;/strong&gt; Les pourcentages sont arrondis &lt;em&gt;au supérieur&lt;/em&gt;, au pod entier le plus proche. &lt;code&gt;minAvailable: 50%&lt;/code&gt; sur 3 réplicas donne 2, ce qui ne laisse la place qu&apos;à une seule éviction, pas à la « moitié » imaginée. Faites le calcul pour votre nombre réel de réplicas avant de faire confiance à l&apos;intention.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Un sélecteur qui vise les mauvais pods.&lt;/strong&gt; Un PDB dont le sélecteur ne matche rien ne protège rien, en silence. Un sélecteur qui matche des pods appartenant à plusieurs contrôleurs empêche le contrôleur de calculer un nombre attendu à partir d&apos;un pourcentage ou de &lt;code&gt;maxUnavailable&lt;/code&gt;, et il refusera alors les évictions par prudence. Un PDB par workload, avec un sélecteur qui vise exactement ce workload.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Des pods cassés que vous ne pouvez pas évincer.&lt;/strong&gt; Par défaut (&lt;code&gt;unhealthyPodEvictionPolicy: IfHealthyBudget&lt;/code&gt;), un pod non-Ready ne peut être évincé que tant que le budget est satisfait. Si vous êtes déjà sous le plancher parce que des pods crashent, vous ne pouvez même plus évincer les pods cassés pour drainer le nœud : la récupération se bloque. Poser &lt;code&gt;unhealthyPodEvictionPolicy: AlwaysAllow&lt;/code&gt; autorise l&apos;éviction des pods non-Ready dans tous les cas, ce qui est généralement le comportement souhaité pour un drain fluide.&lt;/p&gt;
&lt;h2&gt;À retenir&lt;/h2&gt;
&lt;p&gt;Un PodDisruptionBudget est un limiteur de débit sur les évictions volontaires, pas une garantie de disponibilité. Il protège les opérations planifiées (drains, réductions par l&apos;autoscaler, mises à jour de nœuds) contre le retrait de plus de pods que vous n&apos;autorisez ; il ne fait rien contre les crashs de nœud, les OOM kills ou &lt;code&gt;kubectl delete pod&lt;/code&gt;. Réglez-le avec &lt;code&gt;maxUnavailable&lt;/code&gt; sur les workloads scalables, jamais à une valeur qui autorise zéro perturbation, jamais sur un réplica unique, et pensez à &lt;code&gt;AlwaysAllow&lt;/code&gt; pour que des pods cassés ne coincent pas vos drains. Maîtriser ces primitives, c&apos;est ce qui rend la maintenance de cluster ennuyeuse, et c&apos;est tout l&apos;objet de la &lt;a href=&quot;/expertise/fiabilite-observabilite/&quot;&gt;fiabilité et de l&apos;observabilité&lt;/a&gt;. Plus de contenu Kubernetes dans &lt;a href=&quot;/blog/categorie/kubernetes/&quot;&gt;la catégorie kubernetes&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Kubernetes</category><category>pod-disruption-budget</category><category>evictions</category><category>cluster-autoscaler</category><category>node-drain</category><category>haute-disponibilite</category></item><item><title>PodDisruptionBudget: What It Actually Protects Against</title><link>https://jm-dev.it/blog/kubernetes-pod-disruption-budget/</link><guid isPermaLink="true">https://jm-dev.it/blog/kubernetes-pod-disruption-budget/</guid><description>What a Kubernetes PodDisruptionBudget guards (voluntary evictions, not node crashes), minAvailable vs maxUnavailable, and the misconfigs that stall drains.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=en justification: &quot;poddisruptionbudget&quot;, &quot;pdb minavailable vs maxunavailable&quot; and &quot;kubectl drain stuck pdb&quot; are global technical queries typed almost exclusively in English by engineers mid-operation. Consistent with the strategy of publishing pointed reference tutorials in English.&lt;/p&gt;
&lt;p&gt;Content note (JM directive 2026-07-24): reference tutorial, generic and verifiable end to end against Kubernetes documentation and controller behavior. No production anecdotes by design, this is not a REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;Two symptoms send people to the PodDisruptionBudget docs, and they point in opposite directions. The first: &lt;code&gt;kubectl drain node-7&lt;/code&gt; hangs on &quot;evicting pod ...&quot; and never returns, the cluster autoscaler logs that it cannot scale down, a node-pool upgrade stalls on a single node for an hour. The second: a team adds a PDB expecting it to keep their app alive when a node dies, then loses half their replicas to a hardware failure anyway and wonders why the budget did nothing. Both come from the same misreading of what a PDB is actually for.&lt;/p&gt;
&lt;h2&gt;Voluntary versus involuntary disruptions&lt;/h2&gt;
&lt;p&gt;Kubernetes splits pod disruptions into two families, and a PDB touches exactly one of them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Voluntary disruptions&lt;/strong&gt; go through the Eviction API. That means &lt;code&gt;kubectl drain&lt;/code&gt;, the cluster autoscaler removing an underused node, a managed node-pool rolling upgrade, or anything issuing a &lt;code&gt;POST .../pods/&amp;lt;name&amp;gt;/eviction&lt;/code&gt;. These are deliberate actions initiated by an operator or a controller.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Involuntary disruptions&lt;/strong&gt; are everything else: a kernel panic, hardware failure, the node running out of memory and the OOM killer taking your pod, a network partition, a preemption for a higher-priority pod. Note one entry that surprises people: &lt;code&gt;kubectl delete pod&lt;/code&gt; is a direct delete, not an eviction, so the PDB never sees it either.&lt;/p&gt;
&lt;p&gt;A PodDisruptionBudget is a gate in front of the Eviction API and nothing more. It cannot resurrect a pod that went down with its node, and it never schedules a replacement. If your concern is node crashes, the levers are replica count, &lt;code&gt;topologySpreadConstraints&lt;/code&gt; and anti-affinity, not a PDB. What a budget buys you is protection against &lt;em&gt;your own tooling&lt;/em&gt; removing too many pods at once during planned operations. It caps concurrent voluntary evictions so a drain or a scale-down cannot drop you below a floor you set.&lt;/p&gt;
&lt;h2&gt;minAvailable versus maxUnavailable&lt;/h2&gt;
&lt;p&gt;A PDB carries exactly one of two fields; they are mutually exclusive.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;minAvailable&lt;/code&gt;: the number (or percentage) of pods that must stay healthy. An eviction is refused if it would push the count below this floor.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxUnavailable&lt;/code&gt;: the number (or percentage) of pods allowed to be down at once. An eviction is refused once that many are already unavailable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The disruption controller turns either one into a single computed value. It reads the &lt;em&gt;expected&lt;/em&gt; pod count from the owning controller (a Deployment&apos;s &lt;code&gt;spec.replicas&lt;/code&gt;, for example), counts how many pods are currently &lt;strong&gt;healthy&lt;/strong&gt; (healthy meaning Ready, which is why your &lt;a href=&quot;/blog/kubernetes-readiness-liveness-startup-probes/&quot;&gt;readiness probe&lt;/a&gt; directly feeds this math), and derives &lt;code&gt;disruptionsAllowed = currentHealthy − desiredHealthy&lt;/code&gt;. With &lt;code&gt;minAvailable: 4&lt;/code&gt;, &lt;code&gt;desiredHealthy&lt;/code&gt; is 4. With &lt;code&gt;maxUnavailable: 1&lt;/code&gt; on 6 replicas, &lt;code&gt;desiredHealthy&lt;/code&gt; is 5. When &lt;code&gt;disruptionsAllowed&lt;/code&gt; reaches 0, the Eviction API returns HTTP 429 and &lt;code&gt;kubectl drain&lt;/code&gt; backs off and retries until the budget reopens.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api
spec:
  minAvailable: 4          # never drop below 4 Ready pods
  selector:
    matchLabels:
      app: api
  # unhealthyPodEvictionPolicy: AlwaysAllow   # see gotchas&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get pdb api
NAME   MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS   AGE
api    4               N/A               2                     3d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ALLOWED DISRUPTIONS: 2&lt;/code&gt; means two pods can be evicted right now; drain a node holding three &lt;code&gt;api&lt;/code&gt; pods and the third eviction waits.&lt;/p&gt;
&lt;p&gt;Prefer &lt;code&gt;maxUnavailable&lt;/code&gt; when you scale horizontally: it stays correct as &lt;code&gt;replicas&lt;/code&gt; changes, whereas a fixed &lt;code&gt;minAvailable&lt;/code&gt; silently becomes stricter as you scale down and looser as you scale up.&lt;/p&gt;
&lt;h2&gt;Interaction with drains and the cluster autoscaler&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;kubectl drain&lt;/code&gt; cordons the node, then evicts its pods one by one through the Eviction API, honoring every matching PDB. If a budget is exhausted, drain does not fail; it loops, waiting for pods to reschedule elsewhere and become Ready so the budget reopens. A drain that &quot;hangs&quot; is almost always a PDB doing its job while replacements are not coming up (no room to schedule, or a stuck rollout).&lt;/p&gt;
&lt;p&gt;The cluster autoscaler leans on the same API for scale-down. Before removing an underused node it checks whether every pod on it can be evicted without violating a PDB. If not, the node is marked unremovable and stays. A PDB that can never allow a disruption therefore pins nodes in place and defeats consolidation, which shows up as a cloud bill that never shrinks.&lt;/p&gt;
&lt;h2&gt;Common misconfigurations&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;maxUnavailable: 0&lt;/code&gt; or &lt;code&gt;minAvailable: 100%&lt;/code&gt;.&lt;/strong&gt; This forbids &lt;em&gt;all&lt;/em&gt; voluntary evictions. Every drain blocks forever and the autoscaler can never reclaim the node. It reads like maximum safety; it is a permanent operational deadlock.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A single replica with &lt;code&gt;minAvailable: 1&lt;/code&gt;.&lt;/strong&gt; The one pod can never be evicted, so any drain of its node hangs indefinitely. A PDB cannot make a single-replica workload highly available; it can only make it un-drainable. Raise the replica count first, or accept that this pod takes brief downtime during node operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Percentages on small replica counts.&lt;/strong&gt; Percentages round &lt;em&gt;up&lt;/em&gt; to the nearest whole pod. &lt;code&gt;minAvailable: 50%&lt;/code&gt; on 3 replicas resolves to 2, leaving room for only one eviction, not the &quot;half&quot; you pictured. Do the arithmetic for your actual replica count before trusting the intent.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A selector that matches the wrong pods.&lt;/strong&gt; A PDB whose selector matches nothing silently guards nothing. A selector matching pods owned by several controllers leaves the disruption controller unable to compute an expected count from a percentage or &lt;code&gt;maxUnavailable&lt;/code&gt;, and it will refuse evictions conservatively. One PDB per workload, selector matching exactly that workload.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Broken pods you cannot evict.&lt;/strong&gt; By default (&lt;code&gt;unhealthyPodEvictionPolicy: IfHealthyBudget&lt;/code&gt;), a not-Ready pod can be evicted only while the budget is currently met. If you are already below the floor because pods are crashing, you cannot even evict the broken ones to drain the node, so recovery deadlocks. Setting &lt;code&gt;unhealthyPodEvictionPolicy: AlwaysAllow&lt;/code&gt; lets not-Ready pods be evicted regardless, which is usually what you want for a smooth drain.&lt;/p&gt;
&lt;h2&gt;What to remember&lt;/h2&gt;
&lt;p&gt;A PodDisruptionBudget is a rate limiter on voluntary evictions, not an availability guarantee. It protects planned operations (drains, autoscaler scale-down, node upgrades) from removing more pods than you allow; it does nothing about node crashes, OOM kills, or &lt;code&gt;kubectl delete pod&lt;/code&gt;. Set it with &lt;code&gt;maxUnavailable&lt;/code&gt; on scalable workloads, never at a value that allows zero disruptions, never on a single replica, and consider &lt;code&gt;AlwaysAllow&lt;/code&gt; so broken pods do not wedge your drains. Getting these primitives right is what makes cluster maintenance boring, which is the whole point of &lt;a href=&quot;/expertise/fiabilite-observabilite/&quot;&gt;reliability engineering&lt;/a&gt;. More Kubernetes material in &lt;a href=&quot;/blog/categorie/kubernetes/&quot;&gt;the kubernetes category&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Kubernetes</category><category>pod-disruption-budget</category><category>evictions</category><category>cluster-autoscaler</category><category>node-drain</category><category>high-availability</category></item><item><title>Readiness, Liveness and Startup Probes in Kubernetes</title><link>https://jm-dev.it/blog/kubernetes-readiness-liveness-startup-probes/</link><guid isPermaLink="true">https://jm-dev.it/blog/kubernetes-readiness-liveness-startup-probes/</guid><description>What each Kubernetes probe type actually gates, the failure modes of copy-pasted probe configs, and a decision table for setting each parameter.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=en justification: &quot;kubernetes readiness vs liveness probe&quot; and &quot;startupProbe&quot; are global technical queries searched almost exclusively in English; the audience is the international SRE/platform crowd. Consistent with the site strategy of publishing pointed technical tutorials in English.&lt;/p&gt;
&lt;p&gt;Content note (JM directive 2026-07-24): reference tutorial, generic and verifiable end to end against the Kubernetes documentation and source behavior. No production anecdotes by design — this is not a REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;Kubernetes has three probe types (readiness, liveness and startup), and they answer three different questions. Most probe-related outages come from treating them as one mechanism: the same &lt;code&gt;/healthz&lt;/code&gt; endpoint pasted into both &lt;code&gt;readinessProbe&lt;/code&gt; and &lt;code&gt;livenessProbe&lt;/code&gt;, no &lt;code&gt;startupProbe&lt;/code&gt; on an application that takes a minute to boot, and default timeouts nobody read. This is a reference for what each probe actually gates, the failure modes that follow from mixing them up, and how to choose the parameters.&lt;/p&gt;
&lt;h2&gt;Three probes, three different questions&lt;/h2&gt;
&lt;p&gt;All three probes share the same YAML shape and the same handlers. What differs is what the kubelet does with the result.&lt;/p&gt;
&lt;h3&gt;readinessProbe: can this pod serve traffic right now?&lt;/h3&gt;
&lt;p&gt;Readiness gates &lt;strong&gt;load balancing&lt;/strong&gt;. While the probe passes, the pod&apos;s IP is listed in the EndpointSlices of every Service that selects it. When it fails (&lt;code&gt;failureThreshold&lt;/code&gt; consecutive times), the pod is marked not-ready and removed from load balancing. The container is not touched: it keeps running, and the moment the probe passes again the pod goes back into rotation.&lt;/p&gt;
&lt;p&gt;Readiness is the only probe designed to fail during normal operation. A pod that is momentarily overloaded, waiting on a cache warm-up, or draining before shutdown is exactly what &quot;not ready&quot; means. It runs for the whole life of the container, not just at startup.&lt;/p&gt;
&lt;p&gt;Readiness failures also gate rollouts: a rolling update only proceeds past a new pod once that pod reports Ready. A wrong readiness check therefore has a second blast radius: it can let a rolling update replace working pods with broken ones that claim to be fine.&lt;/p&gt;
&lt;h3&gt;livenessProbe: is this process wedged beyond recovery?&lt;/h3&gt;
&lt;p&gt;Liveness gates &lt;strong&gt;container restarts&lt;/strong&gt;. When it fails &lt;code&gt;failureThreshold&lt;/code&gt; consecutive times, the kubelet kills the container and restarts it according to the pod&apos;s &lt;code&gt;restartPolicy&lt;/code&gt;, with exponential backoff (&lt;code&gt;CrashLoopBackOff&lt;/code&gt; when it keeps happening).&lt;/p&gt;
&lt;p&gt;That is a brutal remedy, which is why the question liveness answers must be narrow: is the process deadlocked, wedged, or otherwise unable to make progress in a way that only a restart fixes? Anything softer than that belongs in readiness. A liveness probe should almost never fail in a healthy fleet. If yours fires regularly, either the app has a real defect or the probe is checking the wrong thing.&lt;/p&gt;
&lt;h3&gt;startupProbe: has this container finished booting?&lt;/h3&gt;
&lt;p&gt;Startup gates &lt;strong&gt;the other two probes&lt;/strong&gt;. While a &lt;code&gt;startupProbe&lt;/code&gt; is defined and has not yet succeeded, the kubelet does not run liveness or readiness at all. Once it succeeds a single time, it never runs again and the other probes take over.&lt;/p&gt;
&lt;p&gt;Its purpose is slow-starting applications. The maximum boot time you grant is &lt;code&gt;failureThreshold × periodSeconds&lt;/code&gt; (plus &lt;code&gt;initialDelaySeconds&lt;/code&gt; if set); if the probe still has not succeeded by then, the container is killed and restarted. That budget lives in one place, on one probe, instead of being smeared across &lt;code&gt;initialDelaySeconds&lt;/code&gt; on the other two.&lt;/p&gt;
&lt;h2&gt;Probe mechanics and parameters&lt;/h2&gt;
&lt;p&gt;Each probe uses one of four handlers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;httpGet&lt;/code&gt;: success is an HTTP status ≥ 200 and &amp;lt; 400. The most common choice for web workloads.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tcpSocket&lt;/code&gt;: success is completing a TCP connection. Proves a port is open, nothing about the application behind it.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exec&lt;/code&gt;: runs a command in the container; exit code 0 is success. Flexible, and the most expensive, since each execution forks inside the container.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grpc&lt;/code&gt;: uses the standard gRPC Health Checking Protocol (GA since Kubernetes 1.27). The right answer for gRPC servers, replacing the old &lt;code&gt;grpc_health_probe&lt;/code&gt; exec workaround.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The timing parameters, with their defaults:&lt;/p&gt;
&lt;p&gt;| Parameter | Default | Meaning | |---|---|---| | &lt;code&gt;initialDelaySeconds&lt;/code&gt; | 0 | Wait before the first check | | &lt;code&gt;periodSeconds&lt;/code&gt; | 10 | Interval between checks | | &lt;code&gt;timeoutSeconds&lt;/code&gt; | 1 | Time allowed per check | | &lt;code&gt;failureThreshold&lt;/code&gt; | 3 | Consecutive failures before acting | | &lt;code&gt;successThreshold&lt;/code&gt; | 1 | Consecutive successes to recover (must be 1 for liveness and startup) |&lt;/p&gt;
&lt;p&gt;Two defaults deserve attention. &lt;code&gt;timeoutSeconds: 1&lt;/code&gt; is aggressive: an exec probe that forks a shell, or an HTTP handler that touches anything slower than memory, can blow a one-second budget under load, and a probe timeout counts as a failure. &lt;code&gt;periodSeconds: 10&lt;/code&gt; with &lt;code&gt;failureThreshold: 3&lt;/code&gt; means detection takes up to 30 seconds, which is often slower than people assume their &quot;health checks&quot; react.&lt;/p&gt;
&lt;h2&gt;The failure modes of copy-pasted probes&lt;/h2&gt;
&lt;p&gt;These are the patterns that turn probes from a safety mechanism into an outage amplifier. All of them come from copying a working-looking block without asking which question it answers.&lt;/p&gt;
&lt;h3&gt;Liveness and readiness pointing at the same endpoint&lt;/h3&gt;
&lt;p&gt;The most common one. If &lt;code&gt;/healthz&lt;/code&gt; fails because a downstream dependency is slow, readiness correctly pulls the pod out of rotation. Liveness, watching the same endpoint, then restarts a process that was never broken. Multiply by every replica watching the same slow dependency and you get a coordinated restart storm: the dependency recovers, but your application tier is now in &lt;code&gt;CrashLoopBackOff&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The two probes need different endpoints because they answer different questions. &lt;code&gt;/ready&lt;/code&gt; may consult whatever the app needs to serve a request; &lt;code&gt;/live&lt;/code&gt; should verify only that the process itself is responsive.&lt;/p&gt;
&lt;h3&gt;Dependency checks inside the liveness probe&lt;/h3&gt;
&lt;p&gt;The generalized version of the previous mistake. A liveness handler that pings the database, checks a message broker, or calls another service converts every dependency blip into a mass restart of the dependents. Restarting your API pods does not fix your database; it adds connection churn and cold caches on top of the original problem, and it does so on every replica at roughly the same time.&lt;/p&gt;
&lt;p&gt;Dependency awareness belongs in readiness, and even there with care: if every replica&apos;s readiness fails simultaneously because a shared dependency is down, the Service ends up with zero endpoints and clients get connection refused instead of a degraded response. For a dependency the app could partially work without, returning 200 with degraded behavior is often better than failing the probe.&lt;/p&gt;
&lt;h3&gt;No startupProbe on a slow-starting application&lt;/h3&gt;
&lt;p&gt;Without a startup probe, liveness starts checking after &lt;code&gt;initialDelaySeconds&lt;/code&gt;. For an application with variable boot time (JVMs warming up, migrations running, large caches loading), that forces a bad choice: either set &lt;code&gt;initialDelaySeconds&lt;/code&gt; high enough for the worst case, and eat that delay on every restart even the fast ones, or set it optimistically and watch the kubelet kill containers that were 5 seconds from finishing boot. The kill leads to a restart, which boots slowly again, which gets killed again: a startup crash loop caused entirely by probe configuration.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;startupProbe&lt;/code&gt; with a generous &lt;code&gt;failureThreshold&lt;/code&gt; solves this cleanly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;startupProbe:
  httpGet:
    path: /live
    port: 8080
  periodSeconds: 5
  failureThreshold: 24      # up to 120 s to boot&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The app gets up to two minutes to come up, checked every five seconds, so a fast boot is detected fast. Once it is up, liveness runs with tight, steady-state settings instead of settings inflated to cover booting.&lt;/p&gt;
&lt;h3&gt;No readinessProbe at all&lt;/h3&gt;
&lt;p&gt;Without a readiness probe, a pod counts as Ready as soon as its containers are running. Traffic arrives before the application listens, and rolling updates make progress based on that lie: old pods get killed because new ones claim to be &quot;Ready&quot;. The result is a burst of errors on every deploy that no amount of liveness tuning can fix.&lt;/p&gt;
&lt;h3&gt;tcpSocket where httpGet was needed&lt;/h3&gt;
&lt;p&gt;A &lt;code&gt;tcpSocket&lt;/code&gt; readiness probe on a web application proves the socket is open. An application can accept TCP connections long before its routes are wired, and long after its event loop has wedged. If the app speaks HTTP, probe it over HTTP.&lt;/p&gt;
&lt;h2&gt;A complete example&lt;/h2&gt;
&lt;p&gt;The three probes together, on a hypothetical app with an occasionally slow boot:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;containers:
  - name: app
    ports:
      - containerPort: 8080
    startupProbe:
      httpGet:
        path: /live
        port: 8080
      periodSeconds: 5
      failureThreshold: 24        # boot budget: 120 s
    readinessProbe:
      httpGet:
        path: /ready              # may check what serving requires
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 2         # out of rotation within ~10 s
    livenessProbe:
      httpGet:
        path: /live               # process responsive, nothing else
        port: 8080
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3         # restart only after ~30 s of silence&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note the asymmetry: readiness reacts faster than liveness (&lt;code&gt;failureThreshold: 2&lt;/code&gt; vs &lt;code&gt;3&lt;/code&gt;, shorter period). Being pulled from load balancing is cheap and reversible; a restart is neither. You want the cheap remedy to trigger first.&lt;/p&gt;
&lt;h2&gt;Decision table&lt;/h2&gt;
&lt;p&gt;| Question | Probe | On failure | Endpoint should check | |---|---|---|---| | Can this pod take traffic now? | &lt;code&gt;readinessProbe&lt;/code&gt; | Removed from Service endpoints | App fully initialized; critical dependencies, with care | | Is the process wedged beyond recovery? | &lt;code&gt;livenessProbe&lt;/code&gt; | Container killed and restarted | Process/event loop responsiveness only, never dependencies | | Has the container finished booting? | &lt;code&gt;startupProbe&lt;/code&gt; | Container killed and restarted | Same handler as liveness, generous &lt;code&gt;failureThreshold&lt;/code&gt; |&lt;/p&gt;
&lt;p&gt;And for the parameters:&lt;/p&gt;
&lt;p&gt;| Situation | Setting | |---|---| | Boot time is variable or &amp;gt; ~10 s | Add a &lt;code&gt;startupProbe&lt;/code&gt;; keep &lt;code&gt;initialDelaySeconds&lt;/code&gt; at 0 elsewhere | | Handler does real work (exec, disk, downstream call) | Raise &lt;code&gt;timeoutSeconds&lt;/code&gt; above the default 1 s | | Traffic must leave a bad pod quickly | Lower readiness &lt;code&gt;periodSeconds&lt;/code&gt; × &lt;code&gt;failureThreshold&lt;/code&gt; product | | Restarts are expensive (caches, connections) | Raise liveness &lt;code&gt;failureThreshold&lt;/code&gt;; make the handler trivial | | gRPC server | Use the &lt;code&gt;grpc&lt;/code&gt; handler, not an exec workaround |&lt;/p&gt;
&lt;h2&gt;What probes do not solve&lt;/h2&gt;
&lt;p&gt;Two boundaries worth knowing. First, a correct &lt;code&gt;readinessProbe&lt;/code&gt; does not give you zero-downtime deploys: pod termination during a rollout is a propagation race that readiness cannot influence, and fixing it involves &lt;code&gt;preStop&lt;/code&gt; hooks and SIGTERM handling — the moving parts are summarized in the glossary entry on &lt;a href=&quot;/glossaire/rolling-deployment/&quot;&gt;rolling deployments&lt;/a&gt; (in French). Second, probes keep working under a default-deny NetworkPolicy: the kubelet probes pods from the node itself and CNIs exempt that host-local traffic, a detail that matters when rolling out a &lt;a href=&quot;/glossaire/network-policy/&quot;&gt;default-deny NetworkPolicy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The mental model that survives contact with production is small: readiness is a routing decision, liveness is a restart decision, startup is a boot-time budget. Configure each one to answer only its own question, give the cheap remedy a faster trigger than the brutal one, and read the defaults before trusting them. Getting probes to answer honestly is one small piece of a larger discipline, making a platform surface its own failures before users do, which is what my &lt;a href=&quot;/expertise/fiabilite-observabilite/&quot;&gt;reliability and observability work&lt;/a&gt; (in French) is about. More Kubernetes material in &lt;a href=&quot;/blog/categorie/kubernetes/&quot;&gt;the kubernetes category&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Kubernetes</category><category>readiness-probe</category><category>liveness-probe</category><category>startup-probe</category><category>health-checks</category></item><item><title>Kubernetes requests vs limits: throttling, OOMKill and QoS</title><link>https://jm-dev.it/blog/kubernetes-resource-requests-limits/</link><guid isPermaLink="true">https://jm-dev.it/blog/kubernetes-resource-requests-limits/</guid><description>What actually happens when a pod crosses its CPU or memory limit, how QoS classes are derived, the no-limits debate, and a method to set sane defaults.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=en justification: &quot;kubernetes requests vs limits&quot;, &quot;cpu throttling&quot;, &quot;OOMKilled&quot; are global technical queries searched almost entirely in English; the audience is the international SRE/platform crowd. Consistent with the site strategy of publishing pointed technical tutorials in English. Paired with the French version via translationKey.&lt;/p&gt;
&lt;p&gt;Content note (JM directive 2026-07-24): reference tutorial, generic and verifiable end to end against the Kubernetes documentation, cgroup behavior and cAdvisor metrics. No production anecdotes by design, this is not a REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;A pod runs fine, then vanishes with &lt;code&gt;OOMKilled&lt;/code&gt; and exit code 137. A different pod is slow under load while the node it sits on is 70% idle. Both symptoms trace back to the same two fields most manifests set by reflex and few people read closely: &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;limits&lt;/code&gt;. They are not two dials for the same thing. They act at different moments, on different subsystems, with very different consequences when you cross them.&lt;/p&gt;
&lt;h2&gt;Requests and limits do different jobs&lt;/h2&gt;
&lt;p&gt;A &lt;strong&gt;request&lt;/strong&gt; is a scheduling input. The scheduler bin-packs pods by summing their requests against a node&apos;s allocatable capacity; a request is a reservation and the floor the pod is guaranteed to get. It never caps usage.&lt;/p&gt;
&lt;p&gt;A &lt;strong&gt;limit&lt;/strong&gt; is a runtime ceiling enforced by the kubelet and the kernel through cgroups. What happens when a container hits that ceiling depends entirely on which resource you are talking about, because CPU and memory behave in opposite ways.&lt;/p&gt;
&lt;h2&gt;Crossing the CPU limit: throttling&lt;/h2&gt;
&lt;p&gt;CPU is a compressible resource. The kernel can hand a process fewer cycles without killing it.&lt;/p&gt;
&lt;p&gt;The CPU &lt;strong&gt;request&lt;/strong&gt; becomes a cgroup weight (&lt;code&gt;cpu.shares&lt;/code&gt; under cgroup v1, &lt;code&gt;cpu.weight&lt;/code&gt; under v2, where one core maps to 1024 shares). That weight only matters under contention: it decides how idle-starved processes split a saturated CPU. When the node has spare capacity, everyone runs freely regardless of requests.&lt;/p&gt;
&lt;p&gt;The CPU &lt;strong&gt;limit&lt;/strong&gt; becomes CFS bandwidth control: a quota per scheduling period (&lt;code&gt;cpu.cfs_quota_us&lt;/code&gt; / &lt;code&gt;cpu.cfs_period_us&lt;/code&gt;, or &lt;code&gt;cpu.max&lt;/code&gt; on v2), with a default 100 ms period. A limit of &lt;code&gt;500m&lt;/code&gt; grants 50 ms of CPU per 100 ms window. Spend that budget before the window closes and the container is throttled, parked until the next period. No kill, just latency.&lt;/p&gt;
&lt;p&gt;The trap lives in that per-period enforcement. Throttling fires whether or not the node is busy, so a bursty, latency-sensitive workload can be throttled hard while the machine sits mostly idle. Watch the ratio of &lt;code&gt;container_cpu_cfs_throttled_periods_total&lt;/code&gt; to &lt;code&gt;container_cpu_cfs_periods_total&lt;/code&gt;; a high value is CFS throttling, not a slow application.&lt;/p&gt;
&lt;h2&gt;Crossing the memory limit: OOMKill&lt;/h2&gt;
&lt;p&gt;Memory is incompressible. You cannot ask a process to hold less RAM the way you can give it fewer cycles.&lt;/p&gt;
&lt;p&gt;The memory &lt;strong&gt;request&lt;/strong&gt; feeds scheduling and eviction ranking; it is not a cap. The memory &lt;strong&gt;limit&lt;/strong&gt; is a hard cap (&lt;code&gt;memory.max&lt;/code&gt; / &lt;code&gt;memory.limit_in_bytes&lt;/code&gt;). Touch one byte past it and the cgroup OOM killer sends SIGKILL to a process inside the cgroup. Kubernetes reports the container as &lt;code&gt;OOMKilled&lt;/code&gt;, exit code 137 (128 + SIGKILL), restarts it per &lt;code&gt;restartPolicy&lt;/code&gt;, and lands in &lt;code&gt;CrashLoopBackOff&lt;/code&gt; if it keeps happening.&lt;/p&gt;
&lt;p&gt;Two distinct memory events are easy to confuse. Hitting your own container&apos;s limit is a cgroup OOMKill: kernel-level, immediate, scoped to that pod. The node running low on memory overall triggers kubelet eviction instead, which ranks victims by QoS class and by how far each pod has grown above its request.&lt;/p&gt;
&lt;h2&gt;QoS classes are derived, not declared&lt;/h2&gt;
&lt;p&gt;Kubernetes assigns each pod a class from what you set:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Guaranteed&lt;/strong&gt;: every container sets &lt;code&gt;requests == limits&lt;/code&gt; for both CPU and memory. It gets a strongly negative &lt;code&gt;oom_score_adj&lt;/code&gt; (-997) and is evicted last.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Burstable&lt;/strong&gt;: at least one request or limit is set, but the pod is not Guaranteed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BestEffort&lt;/strong&gt;: nothing set at all. It gets &lt;code&gt;oom_score_adj&lt;/code&gt; 1000 and is the first to be evicted and the first the kernel kills under memory pressure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The class is not cosmetic. Under node pressure it decides who dies first, and that ordering is the whole point of setting requests honestly.&lt;/p&gt;
&lt;h2&gt;The &quot;no CPU limits&quot; argument&lt;/h2&gt;
&lt;p&gt;This is a genuine, ongoing debate. The case for dropping CPU limits: CFS throttling costs latency even on an idle node, and a CPU request already guarantees a proportional share under contention, so a limit often just buys you throttling you never needed. The case for keeping them: predictable capacity planning, protection from a runaway neighbour, and the fact that CPU limits are required for the Guaranteed class and for static CPU pinning.&lt;/p&gt;
&lt;p&gt;The pragmatic middle most platform teams land on: almost always set memory &lt;code&gt;request == limit&lt;/code&gt; (incompressible resource, you want a hard and predictable cap), always set a CPU request, and treat CPU limits as optional, applied only where a specific policy demands one.&lt;/p&gt;
&lt;h2&gt;A method for setting defaults&lt;/h2&gt;
&lt;p&gt;Measure, do not guess. Use the Vertical Pod Autoscaler in recommendation mode (&lt;code&gt;updateMode: &quot;Off&quot;&lt;/code&gt;) or query Prometheus history directly. The same measure-first reflex applies to &lt;a href=&quot;/glossaire/cardinalite-metriques/&quot;&gt;metric cardinality&lt;/a&gt;: observe before you tune, not the other way round.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# peak memory working set over 7 days, per pod
max_over_time(container_memory_working_set_bytes{container=&quot;app&quot;}[7d])

# 95th percentile of CPU cores used over 7 days
quantile_over_time(0.95, rate(container_cpu_usage_seconds_total{container=&quot;app&quot;}[5m])[7d:5m])&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then set values from those numbers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resources:
  requests:
    cpu: 250m           # typical usage, drives scheduling
    memory: 512Mi
  limits:
    memory: 512Mi       # equal to request: hard, predictable cap
    # no cpu limit: bursts into idle capacity, still guaranteed 250m under contention&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Memory &lt;code&gt;request == limit&lt;/code&gt; set to the observed peak working set plus roughly 20 to 30% headroom. CPU request at typical usage (P90-ish), CPU limit left off unless a policy needs one. Re-measure once real traffic lands; your first numbers are a hypothesis, not a verdict.&lt;/p&gt;
&lt;h2&gt;Common pitfalls&lt;/h2&gt;
&lt;p&gt;Setting only a limit makes Kubernetes copy it into the request, which can silently over-reserve the node. Copying another team&apos;s numbers ignores that requests are workload-specific: too high wastes nodes, too low overcommits and invites eviction. Sizing memory off RSS instead of working set misses page cache the kernel counts against your limit. And blaming the application for latency that is actually CFS throttling wastes days; check the throttling ratio before you profile code.&lt;/p&gt;
&lt;h2&gt;What to remember&lt;/h2&gt;
&lt;p&gt;Requests are for the scheduler, limits are for the kernel. Over the CPU limit you get throttled, over the memory limit you get killed. Set memory &lt;code&gt;request == limit&lt;/code&gt;, set a CPU request, be deliberate about CPU limits, and derive every number from measured usage rather than a template. Restarts from an OOMKill and restarts from a failing liveness probe look identical in &lt;code&gt;kubectl describe&lt;/code&gt;, so it is worth knowing &lt;a href=&quot;/blog/kubernetes-readiness-liveness-startup-probes/&quot;&gt;how probes actually behave&lt;/a&gt; before you debug the wrong one. Getting resource sizing right across a whole fleet is a good chunk of what makes a move to Kubernetes hold up in production instead of paging you later, and it is the heart of my &lt;a href=&quot;/expertise/migration-kubernetes/&quot;&gt;legacy-to-Kubernetes migration work&lt;/a&gt; (in French). More Kubernetes material in &lt;a href=&quot;/blog/categorie/kubernetes/&quot;&gt;the kubernetes category&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Kubernetes</category><category>resources</category><category>requests-limits</category><category>qos</category><category>oomkilled</category><category>cpu-throttling</category></item><item><title>kubectl rollout undo : comment fonctionnent les rollbacks</title><link>https://jm-dev.it/blog/kubectl-rollout-undo-rollbacks-fr/</link><guid isPermaLink="true">https://jm-dev.it/blog/kubectl-rollout-undo-rollbacks-fr/</guid><description>Comment les révisions d&apos;un Deployment correspondent aux ReplicaSets, ce que kubectl rollout undo restaure vraiment, et pourquoi GitOps préfère un revert Git.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=fr justification : version française d&apos;un tutoriel de référence générique, alignée sur l&apos;amendement §2.4 (i18n systématique pour le contenu factuel générique). L&apos;original EN vise la longue traîne technique mondiale ; cette version relaie sur LinkedIn FR et couvre les requêtes francophones (&quot;rollback kubernetes&quot;, &quot;revenir en arrière deployment&quot;).&lt;/p&gt;
&lt;p&gt;Note de contenu (directive JM 2026-07-24) : tutoriel de référence, générique et vérifiable de bout en bout face à la documentation Kubernetes et au comportement des contrôleurs. Aucune anecdote de production par construction, ce n&apos;est pas un REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kubectl rollout undo&lt;/code&gt; a tout du bouton « annuler » : mauvais déploiement, une commande, retour au calme. C&apos;est à peu près le cas. Mais ce qu&apos;il restaure, jusqu&apos;où il peut remonter et ce qui se passe quand un contrôleur GitOps surveille le même Deployment obéissent à des mécaniques qu&apos;il vaut mieux comprendre &lt;em&gt;avant&lt;/em&gt; le mauvais déploiement. Voici comment les rollbacks de Deployment Kubernetes fonctionnent réellement : où vivent les révisions, ce que &lt;code&gt;rollout undo&lt;/code&gt; restaure ou non, et pourquoi dans un pipeline GitOps le vrai rollback est un revert Git.&lt;/p&gt;
&lt;h2&gt;Les révisions sont des ReplicaSets, pas des snapshots&lt;/h2&gt;
&lt;p&gt;Un Deployment ne conserve pas l&apos;historique de ses specs passées. À chaque modification du template de pod (&lt;code&gt;spec.template&lt;/code&gt; : image, variables d&apos;environnement, sondes, labels des pods), le contrôleur de Deployment calcule un hash du nouveau template et crée un &lt;strong&gt;ReplicaSet&lt;/strong&gt; nommé d&apos;après ce hash, puis scale le nouveau ReplicaSet vers le haut et l&apos;ancien vers le bas selon la stratégie de rollout. L&apos;ancien ReplicaSet n&apos;est pas supprimé : il reste à 0 réplica, tout en conservant le template de pod exact avec lequel il a été créé.&lt;/p&gt;
&lt;p&gt;Ce ReplicaSet mis à la retraite &lt;em&gt;est&lt;/em&gt; la révision. La comptabilité tient dans une seule annotation :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get rs -l app=my-app
NAME                  DESIRED   CURRENT   READY   AGE
my-app-5d8c7b9f6d     0         0         0       2d    # revision 1
my-app-7f9b4c8d5e     0         0         0       1d    # revision 2
my-app-6c5d9e8f7a     3         3         3       2h    # revision 3

$ kubectl get rs my-app-7f9b4c8d5e \
    -o jsonpath=&apos;{.metadata.annotations.deployment\.kubernetes\.io/revision}&apos;
2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kubectl rollout history deployment/my-app&lt;/code&gt; n&apos;est en substance qu&apos;un formateur d&apos;affichage au-dessus de ces annotations. Deux conséquences en découlent directement :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Une révision n&apos;enregistre que le template de pod.&lt;/strong&gt; Le nombre de réplicas, les réglages du HPA, les labels et annotations propres au Deployment, et tout ce qui vit en dehors de &lt;code&gt;spec.template&lt;/code&gt; n&apos;en font pas partie.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Si le ReplicaSet a disparu, la révision aussi.&lt;/strong&gt; Il n&apos;en existe aucune autre copie.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Une subtilité : si vous déployez un template identique à celui que détient déjà un ancien ReplicaSet, le contrôleur réutilise ce ReplicaSet au lieu d&apos;en créer un doublon, et son annotation de révision est incrémentée au numéro le plus récent (les numéros précédents sont conservés dans une annotation &lt;code&gt;deployment.kubernetes.io/revision-history&lt;/code&gt;). Les révisions se dédupliquent par contenu.&lt;/p&gt;
&lt;h2&gt;Ce que rollout undo fait réellement&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# retour à la révision précédente
kubectl rollout undo deployment/my-app

# retour à une révision précise
kubectl rollout history deployment/my-app
kubectl rollout history deployment/my-app --revision=2   # inspecter avant de sauter
kubectl rollout undo deployment/my-app --to-revision=2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rollout undo&lt;/code&gt; retrouve le ReplicaSet de la révision cible, prend son template de pod et le réinjecte dans le &lt;code&gt;spec.template&lt;/code&gt; du Deployment. C&apos;est tout. Il n&apos;existe pas de marche arrière spéciale : le patch déclenche un rolling update ordinaire &lt;em&gt;vers l&apos;avant&lt;/em&gt; jusqu&apos;à l&apos;ancien template, en utilisant la &lt;code&gt;strategy&lt;/code&gt;, les sondes, le &lt;code&gt;maxSurge&lt;/code&gt; et le &lt;code&gt;maxUnavailable&lt;/code&gt; habituels du Deployment. Un rollback est exactement aussi rapide, et exactement aussi sûr, que vos rollouts habituels. Si vos déploiements perdent des requêtes en cours de route, vos rollbacks aussi, une raison de plus de bien régler votre &lt;a href=&quot;/glossaire/rolling-deployment/&quot;&gt;rolling deployment&lt;/a&gt; avant d&apos;en avoir besoin sous pression.&lt;/p&gt;
&lt;p&gt;La numérotation des révisions après un undo surprend. Revenir à la révision 2 ne « ramène pas à la révision 2 » : le template de la révision 2 devient la nouvelle révision la plus récente (disons la révision 4), et l&apos;entrée d&apos;historique du 2 disparaît, son contenu vit désormais tout en haut. Les numéros de révision ne font que croître ; &lt;code&gt;rollout history&lt;/code&gt; est un journal de templates, pas une chronologie que l&apos;on rembobine.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl rollout undo deployment/my-app --to-revision=2
deployment.apps/my-app rolled back

$ kubectl rollout history deployment/my-app
REVISION  CHANGE-CAUSE
1         &amp;lt;none&amp;gt;
3         &amp;lt;none&amp;gt;
4         &amp;lt;none&amp;gt;      # c&apos;est l&apos;ancienne révision 2, renumérotée&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Ce qu&apos;un rollback ne restaure pas&lt;/h2&gt;
&lt;p&gt;Puisqu&apos;une révision n&apos;est qu&apos;un template de pod, plusieurs choses que l&apos;on attend d&apos;un « annuler » sortent de son périmètre.&lt;/p&gt;
&lt;h3&gt;Le nombre de réplicas&lt;/h3&gt;
&lt;p&gt;Le scaling ne crée pas de révision : &lt;code&gt;replicas&lt;/code&gt; vit en dehors de &lt;code&gt;spec.template&lt;/code&gt;, donc &lt;code&gt;kubectl scale&lt;/code&gt; ne laisse aucune trace dans &lt;code&gt;rollout history&lt;/code&gt;. De façon symétrique, &lt;code&gt;rollout undo&lt;/code&gt; ne touche jamais au nombre de réplicas : si vous êtes passé de 3 à 10 après le mauvais déploiement, vous êtes toujours à 10 après le undo. C&apos;est généralement ce que vous voulez ; la surprise vient quand on s&apos;attend à ce que le undo annule aussi un scale-down accidentel. Il ne le fera pas. Le scaling et le template sont deux axes indépendants.&lt;/p&gt;
&lt;h3&gt;ConfigMaps, Secrets, et tout ce qui entoure le pod&lt;/h3&gt;
&lt;p&gt;Le template de pod référence une ConfigMap ou un Secret &lt;strong&gt;par son nom&lt;/strong&gt;. Si le mauvais déploiement a aussi modifié le contenu de la ConfigMap, revenir en arrière sur le Deployment remonte la même ConfigMap, toujours modifiée. Idem pour les migrations de base de données, les CRDs, les changements de RBAC, ou tout ce que votre release a touché en dehors de &lt;code&gt;spec.template&lt;/code&gt;. &lt;code&gt;rollout undo&lt;/code&gt; revient en arrière sur un champ d&apos;un seul objet.&lt;/p&gt;
&lt;p&gt;(La parade standard consiste à utiliser des noms immuables, hashés sur le contenu, comme &lt;code&gt;app-config-7d9f2b&lt;/code&gt;, pour qu&apos;un rollback de template ramène mécaniquement l&apos;ancienne config. Le &lt;code&gt;configMapGenerator&lt;/code&gt; de Kustomize et les annotations de checksum de Helm existent tous les deux pour cette raison.)&lt;/p&gt;
&lt;h3&gt;Tout ce qu&apos;a fait un Deployment en pause&lt;/h3&gt;
&lt;p&gt;Vous ne pouvez pas revenir en arrière sur un Deployment en pause ; &lt;code&gt;kubectl rollout undo&lt;/code&gt; refuse jusqu&apos;à ce que vous fassiez &lt;code&gt;kubectl rollout resume&lt;/code&gt;. Si vous utilisez pause/resume pour regrouper des changements, gardez en tête que la période de pause produit une seule révision, pas une par changement : la granularité du undo est le resume, pas l&apos;édition.&lt;/p&gt;
&lt;h2&gt;revisionHistoryLimit : jusqu&apos;où vous pouvez remonter&lt;/h2&gt;
&lt;p&gt;Le contrôleur de Deployment fait le ménage des anciens ReplicaSets au-delà de &lt;code&gt;spec.revisionHistoryLimit&lt;/code&gt;, qui vaut &lt;strong&gt;10&lt;/strong&gt; par défaut. Les révisions plus anciennes sont supprimées, et avec elles la possibilité d&apos;y revenir :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl rollout undo deployment/my-app --to-revision=1
error: unable to find specified revision 1 in history&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Les deux extrémités du curseur :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;revisionHistoryLimit: 0&lt;/code&gt; supprime immédiatement tout ReplicaSet inactif. Cela rend le rollback impossible, point final. Parfois proposé au nom du « nettoyage », c&apos;est en réalité la suppression de votre filet de sécurité.&lt;/li&gt;
&lt;li&gt;Une limite élevée ne coûte presque rien (les ReplicaSets retirés sont de petits objets d&apos;API sans aucun pod), mais notez que les pipelines bavards consomment l&apos;historique vite. Dix déploiements dans une journée chargée, et la révision known-good d&apos;hier peut déjà être hors de portée le temps que quelqu&apos;un remarque une régression lente.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Dix est une valeur par défaut raisonnable pour un pipeline piloté par le repo. Si votre stratégie de rollback dépend réellement de &lt;code&gt;rollout undo&lt;/code&gt; (voir plus bas pourquoi ce ne devrait peut-être pas être le cas), dimensionnez la limite en fonction de votre fréquence de déploiement, pas de la valeur par défaut.&lt;/p&gt;
&lt;h2&gt;Le change-cause manquant, et savoir vers quoi on revient&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;rollout history&lt;/code&gt; a une colonne &lt;code&gt;CHANGE-CAUSE&lt;/code&gt; qui affiche &lt;code&gt;&amp;lt;none&amp;gt;&lt;/code&gt; sur la plupart des clusters, parce qu&apos;elle est renseignée depuis l&apos;annotation &lt;code&gt;kubernetes.io/change-cause&lt;/code&gt;, que rien ne pose automatiquement. L&apos;ancien flag &lt;code&gt;kubectl --record&lt;/code&gt; qui la remplissait est déprécié depuis des années. Vous pouvez la poser à la main :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl annotate deployment/my-app \
  kubernetes.io/change-cause=&quot;release 2026-07-24, image v1.42.1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;L&apos;annotation est copiée sur le ReplicaSet au moment du rollout, donc chaque révision garde sa propre cause. En pratique, les annotations maintenues à la main pourrissent. La manière fiable d&apos;identifier une révision consiste à regarder son template réel :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl rollout history deployment/my-app --revision=3
# ...affiche le template de pod : tag d&apos;image, env, ce qui a réellement changé&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Un undo à l&apos;aveugle, c&apos;est-à-dire &lt;code&gt;rollout undo&lt;/code&gt; sans vérifier ce que contient la révision précédente, est un pari : celui que le déploiement précédent était le seul changement récent. Trente secondes d&apos;inspection avec &lt;code&gt;--revision=N&lt;/code&gt; sont une assurance bon marché ; cela vaut double quand l&apos;historique a déjà été renuméroté par un undo antérieur.&lt;/p&gt;
&lt;h2&gt;rollout undo face à un revert Git&lt;/h2&gt;
&lt;p&gt;Si vos Deployments sont appliqués par un pipeline ou un contrôleur GitOps depuis des manifests dans Git, &lt;code&gt;rollout undo&lt;/code&gt; pose un problème structurel : il modifie le cluster, pas le repo. Le Deployment dans le cluster et le manifest dans Git sont désormais en désaccord, et tout le mécanisme que vous avez construit existe précisément pour éliminer ce désaccord.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Avec un pipeline CI (&lt;code&gt;kubectl apply&lt;/code&gt; au merge) :&lt;/strong&gt; le undo tient jusqu&apos;au prochain merge, moment où le pipeline réapplique les manifests, y compris le mauvais tag d&apos;image que vous aviez fui. Le rollback se dé-fait silencieusement, peut-être des jours plus tard, déployé par un changement sans rapport.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Avec un contrôleur GitOps (Argo CD, Flux) :&lt;/strong&gt; le contrôleur détecte le drift à sa prochaine synchronisation. Avec le self-heal activé, il annule votre rollback en quelques minutes. Le contrôleur fait son travail, qui est de rendre le cluster conforme à Git. Le combattre à coups de undo répétés, c&apos;est combattre votre propre outillage ; le seul contournement est de désactiver l&apos;auto-sync, ce qui sacrifie la garantie même pour laquelle GitOps existe.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Le rollback qui colle au modèle est un &lt;strong&gt;revert Git&lt;/strong&gt; : reverter le commit qui a changé le tag d&apos;image (ou le bump de release), laisser le pipeline ordinaire l&apos;appliquer. Vous conservez la piste d&apos;audit, la revue de diff, les checks CI qui gardent vos manifests, et surtout la garantie que Git décrit toujours ce qui tourne. Le compromis est honnête : un revert passe par le pipeline et prend quelques minutes, là où &lt;code&gt;rollout undo&lt;/code&gt; agit en quelques secondes. Un motif d&apos;urgence défendable consiste à utiliser &lt;code&gt;rollout undo&lt;/code&gt; comme mesure d&apos;arrêt qui stoppe l&apos;hémorragie, puis à pousser immédiatement le revert Git pour que le repo s&apos;accorde avec ce que vous venez de faire, et à traiter tout undo non suivi d&apos;un commit comme une action de post-mortem, pas comme un correctif. Si votre pipeline est assez lent pour que cet écart paraisse dangereux, c&apos;est le pipeline qu&apos;il faut réparer ; rendre les déploiements et les rollbacks ennuyeux est exactement l&apos;objectif de &lt;a href=&quot;/expertise/industrialisation-cicd/&quot;&gt;l&apos;industrialisation de votre CI/CD&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Aide-mémoire&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;kubectl rollout history deployment/my-app                 # lister les révisions
kubectl rollout history deployment/my-app --revision=3    # en inspecter une
kubectl rollout undo deployment/my-app                    # reculer d&apos;une révision
kubectl rollout undo deployment/my-app --to-revision=3    # revenir à une révision précise
kubectl rollout status deployment/my-app                  # regarder converger&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Et les pièges en une passe : les révisions sont des ReplicaSets retirés et n&apos;enregistrent que le template de pod ; le undo est un rolling update ordinaire vers l&apos;avant et renumérote l&apos;historique ; il ne restaure ni le nombre de réplicas ni les ConfigMaps ; &lt;code&gt;revisionHistoryLimit&lt;/code&gt; (10 par défaut, jamais 0) borne la distance à laquelle vous pouvez remonter ; un Deployment en pause refuse de revenir en arrière ; et sous GitOps, le rollback durable est le revert Git, &lt;code&gt;rollout undo&lt;/code&gt; n&apos;en étant au mieux que la première moitié. D&apos;autres ressources d&apos;automatisation dans &lt;a href=&quot;/blog/categorie/automatisation/&quot;&gt;la catégorie automatisation&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Automatisation</category><category>kubectl</category><category>deployments</category><category>rollback</category><category>gitops</category></item><item><title>kubectl rollout undo: How Kubernetes Rollbacks Work</title><link>https://jm-dev.it/blog/kubectl-rollout-undo-rollbacks/</link><guid isPermaLink="true">https://jm-dev.it/blog/kubectl-rollout-undo-rollbacks/</guid><description>How Deployment revisions map to ReplicaSets, what kubectl rollout undo actually restores, why GitOps prefers a Git revert, and the gotchas around rollbacks.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=en justification: &quot;kubectl rollout undo&quot;, &quot;kubernetes rollback deployment&quot; and &quot;revisionHistoryLimit&quot; are global technical queries searched almost exclusively in English by engineers mid-incident. Consistent with the strategy of publishing pointed tutorials in English.&lt;/p&gt;
&lt;p&gt;Content note (JM directive 2026-07-24): reference tutorial, generic and verifiable end to end against Kubernetes documentation and controller behavior. No production anecdotes by design — this is not a REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kubectl rollout undo&lt;/code&gt; looks like an undo button: bad deploy, one command, back to safety. It mostly is one. But what it restores, how far back it can reach, and what happens when a GitOps controller is watching the same Deployment are all governed by mechanics worth understanding &lt;em&gt;before&lt;/em&gt; the bad deploy. This is how Kubernetes Deployment rollbacks actually work: where revisions live, what &lt;code&gt;rollout undo&lt;/code&gt; does and does not restore, and why in a GitOps pipeline the real rollback is a Git revert.&lt;/p&gt;
&lt;h2&gt;Revisions are ReplicaSets, not snapshots&lt;/h2&gt;
&lt;p&gt;A Deployment does not store a history of its past specs. Every time you change the pod template (&lt;code&gt;spec.template&lt;/code&gt;: image, env vars, probes, labels on the pods), the Deployment controller computes a hash of the new template and creates a &lt;strong&gt;ReplicaSet&lt;/strong&gt; named after it, then scales the new ReplicaSet up and the old one down according to the rollout strategy. The old ReplicaSet is not deleted: it stays at 0 replicas, still holding the exact pod template it was created with.&lt;/p&gt;
&lt;p&gt;That retired ReplicaSet &lt;em&gt;is&lt;/em&gt; the revision. The bookkeeping is a single annotation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl get rs -l app=my-app
NAME                  DESIRED   CURRENT   READY   AGE
my-app-5d8c7b9f6d     0         0         0       2d    # revision 1
my-app-7f9b4c8d5e     0         0         0       1d    # revision 2
my-app-6c5d9e8f7a     3         3         3       2h    # revision 3

$ kubectl get rs my-app-7f9b4c8d5e \
    -o jsonpath=&apos;{.metadata.annotations.deployment\.kubernetes\.io/revision}&apos;
2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kubectl rollout history deployment/my-app&lt;/code&gt; is essentially a pretty-printer over these annotations. Two consequences follow directly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A revision only records the pod template.&lt;/strong&gt; Replica count, HPA settings, the Deployment&apos;s own labels and annotations, and anything outside &lt;code&gt;spec.template&lt;/code&gt; are not part of it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If the ReplicaSet is gone, the revision is gone.&lt;/strong&gt; There is no other copy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One wrinkle: if you roll out a template identical to one an old ReplicaSet already holds, the controller reuses that ReplicaSet rather than creating a duplicate, and its revision annotation is bumped to the newest number (the previous numbers are kept in a &lt;code&gt;deployment.kubernetes.io/revision-history&lt;/code&gt; annotation). Revisions deduplicate by content.&lt;/p&gt;
&lt;h2&gt;What rollout undo actually does&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# back to the previous revision
kubectl rollout undo deployment/my-app

# back to a specific revision
kubectl rollout history deployment/my-app
kubectl rollout history deployment/my-app --revision=2   # inspect before jumping
kubectl rollout undo deployment/my-app --to-revision=2&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rollout undo&lt;/code&gt; finds the target revision&apos;s ReplicaSet, takes its pod template, and patches it back into the Deployment&apos;s &lt;code&gt;spec.template&lt;/code&gt;. That is all. There is no special reverse gear: the patch triggers an ordinary rolling update &lt;em&gt;forward&lt;/em&gt; to the old template, using the Deployment&apos;s normal &lt;code&gt;strategy&lt;/code&gt;, probes, &lt;code&gt;maxSurge&lt;/code&gt; and &lt;code&gt;maxUnavailable&lt;/code&gt;. A rollback is exactly as fast, and exactly as safe, as your regular rollouts are. If your deploys drop requests mid-rollout, your rollbacks will too, which is one more reason to get your &lt;a href=&quot;/glossaire/rolling-deployment/&quot;&gt;rolling deployment&lt;/a&gt; settings right before you need one under pressure.&lt;/p&gt;
&lt;p&gt;The revision numbering after an undo surprises people. Rolling back to revision 2 does not &quot;return to revision 2&quot;: the template of revision 2 becomes the new latest revision (say, revision 4), and the history entry for 2 disappears — its content now lives at the top. Revision numbers only ever increase; &lt;code&gt;rollout history&lt;/code&gt; is a log of templates, not a timeline you rewind.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl rollout undo deployment/my-app --to-revision=2
deployment.apps/my-app rolled back

$ kubectl rollout history deployment/my-app
REVISION  CHANGE-CAUSE
1         &amp;lt;none&amp;gt;
3         &amp;lt;none&amp;gt;
4         &amp;lt;none&amp;gt;      # this is the old revision 2, renumbered&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What a rollback does not restore&lt;/h2&gt;
&lt;p&gt;Because a revision is only a pod template, several things people expect from an &quot;undo&quot; are out of scope.&lt;/p&gt;
&lt;h3&gt;Replica count&lt;/h3&gt;
&lt;p&gt;Scaling does not create a revision: &lt;code&gt;replicas&lt;/code&gt; sits outside &lt;code&gt;spec.template&lt;/code&gt;, so &lt;code&gt;kubectl scale&lt;/code&gt; leaves no trace in &lt;code&gt;rollout history&lt;/code&gt;. Symmetrically, &lt;code&gt;rollout undo&lt;/code&gt; never touches the replica count: if you scaled from 3 to 10 after the bad deploy, you are still at 10 after the undo. Usually that is what you want; the surprise is when someone expects undo to also revert an accidental scale-down. It will not. Scale and template are independent axes.&lt;/p&gt;
&lt;h3&gt;ConfigMaps, Secrets, and everything around the pod&lt;/h3&gt;
&lt;p&gt;The pod template references a ConfigMap or Secret &lt;strong&gt;by name&lt;/strong&gt;. If the bad deploy also changed the ConfigMap&apos;s contents, rolling back the Deployment re-mounts the same, still-changed ConfigMap. The same applies to database migrations, CRDs, RBAC changes, or anything else your release touched outside &lt;code&gt;spec.template&lt;/code&gt;. &lt;code&gt;rollout undo&lt;/code&gt; rolls back one field of one object.&lt;/p&gt;
&lt;p&gt;(The standard mitigation is immutable, content-hashed names like &lt;code&gt;app-config-7d9f2b&lt;/code&gt;, so a template rollback mechanically brings back the old config. Kustomize&apos;s &lt;code&gt;configMapGenerator&lt;/code&gt; and Helm&apos;s checksum annotations both exist for this reason.)&lt;/p&gt;
&lt;h3&gt;Anything a paused Deployment did&lt;/h3&gt;
&lt;p&gt;You cannot roll back a paused Deployment; &lt;code&gt;kubectl rollout undo&lt;/code&gt; refuses until you &lt;code&gt;kubectl rollout resume&lt;/code&gt; it. If you use pause/resume to batch changes, remember that the paused period produces a single revision, not one per change: the undo granularity is the resume, not the edit.&lt;/p&gt;
&lt;h2&gt;revisionHistoryLimit: how far back you can reach&lt;/h2&gt;
&lt;p&gt;The Deployment controller garbage-collects old ReplicaSets beyond &lt;code&gt;spec.revisionHistoryLimit&lt;/code&gt;, which defaults to &lt;strong&gt;10&lt;/strong&gt;. Older revisions are deleted, and with them the ability to roll back to them:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ kubectl rollout undo deployment/my-app --to-revision=1
error: unable to find specified revision 1 in history&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two edges of the dial:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;revisionHistoryLimit: 0&lt;/code&gt; deletes every inactive ReplicaSet immediately. It makes rollback impossible, full stop. Occasionally proposed as &quot;cleanup&quot;; it is the removal of your safety net.&lt;/li&gt;
&lt;li&gt;A high limit costs almost nothing (retired ReplicaSets are small API objects with zero pods), but note that noisy pipelines burn through history fast. Ten deploys on a busy day means yesterday&apos;s known-good revision may already be beyond reach by the time someone notices a slow regression.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ten is a reasonable default for a repo-driven pipeline. If your rollback strategy genuinely depends on &lt;code&gt;rollout undo&lt;/code&gt; (see below for why it maybe should not), size the limit against your deploy frequency, not the default.&lt;/p&gt;
&lt;h2&gt;The missing change-cause, and knowing what you are rolling back to&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;rollout history&lt;/code&gt; has a &lt;code&gt;CHANGE-CAUSE&lt;/code&gt; column that is &lt;code&gt;&amp;lt;none&amp;gt;&lt;/code&gt; on most clusters, because it is populated from the &lt;code&gt;kubernetes.io/change-cause&lt;/code&gt; annotation, which nothing sets automatically. The old &lt;code&gt;kubectl --record&lt;/code&gt; flag that filled it has been deprecated for years. You can set it by hand:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl annotate deployment/my-app \
  kubernetes.io/change-cause=&quot;release 2026-07-24, image v1.42.1&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The annotation is copied to the ReplicaSet at rollout time, so each revision keeps its own cause. In practice, hand-maintained annotations rot. The reliable way to identify a revision is to look at its actual template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl rollout history deployment/my-app --revision=3
# ...prints the pod template: image tag, env, the thing that actually changed&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An undo done blind, meaning &lt;code&gt;rollout undo&lt;/code&gt; without checking what the previous revision contains, is a gamble that the previous deploy was the only recent change. Thirty seconds of &lt;code&gt;--revision=N&lt;/code&gt; inspection is cheap insurance; this matters double when history has been renumbered by an earlier undo.&lt;/p&gt;
&lt;h2&gt;rollout undo vs a Git revert&lt;/h2&gt;
&lt;p&gt;If your Deployments are applied by a pipeline or a GitOps controller from manifests in Git, &lt;code&gt;rollout undo&lt;/code&gt; has a structural problem: it changes the cluster, not the repo. The Deployment in the cluster and the manifest in Git now disagree, and every mechanism you have built exists to eliminate exactly that disagreement.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;With a CI pipeline (&lt;code&gt;kubectl apply&lt;/code&gt; on merge):&lt;/strong&gt; the undo holds until the next merge, at which point the pipeline re-applies the manifests, including the bad image tag you rolled away from. The rollback silently un-rolls itself, possibly days later, deployed by an unrelated change.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;With a GitOps controller (Argo CD, Flux):&lt;/strong&gt; the controller detects the drift on its next sync. With self-heal enabled, it reverts your rollback within minutes. The controller is doing its job, which is making the cluster match Git. Fighting it with repeated undos is fighting your own tooling; the only workaround is disabling auto-sync, which trades away the guarantee GitOps exists to provide.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rollback that fits the model is a &lt;strong&gt;Git revert&lt;/strong&gt;: revert the commit that changed the image tag (or the release bump), let the ordinary pipeline apply it. You keep the audit trail, the diff review, whatever CI checks guard your manifests, and, critically, the guarantee that Git still describes what runs. The trade-off is honest: a revert goes through the pipeline and takes minutes, while &lt;code&gt;rollout undo&lt;/code&gt; acts in seconds. A defensible emergency pattern is to use &lt;code&gt;rollout undo&lt;/code&gt; as the stopgap that stops the bleeding, then immediately push the Git revert so the repo agrees with what you just did, and treat any undo that is not followed by a commit as an incident action item, not a fix. If your pipeline is slow enough that this gap feels dangerous, the pipeline is the thing to fix; making deploys and rollbacks boring is precisely the point of &lt;a href=&quot;/expertise/industrialisation-cicd/&quot;&gt;industrializing your CI/CD&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Quick reference&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;kubectl rollout history deployment/my-app                 # list revisions
kubectl rollout history deployment/my-app --revision=3    # inspect one
kubectl rollout undo deployment/my-app                    # back one revision
kubectl rollout undo deployment/my-app --to-revision=3    # back to a specific one
kubectl rollout status deployment/my-app                  # watch it converge&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the gotchas in one pass: revisions are retired ReplicaSets and record only the pod template; undo is a normal rolling update forward and renumbers history; it restores neither replica count nor ConfigMaps; &lt;code&gt;revisionHistoryLimit&lt;/code&gt; (default 10, never 0) bounds how far back you can go; a paused Deployment refuses to roll back; and under GitOps, the durable rollback is the Git revert — &lt;code&gt;rollout undo&lt;/code&gt; is at best the first half of one. More automation material in &lt;a href=&quot;/blog/categorie/automatisation/&quot;&gt;the automatisation category&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Automatisation</category><category>kubectl</category><category>deployments</category><category>rollback</category><category>gitops</category></item><item><title>Symptom-based Prometheus alerts that don&apos;t page at 3am</title><link>https://jm-dev.it/blog/prometheus-alerting-rules-good-vs-bad/</link><guid isPermaLink="true">https://jm-dev.it/blog/prometheus-alerting-rules-good-vs-bad/</guid><description>Why symptom-based Prometheus alerts beat cause-based ones, how the for clause and severity labels cut noise, plus a bad rule rewritten into a good one.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=en justification: &quot;prometheus alerting best practices&quot;, &quot;symptom based alerting&quot;, &quot;prometheus for clause&quot; are global technical queries searched almost exclusively in English. Reference tutorial for the international SRE/platform audience, consistent with the site strategy of publishing pointed technical material in English.&lt;/p&gt;
&lt;p&gt;Content note (JM directive 2026-07-24): reference tutorial, generic and verifiable end to end against Prometheus and Alertmanager documented behavior. No production anecdotes by design; this is not a REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;It is 3am and the pager fires: &lt;code&gt;HighCPUUsage&lt;/code&gt;, node at 92%. You open a laptop, look around, and everything works. Users are asleep, the batch job that spiked CPU finished on its own, and you go back to bed annoyed. Two weeks of that and people start muting the channel. The alert was technically correct and completely useless, because it told you about a &lt;em&gt;cause&lt;/em&gt; that may or may not have a &lt;em&gt;symptom&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The single most useful idea in alerting is this: page a human on symptoms, and let causes stay on dashboards. High CPU is not a problem. Slow responses are. If the CPU is at 92% and the service still meets its latency target, there is nothing to do. Alert on the thing your users actually feel, and you get a pager that means &quot;something is broken right now&quot; instead of &quot;a number crossed a line&quot;.&lt;/p&gt;
&lt;h2&gt;Symptom-based versus cause-based&lt;/h2&gt;
&lt;p&gt;A cause-based rule watches an internal resource: CPU, memory, queue depth, disk usage. A symptom-based rule watches the contract with the user: error rate, latency, availability. The problem with cause-based alerting is that the mapping from cause to symptom is not one-to-one. High CPU sometimes hurts and usually does not. A full disk is a genuine emergency on a database node and a shrug on a stateless replica. Every cause needs a human to decide whether it matters, at 3am, which is exactly the decision the alert was supposed to make for you.&lt;/p&gt;
&lt;p&gt;This does not mean deleting your resource metrics. Keep them; they are how you &lt;em&gt;diagnose&lt;/em&gt; an incident once a symptom alert has woken you. The rule of thumb: symptoms page, causes inform. Disk-filling and certificate-expiry are the honest exceptions, because they are causes with a guaranteed, predictable symptom in the near future, so alerting early is the whole point.&lt;/p&gt;
&lt;h2&gt;How &lt;code&gt;for&lt;/code&gt; and severity actually work&lt;/h2&gt;
&lt;p&gt;Prometheus evaluates alerting rules every &lt;code&gt;evaluation_interval&lt;/code&gt; (1 minute by default). When a rule&apos;s expression returns samples, the matching alerts enter the &lt;code&gt;pending&lt;/code&gt; state. They only become &lt;code&gt;firing&lt;/code&gt;, and get sent to Alertmanager, once the expression has stayed true continuously for the duration in the &lt;code&gt;for&lt;/code&gt; field. A single evaluation where the expression returns nothing resets the timer.&lt;/p&gt;
&lt;p&gt;That &lt;code&gt;for&lt;/code&gt; clause is your first and cheapest noise filter. An expression with no &lt;code&gt;for&lt;/code&gt; fires on a single scrape, so one slow scrape or one transient spike pages you. &lt;code&gt;for: 10m&lt;/code&gt; says &quot;I only care if this is still true ten minutes from now&quot;, which filters out exactly the self-healing blips that generate 3am false alarms. The trade-off is detection latency, so tune it per symptom: a hard-down service might use &lt;code&gt;for: 2m&lt;/code&gt;, a slow latency creep &lt;code&gt;for: 15m&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Severity is a plain label, not a built-in feature. The convention is &lt;code&gt;severity: critical&lt;/code&gt; for &quot;wake someone up&quot; and &lt;code&gt;severity: warning&lt;/code&gt; for &quot;look at it during business hours&quot;. The label carries no meaning to Prometheus itself; it becomes useful in Alertmanager routing, where you send &lt;code&gt;critical&lt;/code&gt; to the pager and &lt;code&gt;warning&lt;/code&gt; to a chat channel. Getting the split right is most of what separates a calm on-call rotation from a burned-out one. If everything is critical, nothing is.&lt;/p&gt;
&lt;h2&gt;A bad rule rewritten into a good one&lt;/h2&gt;
&lt;p&gt;Here is a rule that looks reasonable and pages constantly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;groups:
  - name: bad.rules
    rules:
      - alert: HighCPUUsage
        expr: rate(node_cpu_seconds_total{mode!=&quot;idle&quot;}[5m]) &amp;gt; 0.8
        labels:
          severity: critical
        annotations:
          summary: &quot;CPU is high&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything about it invites fatigue. It fires per core rather than per node, has no &lt;code&gt;for&lt;/code&gt; so a single spike pages, calls a resource-usage number &lt;code&gt;critical&lt;/code&gt;, and the annotation tells the responder nothing they can act on. Rewritten around a symptom:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;groups:
  - name: good.rules
    rules:
      - alert: HighRequestErrorRate
        expr: |
          sum(rate(http_requests_total{status=~&quot;5..&quot;}[5m])) by (service)
            /
          sum(rate(http_requests_total[5m])) by (service)
            &amp;gt; 0.05
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: &quot;{{ $labels.service }} is serving &amp;gt;5% errors&quot;
          description: &quot;5xx error ratio is {{ $value | humanizePercentage }} over 5m. Check recent deploys and downstream dependencies.&quot;
          runbook_url: &quot;https://runbooks.internal/high-error-rate&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The good rule alerts on what a user experiences (a failed request), is aggregated to a meaningful unit (per service), survives a transient blip (&lt;code&gt;for: 10m&lt;/code&gt;), and hands the responder a starting point through its annotations and a runbook link. Deciding &lt;em&gt;where&lt;/em&gt; to set that 5% threshold is a job for an &lt;a href=&quot;/glossaire/error-budget/&quot;&gt;error budget&lt;/a&gt; and a defined &lt;a href=&quot;/glossaire/slo-service-level-objective/&quot;&gt;service level objective&lt;/a&gt; rather than a number you invent at the keyboard. And this rule assumes the right metrics already exist: the error rate and latency queried here come from an application instrumented to expose them, a prerequisite covered in &lt;a href=&quot;/blog/prometheus-instrumentation-golden-signals/&quot;&gt;instrumenting an app for Prometheus&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Pitfalls that survive good intentions&lt;/h2&gt;
&lt;p&gt;Three traps recur even in symptom-based setups. First, the divide-by-zero: a ratio like the one above produces no result when a service gets no traffic, so an outage that stops all requests can silence the very alert meant to catch it. Pair error-rate alerts with a low-traffic or absence check. Second, over-eager thresholds without burn-rate awareness: a flat &quot;5% for 10m&quot; both misses a slow burn that quietly eats the month&apos;s budget and screams during a brief spike, which is why multi-window, multi-burn-rate alerting exists. Third, &lt;code&gt;critical&lt;/code&gt; inflation: every alert added as critical because &quot;it felt important&quot; trains the on-call to ignore the pager, and a muted pager is worse than no pager.&lt;/p&gt;
&lt;h2&gt;What to keep&lt;/h2&gt;
&lt;p&gt;Alert on symptoms, keep causes for diagnosis. Use &lt;code&gt;for&lt;/code&gt; to filter self-healing noise, and reserve &lt;code&gt;critical&lt;/code&gt; for things that genuinely need a human out of bed. Write annotations for the tired person reading them at 3am, not for yourself writing them at 3pm. An alerting stack built this way is a core deliverable of a &lt;a href=&quot;/expertise/fiabilite-observabilite/&quot;&gt;reliability and observability&lt;/a&gt; engagement. More in the &lt;a href=&quot;/blog/categorie/observabilite/&quot;&gt;observability category&lt;/a&gt;, and the same discipline applied to probes lives in &lt;a href=&quot;/blog/kubernetes-readiness-liveness-startup-probes/&quot;&gt;Kubernetes readiness and liveness probes&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Observabilité</category><category>prometheus</category><category>alerting</category><category>alertmanager</category><category>sre</category><category>monitoring</category></item><item><title>Probes Kubernetes : readiness, liveness et startup</title><link>https://jm-dev.it/blog/readiness-liveness-startup-probes-kubernetes/</link><guid isPermaLink="true">https://jm-dev.it/blog/readiness-liveness-startup-probes-kubernetes/</guid><description>Ce que chaque type de probe Kubernetes contrôle réellement, les modes de défaillance des configs copiées-collées, et une table de décision par paramètre.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- justification lang=fr : version française de l&apos;article EN &quot;kubernetes-readiness-liveness-startup-probes&quot;. Le contenu est un tutoriel de référence générique ; la traduction FR sert le public francophone (relais LinkedIn FR) et le maillage interne vers le glossaire français. Paire liée via translationKey.&lt;/p&gt;
&lt;p&gt;Note de contenu (directive JM 2026-07-24) : tutoriel de référence, générique et vérifiable de bout en bout contre la documentation Kubernetes et le comportement des sources. Aucune anecdote de production par construction, ce n&apos;est pas un REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;Kubernetes dispose de trois types de probes (readiness, liveness et startup), et chacune répond à une question différente. La plupart des incidents liés aux probes viennent du fait qu&apos;on les traite comme un seul mécanisme : le même endpoint &lt;code&gt;/healthz&lt;/code&gt; collé dans &lt;code&gt;readinessProbe&lt;/code&gt; et dans &lt;code&gt;livenessProbe&lt;/code&gt;, aucun &lt;code&gt;startupProbe&lt;/code&gt; sur une application qui met une minute à démarrer, et des timeouts par défaut que personne n&apos;a lus. Voici une référence de ce que chaque probe contrôle vraiment, des modes de défaillance qui découlent de leur confusion, et de la manière d&apos;en choisir les paramètres.&lt;/p&gt;
&lt;h2&gt;Trois probes, trois questions différentes&lt;/h2&gt;
&lt;p&gt;Les trois probes partagent la même structure YAML et les mêmes handlers. Ce qui change, c&apos;est ce que le kubelet fait du résultat.&lt;/p&gt;
&lt;h3&gt;readinessProbe : ce pod peut-il servir du trafic maintenant ?&lt;/h3&gt;
&lt;p&gt;La readiness contrôle le &lt;strong&gt;load balancing&lt;/strong&gt;. Tant que la probe passe, l&apos;IP du pod figure dans les EndpointSlices de chaque Service qui le sélectionne. Quand elle échoue (&lt;code&gt;failureThreshold&lt;/code&gt; fois de suite), le pod est marqué not-ready et retiré du load balancing. Le conteneur n&apos;est pas touché : il continue de tourner, et dès que la probe repasse au vert le pod revient dans la rotation.&lt;/p&gt;
&lt;p&gt;La readiness est la seule probe conçue pour échouer en fonctionnement normal. Un pod momentanément surchargé, en attente d&apos;un préchauffage de cache, ou en train de se vider avant un arrêt correspond exactement à ce que « not ready » veut dire. Elle tourne pendant toute la vie du conteneur, pas seulement au démarrage.&lt;/p&gt;
&lt;p&gt;Les échecs de readiness conditionnent aussi les rollouts : une mise à jour progressive ne dépasse un nouveau pod que lorsque celui-ci se déclare Ready. Un contrôle de readiness mal fichu a donc un second rayon d&apos;action : il peut laisser un rolling update remplacer des pods sains par des pods cassés qui prétendent aller bien.&lt;/p&gt;
&lt;h3&gt;livenessProbe : ce processus est-il bloqué au point d&apos;être irrécupérable ?&lt;/h3&gt;
&lt;p&gt;La liveness contrôle les &lt;strong&gt;redémarrages de conteneur&lt;/strong&gt;. Quand elle échoue &lt;code&gt;failureThreshold&lt;/code&gt; fois de suite, le kubelet tue le conteneur et le redémarre selon le &lt;code&gt;restartPolicy&lt;/code&gt; du pod, avec un backoff exponentiel (&lt;code&gt;CrashLoopBackOff&lt;/code&gt; quand ça se répète).&lt;/p&gt;
&lt;p&gt;C&apos;est un remède brutal, et c&apos;est pourquoi la question à laquelle répond la liveness doit rester étroite : le processus est-il en deadlock, bloqué, ou incapable de progresser d&apos;une façon que seul un redémarrage corrige ? Tout ce qui est moins grave relève de la readiness. Une probe de liveness ne devrait presque jamais échouer sur une flotte saine. Si la vôtre se déclenche régulièrement, soit l&apos;application a un vrai défaut, soit la probe vérifie la mauvaise chose.&lt;/p&gt;
&lt;h3&gt;startupProbe : ce conteneur a-t-il fini de démarrer ?&lt;/h3&gt;
&lt;p&gt;La startup contrôle &lt;strong&gt;les deux autres probes&lt;/strong&gt;. Tant qu&apos;un &lt;code&gt;startupProbe&lt;/code&gt; est défini et n&apos;a pas encore réussi, le kubelet n&apos;exécute ni la liveness ni la readiness. Dès qu&apos;elle réussit une seule fois, elle ne tourne plus jamais et les autres probes prennent le relais.&lt;/p&gt;
&lt;p&gt;Son but concerne les applications lentes à démarrer. Le temps de boot maximal accordé vaut &lt;code&gt;failureThreshold × periodSeconds&lt;/code&gt; (plus &lt;code&gt;initialDelaySeconds&lt;/code&gt; s&apos;il est défini) ; si la probe n&apos;a toujours pas réussi à ce moment-là, le conteneur est tué et redémarré. Ce budget vit à un seul endroit, sur une seule probe, au lieu d&apos;être étalé sur l&apos;&lt;code&gt;initialDelaySeconds&lt;/code&gt; des deux autres.&lt;/p&gt;
&lt;h2&gt;Mécanique et paramètres des probes&lt;/h2&gt;
&lt;p&gt;Chaque probe utilise l&apos;un des quatre handlers :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;httpGet&lt;/code&gt; : le succès est un statut HTTP ≥ 200 et &amp;lt; 400. Le choix le plus courant pour les workloads web.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tcpSocket&lt;/code&gt; : le succès est l&apos;établissement d&apos;une connexion TCP. Prouve qu&apos;un port est ouvert, rien sur l&apos;application derrière.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exec&lt;/code&gt; : lance une commande dans le conteneur ; le code de sortie 0 est un succès. Souple, et le plus coûteux, puisque chaque exécution effectue un fork dans le conteneur.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grpc&lt;/code&gt; : utilise le gRPC Health Checking Protocol standard (GA depuis Kubernetes 1.27). La bonne réponse pour les serveurs gRPC, en remplacement de l&apos;ancien contournement par exec &lt;code&gt;grpc_health_probe&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Les paramètres de timing, avec leurs valeurs par défaut :&lt;/p&gt;
&lt;p&gt;| Paramètre | Défaut | Signification | |---|---|---| | &lt;code&gt;initialDelaySeconds&lt;/code&gt; | 0 | Attente avant le premier contrôle | | &lt;code&gt;periodSeconds&lt;/code&gt; | 10 | Intervalle entre les contrôles | | &lt;code&gt;timeoutSeconds&lt;/code&gt; | 1 | Temps alloué par contrôle | | &lt;code&gt;failureThreshold&lt;/code&gt; | 3 | Échecs consécutifs avant d&apos;agir | | &lt;code&gt;successThreshold&lt;/code&gt; | 1 | Succès consécutifs pour récupérer (doit valoir 1 pour liveness et startup) |&lt;/p&gt;
&lt;p&gt;Deux valeurs par défaut méritent l&apos;attention. &lt;code&gt;timeoutSeconds: 1&lt;/code&gt; est agressif : une probe exec qui fork un shell, ou un handler HTTP qui touche quoi que ce soit de plus lent que la mémoire, peut faire sauter un budget d&apos;une seconde sous charge, et un timeout de probe compte comme un échec. &lt;code&gt;periodSeconds: 10&lt;/code&gt; avec &lt;code&gt;failureThreshold: 3&lt;/code&gt; signifie que la détection prend jusqu&apos;à 30 secondes, souvent plus lent que ce que les gens imaginent de la réactivité de leurs « health checks ».&lt;/p&gt;
&lt;h2&gt;Les modes de défaillance des probes copiées-collées&lt;/h2&gt;
&lt;p&gt;Voici les schémas qui transforment les probes d&apos;un mécanisme de sécurité en amplificateur d&apos;incident. Tous viennent de la copie d&apos;un bloc qui a l&apos;air de marcher sans se demander à quelle question il répond.&lt;/p&gt;
&lt;h3&gt;Liveness et readiness pointant sur le même endpoint&lt;/h3&gt;
&lt;p&gt;Le plus fréquent. Si &lt;code&gt;/healthz&lt;/code&gt; échoue parce qu&apos;une dépendance en aval est lente, la readiness sort correctement le pod de la rotation. La liveness, qui surveille le même endpoint, redémarre alors un processus qui n&apos;a jamais été cassé. Multipliez par chaque réplica qui surveille la même dépendance lente, et vous obtenez une tempête de redémarrages coordonnés : la dépendance récupère, mais votre couche applicative est désormais en &lt;code&gt;CrashLoopBackOff&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Les deux probes ont besoin d&apos;endpoints différents parce qu&apos;elles répondent à des questions différentes. &lt;code&gt;/ready&lt;/code&gt; peut consulter tout ce dont l&apos;application a besoin pour servir une requête ; &lt;code&gt;/live&lt;/code&gt; ne devrait vérifier que le fait que le processus lui-même répond.&lt;/p&gt;
&lt;h3&gt;Contrôles de dépendances dans la probe de liveness&lt;/h3&gt;
&lt;p&gt;La version généralisée de l&apos;erreur précédente. Un handler de liveness qui ping la base de données, interroge un broker de messages ou appelle un autre service convertit chaque hoquet de dépendance en un redémarrage massif des dépendants. Redémarrer vos pods d&apos;API ne répare pas votre base de données ; cela ajoute du churn de connexions et des caches froids par-dessus le problème initial, et le fait sur chaque réplica à peu près au même moment.&lt;/p&gt;
&lt;p&gt;La conscience des dépendances relève de la readiness, et même là avec précaution : si la readiness de chaque réplica échoue simultanément parce qu&apos;une dépendance partagée est tombée, le Service se retrouve avec zéro endpoint et les clients reçoivent un connection refused au lieu d&apos;une réponse dégradée. Pour une dépendance sans laquelle l&apos;application pourrait fonctionner partiellement, renvoyer un 200 avec un comportement dégradé vaut souvent mieux que faire échouer la probe.&lt;/p&gt;
&lt;h3&gt;Aucun startupProbe sur une application lente à démarrer&lt;/h3&gt;
&lt;p&gt;Sans probe de startup, la liveness commence à vérifier après &lt;code&gt;initialDelaySeconds&lt;/code&gt;. Pour une application au temps de boot variable (JVM qui chauffent, migrations qui tournent, gros caches à charger), cela impose un mauvais choix : soit régler &lt;code&gt;initialDelaySeconds&lt;/code&gt; assez haut pour le pire cas, et encaisser ce délai à chaque redémarrage, même les rapides, soit le régler avec optimisme et regarder le kubelet tuer des conteneurs qui étaient à 5 secondes de la fin de leur boot. Le kill provoque un redémarrage, qui reboote lentement, qui se fait tuer à nouveau : une boucle de crash au démarrage entièrement causée par la configuration des probes.&lt;/p&gt;
&lt;p&gt;Un &lt;code&gt;startupProbe&lt;/code&gt; avec un &lt;code&gt;failureThreshold&lt;/code&gt; généreux résout cela proprement :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;startupProbe:
  httpGet:
    path: /live
    port: 8080
  periodSeconds: 5
  failureThreshold: 24      # up to 120 s to boot&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;L&apos;application dispose de jusqu&apos;à deux minutes pour se lever, contrôlée toutes les cinq secondes, si bien qu&apos;un boot rapide est détecté rapidement. Une fois levée, la liveness tourne avec des réglages serrés de régime établi, plutôt que des réglages gonflés pour couvrir le démarrage.&lt;/p&gt;
&lt;h3&gt;Aucun readinessProbe du tout&lt;/h3&gt;
&lt;p&gt;Sans probe de readiness, un pod compte comme Ready dès que ses conteneurs tournent. Le trafic arrive avant que l&apos;application n&apos;écoute, et les rolling updates progressent sur la base de ce mensonge : les anciens pods sont tués parce que les nouveaux se déclarent « Ready ». Résultat, une salve d&apos;erreurs à chaque déploiement qu&apos;aucun réglage de liveness ne peut corriger.&lt;/p&gt;
&lt;h3&gt;tcpSocket là où httpGet était nécessaire&lt;/h3&gt;
&lt;p&gt;Une probe de readiness &lt;code&gt;tcpSocket&lt;/code&gt; sur une application web prouve que le socket est ouvert. Une application peut accepter des connexions TCP bien avant que ses routes ne soient câblées, et bien après que sa boucle d&apos;événements s&apos;est bloquée. Si l&apos;application parle HTTP, sondez-la en HTTP.&lt;/p&gt;
&lt;h2&gt;Un exemple complet&lt;/h2&gt;
&lt;p&gt;Les trois probes réunies, sur une application hypothétique au démarrage parfois lent :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;containers:
  - name: app
    ports:
      - containerPort: 8080
    startupProbe:
      httpGet:
        path: /live
        port: 8080
      periodSeconds: 5
      failureThreshold: 24        # boot budget: 120 s
    readinessProbe:
      httpGet:
        path: /ready              # may check what serving requires
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 2         # out of rotation within ~10 s
    livenessProbe:
      httpGet:
        path: /live               # process responsive, nothing else
        port: 8080
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3         # restart only after ~30 s of silence&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notez l&apos;asymétrie : la readiness réagit plus vite que la liveness (&lt;code&gt;failureThreshold: 2&lt;/code&gt; contre &lt;code&gt;3&lt;/code&gt;, période plus courte). Être retiré du load balancing est peu coûteux et réversible ; un redémarrage n&apos;est ni l&apos;un ni l&apos;autre. Vous voulez que le remède le moins cher se déclenche en premier.&lt;/p&gt;
&lt;h2&gt;Table de décision&lt;/h2&gt;
&lt;p&gt;| Question | Probe | En cas d&apos;échec | L&apos;endpoint devrait vérifier | |---|---|---|---| | Ce pod peut-il prendre du trafic maintenant ? | &lt;code&gt;readinessProbe&lt;/code&gt; | Retiré des endpoints du Service | Application pleinement initialisée ; dépendances critiques, avec précaution | | Le processus est-il bloqué au point d&apos;être irrécupérable ? | &lt;code&gt;livenessProbe&lt;/code&gt; | Conteneur tué et redémarré | Réactivité du processus / de la boucle d&apos;événements uniquement, jamais les dépendances | | Le conteneur a-t-il fini de démarrer ? | &lt;code&gt;startupProbe&lt;/code&gt; | Conteneur tué et redémarré | Même handler que la liveness, &lt;code&gt;failureThreshold&lt;/code&gt; généreux |&lt;/p&gt;
&lt;p&gt;Et pour les paramètres :&lt;/p&gt;
&lt;p&gt;| Situation | Réglage | |---|---| | Temps de boot variable ou &amp;gt; ~10 s | Ajouter un &lt;code&gt;startupProbe&lt;/code&gt; ; garder &lt;code&gt;initialDelaySeconds&lt;/code&gt; à 0 ailleurs | | Le handler fait un vrai travail (exec, disque, appel en aval) | Monter &lt;code&gt;timeoutSeconds&lt;/code&gt; au-dessus du défaut d&apos;1 s | | Le trafic doit quitter vite un pod défaillant | Baisser le produit &lt;code&gt;periodSeconds&lt;/code&gt; × &lt;code&gt;failureThreshold&lt;/code&gt; de la readiness | | Les redémarrages coûtent cher (caches, connexions) | Monter le &lt;code&gt;failureThreshold&lt;/code&gt; de la liveness ; rendre le handler trivial | | Serveur gRPC | Utiliser le handler &lt;code&gt;grpc&lt;/code&gt;, pas un contournement par exec |&lt;/p&gt;
&lt;h2&gt;Ce que les probes ne résolvent pas&lt;/h2&gt;
&lt;p&gt;Deux limites qu&apos;il vaut la peine de connaître. D&apos;abord, un &lt;code&gt;readinessProbe&lt;/code&gt; correct ne vous donne pas des déploiements sans interruption : la terminaison d&apos;un pod pendant un rollout est une course de propagation que la readiness ne peut pas influencer, et la corriger passe par les hooks &lt;code&gt;preStop&lt;/code&gt; et la gestion de SIGTERM. Les rouages sont résumés dans l&apos;entrée de glossaire sur les &lt;a href=&quot;/glossaire/rolling-deployment/&quot;&gt;rolling deployments&lt;/a&gt;. Ensuite, les probes continuent de fonctionner sous une NetworkPolicy default-deny : le kubelet sonde les pods depuis le nœud lui-même et les CNI exemptent ce trafic host-local, un détail qui compte quand on déploie une &lt;a href=&quot;/glossaire/network-policy/&quot;&gt;NetworkPolicy default-deny&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Le modèle mental qui survit au contact de la production est simple : la readiness est une décision de routage, la liveness une décision de redémarrage, la startup un budget de temps de démarrage. Configurez chacune pour ne répondre qu&apos;à sa propre question, donnez au remède le moins cher un déclenchement plus rapide qu&apos;au remède brutal, et lisez les valeurs par défaut avant de leur faire confiance. Régler les probes pour qu&apos;elles répondent honnêtement est une petite pièce d&apos;une discipline plus large, amener une plateforme à signaler ses propres pannes avant que les utilisateurs ne les voient, ce qui est l&apos;objet de mon offre &lt;a href=&quot;/expertise/fiabilite-observabilite/&quot;&gt;fiabilité et observabilité&lt;/a&gt;. Plus de contenu Kubernetes dans &lt;a href=&quot;/blog/categorie/kubernetes/&quot;&gt;la catégorie kubernetes&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Kubernetes</category><category>readiness-probe</category><category>liveness-probe</category><category>startup-probe</category><category>health-checks</category></item><item><title>Instrumenting an app for Prometheus: golden signals</title><link>https://jm-dev.it/blog/prometheus-instrumentation-golden-signals/</link><guid isPermaLink="true">https://jm-dev.it/blog/prometheus-instrumentation-golden-signals/</guid><description>Golden signals and the RED method: why latency uses a histogram, not a summary, when instrumenting an app for Prometheus, with matching PromQL queries.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=en justification: &quot;instrument application prometheus&quot;, &quot;golden signals&quot;, &quot;prometheus histogram vs summary&quot;, &quot;histogram_quantile&quot; are global technical queries searched almost exclusively in English. Reference tutorial for the international SRE/platform audience, consistent with the strategy of publishing pointed technical material in English. Foundation article beneath the already-published alerting piece: it produces the metrics that symptom-based alerts consume.&lt;/p&gt;
&lt;p&gt;Content note (JM directive 2026-07-24): reference tutorial, generic and verifiable end to end against documented Prometheus behavior (metric types, histogram_quantile, exposition format). No production anecdotes by design; this is not a REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;Your cluster is covered in metrics. &lt;code&gt;node_exporter&lt;/code&gt; reports CPU, RAM and disk usage, &lt;code&gt;kube-state-metrics&lt;/code&gt; knows the state of every pod. Then a product manager asks &quot;is checkout slower than this morning?&quot;, and nobody can answer. All of those metrics describe the machines and the orchestrator; none of them describe what your application does. That is exactly the gap instrumentation fills: exposing, from the code, the few numbers that say whether the service is serving.&lt;/p&gt;
&lt;h2&gt;The four signals, and where to start&lt;/h2&gt;
&lt;p&gt;The most useful grid is still the golden signals from Google&apos;s SRE book: latency (how long a request takes), traffic (how much you get), errors (how much fails), and saturation (how loaded your resources are). Saturation is already well covered by &lt;code&gt;node_exporter&lt;/code&gt; and cluster metrics. That leaves three things to instrument in the application itself, which the RED method captures neatly for anything that answers requests: Rate, Errors, Duration.&lt;/p&gt;
&lt;p&gt;In other words, before adding a single business metric, expose how many requests arrive, how many go wrong, and how long they take. Everything else is diagnosis you reach for once one of those three signals has paged you.&lt;/p&gt;
&lt;h2&gt;The right metric type for the question&lt;/h2&gt;
&lt;p&gt;Prometheus has four metric types, and the choice is not cosmetic. A &lt;em&gt;counter&lt;/em&gt; only ever goes up and is read with &lt;code&gt;rate()&lt;/code&gt;: it is the type for traffic and errors, an &lt;code&gt;http_requests_total&lt;/code&gt; carrying a &lt;code&gt;status&lt;/code&gt; label. A &lt;em&gt;gauge&lt;/em&gt; is an instantaneous value that goes up and down, for anything counted at a point in time (in-flight connections, queue depth). A &lt;em&gt;histogram&lt;/em&gt; samples a distribution into predefined buckets, and it is the right tool for latency. A &lt;em&gt;summary&lt;/em&gt; looks similar but computes its quantiles differently, with a heavy consequence covered below.&lt;/p&gt;
&lt;h2&gt;An instrumented handler, and the queries to match&lt;/h2&gt;
&lt;p&gt;Concretely, your client library exposes a text format like this at scrape time:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# TYPE http_requests_total counter
http_requests_total{service=&quot;checkout&quot;,status=&quot;200&quot;} 128934
http_requests_total{service=&quot;checkout&quot;,status=&quot;500&quot;} 271

# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{service=&quot;checkout&quot;,le=&quot;0.1&quot;} 24054
http_request_duration_seconds_bucket{service=&quot;checkout&quot;,le=&quot;0.3&quot;} 33444
http_request_duration_seconds_bucket{service=&quot;checkout&quot;,le=&quot;1&quot;}   34101
http_request_duration_seconds_bucket{service=&quot;checkout&quot;,le=&quot;+Inf&quot;} 34115
http_request_duration_seconds_sum{service=&quot;checkout&quot;}   5342.7
http_request_duration_seconds_count{service=&quot;checkout&quot;} 34115&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The histogram exposes cumulative counters per &lt;code&gt;le&lt;/code&gt; (less than or equal) boundary, plus a &lt;code&gt;_sum&lt;/code&gt; and a &lt;code&gt;_count&lt;/code&gt;. From there, two queries cover most of the ground:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# error rate per service
sum(rate(http_requests_total{status=~&quot;5..&quot;}[5m])) by (service)
  /
sum(rate(http_requests_total[5m])) by (service)

# p99 latency per service
histogram_quantile(
  0.99,
  sum by (service, le) (rate(http_request_duration_seconds_bucket[5m]))
)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are precisely the metrics that &lt;a href=&quot;/blog/prometheus-alerting-rules-good-vs-bad/&quot;&gt;symptom-based Prometheus alerts&lt;/a&gt; consume: instrument first, alert second. A rule watching error rate or latency only exists because the application was instrumented to expose them.&lt;/p&gt;
&lt;h2&gt;Histogram or summary: why the quantile is computed server-side&lt;/h2&gt;
&lt;p&gt;Here is the distinction that decides the type. A summary computes its quantiles in the client, per instance, and publishes a ready-made &lt;code&gt;quantile=&quot;0.99&quot;&lt;/code&gt;. The catch: you cannot sum or average quantiles. The p99 of ten pods is not the mean of their ten p99s. The moment your service runs in more than one replica, a summary hands you ten numbers that cannot be recombined into one service latency.&lt;/p&gt;
&lt;p&gt;A histogram does the opposite: it exposes raw per-bucket counts, and &lt;code&gt;histogram_quantile()&lt;/code&gt; reconstructs the quantile server-side, after you aggregate the buckets with &lt;code&gt;sum by (le)&lt;/code&gt;. Since everything runs in multiple copies on Kubernetes, the histogram is the default. Its cost: you have to choose the buckets up front.&lt;/p&gt;
&lt;h2&gt;Buckets, interpolation, and the pitfalls&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;histogram_quantile()&lt;/code&gt; interpolates linearly within the bucket where the quantile lands. The precision of your p99 is therefore the width of the bucket around it. The default buckets (a few milliseconds to ten seconds) rarely bracket a real target, so place the boundaries where you make decisions. If your target is 300ms, you want boundaries near &lt;code&gt;0.25&lt;/code&gt;, &lt;code&gt;0.3&lt;/code&gt;, &lt;code&gt;0.5&lt;/code&gt;. And if the quantile falls in the &lt;code&gt;+Inf&lt;/code&gt; bucket, the result stops being a value and becomes a lower bound, a sign your buckets are too coarse. Native histograms, a newer option, remove this bucket choice at the price of still-experimental status.&lt;/p&gt;
&lt;p&gt;Three other traps recur. Cardinality first: a label per user, per raw URL, or per request ID multiplies your series by the number of buckets and blows up storage cost, a subject the glossary entry on &lt;a href=&quot;/glossaire/cardinalite-metriques/&quot;&gt;metric cardinality&lt;/a&gt; covers. Keep labels bounded: route template, not raw path; status class, not full message. Averaging quantiles second, an &lt;code&gt;avg&lt;/code&gt; of a p99, which means nothing: aggregate the buckets, never the quantiles. Too few buckets around the target last, which makes the p99 unreadable at the exact moment it matters.&lt;/p&gt;
&lt;h2&gt;What to keep&lt;/h2&gt;
&lt;p&gt;Instrument three things from the code, traffic, errors and duration, and leave saturation to &lt;code&gt;node_exporter&lt;/code&gt;. A counter for rates, a histogram for latency, with its boundaries placed around the decision you actually make. Prefer histograms to summaries the moment the service runs in more than one replica, and keep label cardinality bounded, because an instrumented app that costs more than it observes is its own kind of outage. This kind of useful &lt;a href=&quot;/glossaire/observabilite/&quot;&gt;observability&lt;/a&gt; is the foundation of my &lt;a href=&quot;/expertise/fiabilite-observabilite/&quot;&gt;reliability and observability&lt;/a&gt; work; more in the &lt;a href=&quot;/blog/categorie/observabilite/&quot;&gt;observability category&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Observabilité</category><category>prometheus</category><category>instrumentation</category><category>golden-signals</category><category>histogram</category><category>metrics</category></item><item><title>Alertes Prometheus actionnables : symptômes, pas les causes</title><link>https://jm-dev.it/blog/regles-alerting-prometheus-symptomes-vs-causes/</link><guid isPermaLink="true">https://jm-dev.it/blog/regles-alerting-prometheus-symptomes-vs-causes/</guid><description>Pourquoi alerter sur les symptômes et non les causes, comment régler la clause for et le label severity, et une mauvaise règle Prometheus réécrite proprement.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=fr justification : cible les requêtes FR &quot;alerting prometheus&quot;, &quot;alerte symptome ou cause&quot;, &quot;clause for prometheus&quot;, et sert de relais LinkedIn francophone. Le pendant EN vise la longue traîne mondiale (même translationKey).&lt;/p&gt;
&lt;p&gt;Note contenu (directive JM 2026-07-24) : tutoriel de référence, générique et vérifiable de bout en bout contre le comportement documenté de Prometheus et Alertmanager. Aucune anecdote de production par construction ; ce n&apos;est pas un REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;3h du matin, le pager se déclenche : &lt;code&gt;HighCPUUsage&lt;/code&gt;, nœud à 92 %. Vous ouvrez un laptop, vous regardez, tout fonctionne. Les utilisateurs dorment, le batch qui a fait grimper le CPU s&apos;est terminé tout seul, et vous retournez vous coucher agacé. Deux semaines de ce régime et l&apos;équipe coupe le son du canal d&apos;alertes. La règle était techniquement correcte et totalement inutile, parce qu&apos;elle signalait une &lt;em&gt;cause&lt;/em&gt; qui n&apos;a pas forcément de conséquence visible.&lt;/p&gt;
&lt;p&gt;L&apos;idée la plus utile en alerting tient en une phrase : on réveille un humain sur un symptôme, et on laisse les causes sur les tableaux de bord. Un CPU élevé n&apos;est pas un problème. Des réponses lentes en sont un. Si le CPU est à 92 % et que le service tient toujours sa cible de latence, il n&apos;y a rien à faire. En alertant sur ce que les utilisateurs ressentent réellement, on obtient un pager qui veut dire « quelque chose est cassé maintenant » au lieu de « un chiffre a franchi un seuil ».&lt;/p&gt;
&lt;h2&gt;Alerte sur symptôme contre alerte sur cause&lt;/h2&gt;
&lt;p&gt;Une règle par cause surveille une ressource interne : CPU, mémoire, profondeur de file, remplissage disque. Une règle par symptôme surveille le contrat avec l&apos;utilisateur : taux d&apos;erreur, latence, disponibilité. Le défaut de l&apos;alerte par cause, c&apos;est que la correspondance entre cause et symptôme n&apos;est pas bijective. Un CPU saturé fait mal parfois, et le plus souvent non. Un disque plein est une vraie urgence sur un nœud de base de données, un haussement d&apos;épaules sur un réplica sans état. Chaque cause exige qu&apos;un humain tranche pour savoir si elle compte, à 3h du matin, ce qui est précisément la décision que l&apos;alerte était censée prendre à votre place.&lt;/p&gt;
&lt;p&gt;Il ne s&apos;agit pas de supprimer vos métriques de ressources. Gardez-les : c&apos;est avec elles que vous &lt;em&gt;diagnostiquez&lt;/em&gt; un incident une fois qu&apos;une alerte de symptôme vous a réveillé. La règle de pouce tient en trois mots : les symptômes réveillent, les causes renseignent. Le disque qui se remplit et le certificat qui expire sont les exceptions honnêtes, parce que ce sont des causes dont le symptôme futur est garanti et prévisible ; alerter en avance est justement tout l&apos;intérêt.&lt;/p&gt;
&lt;h2&gt;Ce que font vraiment &lt;code&gt;for&lt;/code&gt; et le label de sévérité&lt;/h2&gt;
&lt;p&gt;Prometheus évalue les règles d&apos;alerte à chaque &lt;code&gt;evaluation_interval&lt;/code&gt; (1 minute par défaut). Quand l&apos;expression d&apos;une règle renvoie des séries, les alertes correspondantes passent à l&apos;état &lt;code&gt;pending&lt;/code&gt;. Elles ne deviennent &lt;code&gt;firing&lt;/code&gt;, et ne sont transmises à Alertmanager, qu&apos;une fois l&apos;expression restée vraie sans interruption pendant la durée du champ &lt;code&gt;for&lt;/code&gt;. Une seule évaluation où l&apos;expression ne renvoie rien remet le compteur à zéro.&lt;/p&gt;
&lt;p&gt;Cette clause &lt;code&gt;for&lt;/code&gt; est votre premier filtre anti-bruit, et le moins cher. Une expression sans &lt;code&gt;for&lt;/code&gt; se déclenche sur un seul scrape : un scrape lent isolé ou un pic transitoire suffit à vous notifier. &lt;code&gt;for: 10m&lt;/code&gt; signifie « je ne m&apos;y intéresse que si c&apos;est encore vrai dans dix minutes », ce qui écarte exactement les micro-incidents auto-résolus à l&apos;origine des fausses alertes nocturnes. La contrepartie est un délai de détection, alors ajustez-le par symptôme : un service franchement à terre justifie un &lt;code&gt;for: 2m&lt;/code&gt;, une latence qui dérive lentement un &lt;code&gt;for: 15m&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;La sévérité est un simple label, pas une fonctionnalité intégrée. La convention retient &lt;code&gt;severity: critical&lt;/code&gt; pour « réveiller quelqu&apos;un » et &lt;code&gt;severity: warning&lt;/code&gt; pour « à regarder en heures ouvrées ». Le label ne veut rien dire pour Prometheus lui-même ; il devient utile dans le routage Alertmanager, où l&apos;on envoie &lt;code&gt;critical&lt;/code&gt; vers le pager et &lt;code&gt;warning&lt;/code&gt; vers un canal de discussion. Bien poser ce partage, c&apos;est l&apos;essentiel de ce qui sépare une astreinte sereine d&apos;une astreinte épuisée. Si tout est critique, plus rien ne l&apos;est.&lt;/p&gt;
&lt;h2&gt;Une mauvaise règle réécrite en bonne règle&lt;/h2&gt;
&lt;p&gt;Voici une règle qui a l&apos;air raisonnable et qui notifie sans arrêt :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;groups:
  - name: bad.rules
    rules:
      - alert: HighCPUUsage
        expr: rate(node_cpu_seconds_total{mode!=&quot;idle&quot;}[5m]) &amp;gt; 0.8
        labels:
          severity: critical
        annotations:
          summary: &quot;CPU élevé&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tout y invite à la fatigue. Elle se déclenche par cœur et non par nœud, n&apos;a pas de &lt;code&gt;for&lt;/code&gt; donc un pic isolé notifie, qualifie un chiffre de consommation de &lt;code&gt;critical&lt;/code&gt;, et son annotation ne dit rien d&apos;exploitable au répondant. Réécrite autour d&apos;un symptôme :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;groups:
  - name: good.rules
    rules:
      - alert: HighRequestErrorRate
        expr: |
          sum(rate(http_requests_total{status=~&quot;5..&quot;}[5m])) by (service)
            /
          sum(rate(http_requests_total[5m])) by (service)
            &amp;gt; 0.05
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: &quot;{{ $labels.service }} sert plus de 5 % d&apos;erreurs&quot;
          description: &quot;Ratio d&apos;erreurs 5xx à {{ $value | humanizePercentage }} sur 5m. Vérifier les déploiements récents et les dépendances en aval.&quot;
          runbook_url: &quot;https://runbooks.internal/high-error-rate&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La bonne règle alerte sur ce que vit un utilisateur (une requête en échec), agrège à une unité qui a du sens (par service), survit à un pic transitoire (&lt;code&gt;for: 10m&lt;/code&gt;) et fournit au répondant un point de départ via ses annotations et un lien de runbook. Décider &lt;em&gt;où&lt;/em&gt; placer ce seuil de 5 % relève d&apos;un &lt;a href=&quot;/glossaire/error-budget/&quot;&gt;error budget&lt;/a&gt; et d&apos;un &lt;a href=&quot;/glossaire/slo-service-level-objective/&quot;&gt;objectif de niveau de service&lt;/a&gt; défini, pas d&apos;un nombre inventé au clavier. Et cette règle suppose que les bonnes métriques existent déjà : le taux d&apos;erreur et la latence qu&apos;on interroge ici viennent d&apos;une application instrumentée pour les exposer, un préalable détaillé dans &lt;a href=&quot;/blog/instrumenter-application-prometheus-golden-signals/&quot;&gt;instrumenter une application avec Prometheus&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Les pièges qui résistent aux bonnes intentions&lt;/h2&gt;
&lt;p&gt;Trois pièges reviennent même dans un dispositif orienté symptômes. La division par zéro d&apos;abord : un ratio comme celui ci-dessus ne renvoie rien quand un service ne reçoit aucun trafic, si bien qu&apos;une panne stoppant toutes les requêtes peut faire taire l&apos;alerte censée la détecter. Doublez les alertes de taux d&apos;erreur d&apos;un contrôle de trafic faible ou d&apos;absence. Les seuils trop zélés sans notion de burn rate ensuite : un « 5 % pendant 10m » figé rate une combustion lente qui grignote le budget du mois et hurle sur un pic bref, d&apos;où l&apos;existence de l&apos;alerting multi-fenêtre multi-burn-rate. L&apos;inflation de &lt;code&gt;critical&lt;/code&gt; enfin : chaque alerte ajoutée en critique parce qu&apos;elle « semblait importante » entraîne l&apos;astreinte à ignorer le pager, et un pager qu&apos;on ignore est pire que pas de pager.&lt;/p&gt;
&lt;h2&gt;À retenir&lt;/h2&gt;
&lt;p&gt;Alertez sur les symptômes, gardez les causes pour le diagnostic. Servez-vous de &lt;code&gt;for&lt;/code&gt; pour filtrer le bruit auto-résolu, et réservez &lt;code&gt;critical&lt;/code&gt; à ce qui mérite vraiment de sortir un humain du lit. Rédigez les annotations pour la personne fatiguée qui les lit à 3h, pas pour vous qui les écrivez à 15h. Un dispositif d&apos;alerte construit ainsi est un livrable central d&apos;une mission &lt;a href=&quot;/expertise/fiabilite-observabilite/&quot;&gt;fiabilité et observabilité&lt;/a&gt;. D&apos;autres sujets dans la &lt;a href=&quot;/blog/categorie/observabilite/&quot;&gt;catégorie observabilité&lt;/a&gt;, et la même discipline appliquée aux sondes se trouve dans &lt;a href=&quot;/blog/readiness-liveness-startup-probes-kubernetes/&quot;&gt;readiness et liveness sur Kubernetes&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Observabilité</category><category>prometheus</category><category>alerting</category><category>alertmanager</category><category>sre</category><category>supervision</category></item><item><title>Requests et limits Kubernetes : throttling, OOMKill et QoS</title><link>https://jm-dev.it/blog/requests-limits-kubernetes-cpu-memoire/</link><guid isPermaLink="true">https://jm-dev.it/blog/requests-limits-kubernetes-cpu-memoire/</guid><description>Ce qui se passe vraiment quand un pod Kubernetes dépasse sa limite CPU ou mémoire, les classes QoS, le débat des limites, et une méthode de réglage.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- justification lang=fr : version française de l&apos;article EN &quot;kubernetes-resource-requests-limits&quot;. Le contenu est un tutoriel de référence générique ; la version FR sert le public francophone (relais LinkedIn FR) et le maillage interne vers le glossaire et les offres. Paire liée via translationKey.&lt;/p&gt;
&lt;p&gt;Note de contenu (directive JM 2026-07-24) : tutoriel de référence, générique et vérifiable de bout en bout contre la documentation Kubernetes, le comportement des cgroups et les métriques cAdvisor. Aucune anecdote de production par construction, ce n&apos;est pas un REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;Un pod tourne sans histoire, puis disparaît avec &lt;code&gt;OOMKilled&lt;/code&gt; et le code de sortie 137. Un autre rame sous charge alors que le nœud qui l&apos;héberge est inoccupé à 70 %. Les deux symptômes remontent aux mêmes deux champs que la plupart des manifests renseignent par réflexe et que peu de gens lisent vraiment : &lt;code&gt;requests&lt;/code&gt; et &lt;code&gt;limits&lt;/code&gt;. Ce ne sont pas deux réglages du même curseur. Ils agissent à des moments différents, sur des sous-systèmes différents, avec des conséquences opposées quand on les franchit.&lt;/p&gt;
&lt;h2&gt;Requests et limits ne font pas le même travail&lt;/h2&gt;
&lt;p&gt;Une &lt;strong&gt;request&lt;/strong&gt; est une donnée d&apos;ordonnancement. Le scheduler place les pods en additionnant leurs requests par rapport à la capacité allouable d&apos;un nœud ; une request est une réservation, le plancher que le pod est certain d&apos;obtenir. Elle ne plafonne jamais l&apos;usage.&lt;/p&gt;
&lt;p&gt;Une &lt;strong&gt;limit&lt;/strong&gt; est un plafond d&apos;exécution imposé par le kubelet et le noyau via les cgroups. Ce qui arrive quand un conteneur touche ce plafond dépend entièrement de la ressource concernée, car le CPU et la mémoire se comportent à l&apos;inverse l&apos;un de l&apos;autre.&lt;/p&gt;
&lt;h2&gt;Franchir la limite CPU : le throttling&lt;/h2&gt;
&lt;p&gt;Le CPU est une ressource compressible. Le noyau peut accorder moins de cycles à un processus sans le tuer.&lt;/p&gt;
&lt;p&gt;La request CPU devient un poids cgroup (&lt;code&gt;cpu.shares&lt;/code&gt; en cgroup v1, &lt;code&gt;cpu.weight&lt;/code&gt; en v2, où un cœur vaut 1024 shares). Ce poids ne compte qu&apos;en cas de contention : il décide comment des processus affamés se partagent un CPU saturé. Tant que le nœud a de la marge, tout le monde s&apos;exécute librement, requests ou pas.&lt;/p&gt;
&lt;p&gt;La limit CPU devient du CFS bandwidth control : un quota par période d&apos;ordonnancement (&lt;code&gt;cpu.cfs_quota_us&lt;/code&gt; / &lt;code&gt;cpu.cfs_period_us&lt;/code&gt;, ou &lt;code&gt;cpu.max&lt;/code&gt; en v2), avec une période de 100 ms par défaut. Une limite à &lt;code&gt;500m&lt;/code&gt; accorde 50 ms de CPU par fenêtre de 100 ms. Épuisez ce budget avant la fin de la fenêtre et le conteneur est throttlé, mis en pause jusqu&apos;à la période suivante. Pas de kill, juste de la latence.&lt;/p&gt;
&lt;p&gt;Le piège tient à cette application par période. Le throttling se déclenche que le nœud soit chargé ou non, si bien qu&apos;une charge en pics et sensible à la latence peut être fortement throttlée pendant que la machine reste largement inoccupée. Surveillez le rapport entre &lt;code&gt;container_cpu_cfs_throttled_periods_total&lt;/code&gt; et &lt;code&gt;container_cpu_cfs_periods_total&lt;/code&gt; : une valeur élevée, c&apos;est du throttling CFS, pas une application lente.&lt;/p&gt;
&lt;h2&gt;Franchir la limite mémoire : l&apos;OOMKill&lt;/h2&gt;
&lt;p&gt;La mémoire est incompressible. On ne peut pas demander à un processus de détenir moins de RAM comme on lui donne moins de cycles.&lt;/p&gt;
&lt;p&gt;La request mémoire alimente l&apos;ordonnancement et le classement d&apos;éviction ; ce n&apos;est pas un plafond. La limit mémoire, elle, est un plafond dur (&lt;code&gt;memory.max&lt;/code&gt; / &lt;code&gt;memory.limit_in_bytes&lt;/code&gt;). Touchez un octet au-delà et l&apos;OOM killer du cgroup envoie un SIGKILL à un processus du groupe. Kubernetes marque le conteneur &lt;code&gt;OOMKilled&lt;/code&gt;, code de sortie 137 (128 + SIGKILL), le redémarre selon le &lt;code&gt;restartPolicy&lt;/code&gt;, et tombe en &lt;code&gt;CrashLoopBackOff&lt;/code&gt; si cela se répète.&lt;/p&gt;
&lt;p&gt;Deux événements mémoire distincts prêtent à confusion. Atteindre la limite de son propre conteneur, c&apos;est un OOMKill de cgroup : niveau noyau, immédiat, cantonné au pod. Le nœud globalement à court de mémoire déclenche plutôt une éviction par le kubelet, qui trie ses victimes selon la classe QoS et la distance de chaque pod au-dessus de sa request.&lt;/p&gt;
&lt;h2&gt;Les classes QoS se déduisent, elles ne se déclarent pas&lt;/h2&gt;
&lt;p&gt;Kubernetes attribue une classe à chaque pod à partir de ce que vous avez posé :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Guaranteed&lt;/strong&gt; : chaque conteneur fixe &lt;code&gt;requests == limits&lt;/code&gt; pour le CPU et la mémoire. Il reçoit un &lt;code&gt;oom_score_adj&lt;/code&gt; fortement négatif (-997) et est évincé en dernier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Burstable&lt;/strong&gt; : au moins une request ou une limit est posée, sans atteindre Guaranteed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BestEffort&lt;/strong&gt; : rien du tout. Il obtient un &lt;code&gt;oom_score_adj&lt;/code&gt; de 1000, et c&apos;est le premier évincé comme le premier que le noyau tue sous pression mémoire.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La classe n&apos;est pas cosmétique : sous pression, elle décide qui meurt d&apos;abord, et cet ordre justifie à lui seul de poser ses requests honnêtement.&lt;/p&gt;
&lt;h2&gt;Le débat des limites CPU&lt;/h2&gt;
&lt;p&gt;C&apos;est un vrai sujet, toujours vivant. L&apos;argument pour retirer les limites CPU : le throttling CFS coûte de la latence même sur un nœud inoccupé, et une request CPU garantit déjà une part proportionnelle en cas de contention, donc une limite ne fait souvent qu&apos;acheter du throttling inutile. L&apos;argument pour les garder : une planification de capacité prévisible, une protection contre un voisin qui s&apos;emballe, et le fait que les limites CPU sont requises pour la classe Guaranteed et pour le pinning CPU statique.&lt;/p&gt;
&lt;p&gt;Le compromis sur lequel atterrissent la plupart des équipes plateforme : poser presque toujours &lt;code&gt;request == limit&lt;/code&gt; en mémoire (ressource incompressible, on veut un plafond dur et prévisible), toujours poser une request CPU, et traiter les limites CPU comme optionnelles, appliquées seulement là où une règle précise l&apos;exige.&lt;/p&gt;
&lt;h2&gt;Une méthode pour régler les défauts&lt;/h2&gt;
&lt;p&gt;Mesurer, ne pas deviner. Utilisez le Vertical Pod Autoscaler en mode recommandation (&lt;code&gt;updateMode: &quot;Off&quot;&lt;/code&gt;) ou interrogez directement l&apos;historique Prometheus. Le même réflexe de mesure que pour un &lt;a href=&quot;/glossaire/slo-service-level-objective/&quot;&gt;SLO&lt;/a&gt; s&apos;applique ici : on règle ce qu&apos;on a observé, jamais ce qu&apos;on imagine.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# pic de working set mémoire sur 7 jours, par pod
max_over_time(container_memory_working_set_bytes{container=&quot;app&quot;}[7d])

# 95e centile de cœurs CPU consommés sur 7 jours
quantile_over_time(0.95, rate(container_cpu_usage_seconds_total{container=&quot;app&quot;}[5m])[7d:5m])&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;On dérive ensuite les valeurs de ces chiffres :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resources:
  requests:
    cpu: 250m           # usage typique, pilote l&apos;ordonnancement
    memory: 512Mi
  limits:
    memory: 512Mi       # égale à la request : plafond dur et prévisible
    # pas de limite CPU : peut déborder sur la capacité libre, garde 250m sous contention&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Mémoire &lt;code&gt;request == limit&lt;/code&gt;, calée sur le pic de working set observé plus 20 à 30 % de marge. Request CPU à l&apos;usage typique (autour du P90), limite CPU laissée de côté sauf règle contraire. On remesure une fois le vrai trafic arrivé : les premiers chiffres sont une hypothèse, pas un verdict.&lt;/p&gt;
&lt;h2&gt;Pièges classiques&lt;/h2&gt;
&lt;p&gt;Ne poser qu&apos;une limit fait recopier cette valeur dans la request, ce qui peut sur-réserver le nœud en silence. Reprendre les chiffres d&apos;une autre équipe ignore que les requests sont propres à la charge : trop hautes elles gâchent des nœuds, trop basses elles invitent l&apos;éviction. Dimensionner la mémoire sur le RSS plutôt que sur le working set oublie le page cache que le noyau compte dans votre limite. Accuser l&apos;application d&apos;une latence qui n&apos;est que du throttling CFS fait perdre des journées : vérifiez le rapport de throttling avant de profiler du code.&lt;/p&gt;
&lt;h2&gt;À retenir&lt;/h2&gt;
&lt;p&gt;Les requests sont pour le scheduler, les limits pour le noyau. Au-delà de la limite CPU on est throttlé, au-delà de la limite mémoire on est tué. Posez la mémoire &lt;code&gt;request == limit&lt;/code&gt;, posez une request CPU, soyez délibéré sur les limites CPU, et tirez chaque chiffre d&apos;un usage mesuré plutôt que d&apos;un template. Un redémarrage dû à un OOMKill et un redémarrage dû à un liveness probe qui échoue sont indiscernables dans &lt;code&gt;kubectl describe&lt;/code&gt;, d&apos;où l&apos;intérêt de savoir &lt;a href=&quot;/blog/readiness-liveness-startup-probes-kubernetes/&quot;&gt;comment se comportent vraiment les probes&lt;/a&gt; avant de déboguer la mauvaise. Bien dimensionner ces ressources sur tout un parc fait une bonne partie du travail qui rend une migration vers Kubernetes durable en production, et c&apos;est le cœur de mon offre &lt;a href=&quot;/expertise/migration-kubernetes/&quot;&gt;migration d&apos;infrastructure legacy vers Kubernetes&lt;/a&gt;. Plus de contenu Kubernetes dans &lt;a href=&quot;/blog/categorie/kubernetes/&quot;&gt;la catégorie kubernetes&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Kubernetes</category><category>ressources</category><category>requests-limits</category><category>qos</category><category>oomkilled</category><category>throttling-cpu</category></item><item><title>Kubernetes : Secrets natifs, Sealed Secrets ou Vault ?</title><link>https://jm-dev.it/blog/secrets-management-vault-vs-sealed-secrets-fr/</link><guid isPermaLink="true">https://jm-dev.it/blog/secrets-management-vault-vs-sealed-secrets-fr/</guid><description>Les Secrets Kubernetes sont du base64, pas du chiffrement. Comment Sealed Secrets rend un manifeste committable, quand Vault vaut son coût, comment choisir.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- Justification lang=fr : version française d&apos;un tutoriel de référence générique, alignée sur l&apos;amendement §2.4 (i18n systématique pour le contenu factuel générique). L&apos;original EN vise la longue traîne technique mondiale ; cette version couvre les requêtes francophones (&quot;secrets kubernetes chiffrement&quot;, &quot;sealed secrets vault&quot;) et relaie sur LinkedIn FR.&lt;/p&gt;
&lt;p&gt;Note de contenu (directive JM 2026-07-24) : tutoriel de référence, générique et vérifiable de bout en bout face à la documentation Kubernetes, Sealed Secrets et Vault. Aucune anecdote de production par construction, ce n&apos;est pas un REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kubectl get secret app-db -o yaml&lt;/code&gt; renvoie un bloc &lt;code&gt;data&lt;/code&gt; rempli de chaînes illisibles. Ça ressemble à du chiffré. Ça n&apos;en est pas. Les valeurs d&apos;un Secret Kubernetes sont encodées en base64, et le base64 est un encodage de transport que n&apos;importe qui inverse en une commande :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get secret app-db -o jsonpath=&apos;{.data.password}&apos; | base64 -d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ce seul fait commande toutes les décisions qui suivent. Un Secret natif ne protège rien en lui-même. Ce qui le garde vraiment, c&apos;est le RBAC (qui a le droit de faire &lt;code&gt;get&lt;/code&gt; sur l&apos;objet) et, au repos, le fait qu&apos;etcd soit chiffré ou non.&lt;/p&gt;
&lt;h2&gt;Ce qu&apos;un Secret natif protège, et ce qu&apos;il ne protège pas&lt;/h2&gt;
&lt;p&gt;Par défaut, etcd stocke les objets Secret sans chiffrement. Le chiffrement au repos s&apos;active explicitement, via une &lt;code&gt;EncryptionConfiguration&lt;/code&gt; sur l&apos;API server :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: [&quot;secrets&quot;]
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: &amp;lt;clé base64 de 32 octets&amp;gt;
      - identity: {}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sans cette configuration, une sauvegarde d&apos;etcd est une sauvegarde de tous vos credentials en clair (en base64). Avec elle, la valeur est chiffrée dans le datastore mais reste du base64 pour quiconque dispose d&apos;un accès en lecture à l&apos;API. Deux modèles de menace distincts, et les Secrets natifs ne couvrent le premier qu&apos;une fois que vous l&apos;avez délibérément activé.&lt;/p&gt;
&lt;p&gt;Le problème que les Secrets natifs ne résolvent jamais, c&apos;est Git. On ne commit pas un manifeste Secret en clair dans un dépôt. C&apos;est dans cette faille que vivent les deux autres approches.&lt;/p&gt;
&lt;h2&gt;Sealed Secrets : rendre le manifeste committable&lt;/h2&gt;
&lt;p&gt;Sealed Secrets (le contrôleur de Bitnami) ajoute une CRD et un contrôleur dans le cluster qui détient une paire de clés asymétriques. On chiffre avec le certificat public grâce à &lt;code&gt;kubeseal&lt;/code&gt;, et seule la clé privée du contrôleur peut déchiffrer :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create secret generic app-db \
  --from-literal=password=&apos;s3cr3t&apos; --dry-run=client -o yaml \
  | kubeseal --format yaml &amp;gt; sealed-app-db.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le &lt;code&gt;SealedSecret&lt;/code&gt; obtenu vit sans risque dans Git. Le contrôleur le détecte, le déchiffre et matérialise un Secret ordinaire dans le namespace. Votre pipeline &lt;a href=&quot;/glossaire/gitops/&quot;&gt;GitOps&lt;/a&gt; décrit désormais les secrets comme il décrit tout le reste.&lt;/p&gt;
&lt;p&gt;Deux propriétés dessinent les limites. Le scellage est lié au cluster : un SealedSecret est chiffré pour la clé d&apos;un contrôleur donné, donc restaurer les manifestes dans un cluster neuf échoue tant que vous n&apos;avez pas sauvegardé puis restauré la clé de scellage (elle-même un Secret dans &lt;code&gt;kube-system&lt;/code&gt;). Perdre cette clé rend irrécupérable chaque SealedSecret présent dans Git. Le scope par défaut est également strict : le chiffré est lié au couple exact &lt;code&gt;name&lt;/code&gt; plus &lt;code&gt;namespace&lt;/code&gt;, et on ne peut ni renommer ni déplacer un SealedSecret sans le resceller.&lt;/p&gt;
&lt;p&gt;Sealed Secrets stocke des valeurs chiffrées. Il ne les fait pas tourner, ne les expire pas, ne trace pas qui les lit. Faire tourner un mot de passe reste un commit.&lt;/p&gt;
&lt;h2&gt;Vault : secrets dynamiques, leasing, audit&lt;/h2&gt;
&lt;p&gt;Vault en externe change le modèle. Plutôt que de stocker un credential statique, Vault peut en générer un à la demande et le remettre avec un lease :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vault write database/roles/app-db \
  db_name=app-postgres \
  creation_statements=&quot;CREATE ROLE \&quot;{{name}}\&quot; WITH LOGIN PASSWORD &apos;{{password}}&apos; VALID UNTIL &apos;{{expiration}}&apos;;&quot; \
  default_ttl=&quot;1h&quot; max_ttl=&quot;24h&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Une application s&apos;authentifie (l&apos;auth Kubernetes échange le token de ServiceAccount d&apos;un pod contre un token Vault), lit &lt;code&gt;database/creds/app-db&lt;/code&gt; et récupère un utilisateur Postgres neuf, valide une heure. À l&apos;expiration du lease, Vault révoque le credential à la source. Un credential fuité a alors un rayon d&apos;action qui se compte en minutes, et chaque lecture atterrit dans un journal d&apos;audit.&lt;/p&gt;
&lt;p&gt;Tirer ces valeurs jusque dans les pods se fait en général avec l&apos;External Secrets Operator ou le Vault Secrets Operator, qui synchronisent un chemin Vault vers un Secret natif consommé normalement par les workloads.&lt;/p&gt;
&lt;p&gt;Le coût est opérationnel et bien réel : un Vault en haute dispo, c&apos;est un stockage Raft sur trois nœuds ou plus, une stratégie d&apos;unseal (auto-unseal via un KMS cloud, ou des porteurs humains de parts de clé Shamir), des sauvegardes testées et une cadence de montée de version à tenir. Depuis le passage de Vault sous licence BSL, OpenBao est le fork open source à mettre dans la balance.&lt;/p&gt;
&lt;h2&gt;Les pièges classiques&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prendre le base64 pour une frontière de sécurité.&lt;/strong&gt; L&apos;erreur la plus fréquente, et de loin. Restreignez le RBAC, activez le chiffrement au repos, et ne lisez jamais &lt;code&gt;data:&lt;/code&gt; comme une protection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ne pas sauvegarder la clé de Sealed Secrets.&lt;/strong&gt; On commit les SealedSecrets, on se sent en sécurité, puis on découvre après une reconstruction de cluster que Git seul ne les restaure pas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Déployer Vault pour des secrets statiques.&lt;/strong&gt; Si rien de ce que vous stockez n&apos;est dynamique et que vous ne touchez jamais au leasing ni à l&apos;audit, vous avez pris en charge la contrainte d&apos;exploitation d&apos;un système distribué pour faire ce qu&apos;un seul contrôleur Sealed Secrets fait déjà.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Oublier qu&apos;un Secret est référencé par son nom.&lt;/strong&gt; Faire un rollback d&apos;un Deployment ne fait pas de rollback du Secret qu&apos;il pointe.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Comment trancher&lt;/h2&gt;
&lt;p&gt;La taille de l&apos;équipe et l&apos;exigence de conformité tracent la ligne :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Petite équipe, pas de store externe, GitOps déjà en place :&lt;/strong&gt; Secrets natifs avec chiffrement au repos, plus Sealed Secrets pour que le dépôt soit la source de vérité. Coût d&apos;exploitation minimal, et ça couvre la plupart des PME.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secrets statiques, un fournisseur cloud disponible :&lt;/strong&gt; un secret manager managé derrière External Secrets Operator déplace la séparation des environnements dans l&apos;IAM, exactement là où on la veut. C&apos;est la même logique que celle du &lt;a href=&quot;/glossaire/service-discovery/&quot;&gt;service discovery&lt;/a&gt; : ne pas coder en dur ce qui doit rester découvrable et révocable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Credentials dynamiques, vraies exigences d&apos;audit ou de rotation, parc hétérogène (VMs plus Kubernetes plus CI) :&lt;/strong&gt; Vault gagne sa place, mais seulement si vous pouvez nommer aujourd&apos;hui la personne qui le mettra à jour dans dix-huit mois.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;À retenir&lt;/h2&gt;
&lt;p&gt;Les Secrets natifs sont du base64, pas du chiffrement ; ce sont le RBAC et le chiffrement d&apos;etcd au repos qui les protègent réellement. Sealed Secrets rend un manifeste committable et s&apos;insère dans un dépôt GitOps, au prix d&apos;une clé liée au cluster qu&apos;il ne faut jamais perdre. Vault achète des secrets dynamiques, du leasing et de l&apos;audit, au prix d&apos;un système distribué à état à opérer. Calez l&apos;outil sur un fait que vous pouvez désigner (une clause de conformité, une obligation de rotation, un parc hybride), pas sur une intention. Ces arbitrages font partie du travail que je mène dans mon offre &lt;a href=&quot;/expertise/industrialisation-cicd/&quot;&gt;industrialisation du CI/CD&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Automatisation</category><category>kubernetes</category><category>secrets</category><category>vault</category><category>sealed-secrets</category><category>securite</category></item><item><title>Kubernetes Secrets: Plain Secrets, Sealed Secrets, or Vault?</title><link>https://jm-dev.it/blog/secrets-management-vault-vs-sealed-secrets/</link><guid isPermaLink="true">https://jm-dev.it/blog/secrets-management-vault-vs-sealed-secrets/</guid><description>Kubernetes Secrets are base64, not encryption. How Sealed Secrets make manifests git-safe, when external Vault earns its cost, and how to choose.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=en justification: &quot;kubernetes secrets encryption&quot;, &quot;sealed secrets vs vault&quot; and &quot;base64 secret decode&quot; are global technical queries typed almost exclusively in English by engineers evaluating a secrets strategy. Fits the strategy of publishing reference tutorials in English for the worldwide long tail.&lt;/p&gt;
&lt;p&gt;Content note (JM directive 2026-07-24): reference tutorial, generic and verifiable end to end against Kubernetes, Sealed Secrets and Vault documented behavior. No production anecdotes by design, this is not a REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kubectl get secret app-db -o yaml&lt;/code&gt; returns a &lt;code&gt;data&lt;/code&gt; block full of unreadable strings. It looks encrypted. It is not. Kubernetes Secret values are base64-encoded, and base64 is a transport encoding anyone can reverse in a single command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl get secret app-db -o jsonpath=&apos;{.data.password}&apos; | base64 -d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That one fact drives every decision below. A native Secret protects nothing on its own. What actually guards it is RBAC (who is allowed to &lt;code&gt;get&lt;/code&gt; the object) and, at rest, whether etcd itself is encrypted.&lt;/p&gt;
&lt;h2&gt;What native Secrets do and do not protect&lt;/h2&gt;
&lt;p&gt;By default, etcd stores Secret objects without encryption. Encryption at rest is opt-in, configured with an &lt;code&gt;EncryptionConfiguration&lt;/code&gt; on the API server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: [&quot;secrets&quot;]
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: &amp;lt;32-byte base64 key&amp;gt;
      - identity: {}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without it, an etcd backup is a backup of every credential in plaintext (base64). With it, the value is encrypted in the datastore but still plain base64 to anyone holding API read access. Those are two different threat models, and native Secrets address the first one only once you deliberately turn it on.&lt;/p&gt;
&lt;p&gt;The problem native Secrets never solve is Git. You cannot commit a plaintext Secret manifest to a repository. That gap is where the other two approaches live.&lt;/p&gt;
&lt;h2&gt;Sealed Secrets: make the manifest safe to commit&lt;/h2&gt;
&lt;p&gt;Sealed Secrets (the Bitnami controller) adds a CRD and an in-cluster controller holding an asymmetric key pair. You encrypt with the public certificate using &lt;code&gt;kubeseal&lt;/code&gt;, and only the controller&apos;s private key can decrypt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;kubectl create secret generic app-db \
  --from-literal=password=&apos;s3cr3t&apos; --dry-run=client -o yaml \
  | kubeseal --format yaml &amp;gt; sealed-app-db.yaml&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting &lt;code&gt;SealedSecret&lt;/code&gt; is safe in Git. The controller watches for it, decrypts it, and materialises an ordinary Secret in the namespace. Your &lt;a href=&quot;/glossaire/gitops/&quot;&gt;GitOps&lt;/a&gt; pipeline now describes secrets the same way it describes everything else.&lt;/p&gt;
&lt;p&gt;Two properties define the boundaries. Sealing is cluster-bound: a SealedSecret is encrypted for one controller&apos;s key, so restoring manifests into a fresh cluster fails until you have backed up and restored the sealing key (itself a Secret in &lt;code&gt;kube-system&lt;/code&gt;). Lose that key and every SealedSecret in Git becomes unrecoverable. The default scope is also strict, meaning the ciphertext is bound to the exact &lt;code&gt;name&lt;/code&gt; plus &lt;code&gt;namespace&lt;/code&gt;; you cannot rename or move a SealedSecret without re-sealing it.&lt;/p&gt;
&lt;p&gt;Sealed Secrets stores encrypted values. It does not rotate them, expire them, or record who reads them. Rotating a password stays an edit-and-commit.&lt;/p&gt;
&lt;h2&gt;Vault: dynamic secrets, leasing, audit&lt;/h2&gt;
&lt;p&gt;External Vault changes the model. Rather than storing a static credential, Vault can generate one on demand and hand it back with a lease:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vault write database/roles/app-db \
  db_name=app-postgres \
  creation_statements=&quot;CREATE ROLE \&quot;{{name}}\&quot; WITH LOGIN PASSWORD &apos;{{password}}&apos; VALID UNTIL &apos;{{expiration}}&apos;;&quot; \
  default_ttl=&quot;1h&quot; max_ttl=&quot;24h&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;An application authenticates (Kubernetes auth exchanges a pod&apos;s ServiceAccount token for a Vault token), reads &lt;code&gt;database/creds/app-db&lt;/code&gt;, and receives a fresh Postgres user valid for one hour. When the lease expires, Vault revokes the credential at the source. A leaked credential then has a blast radius measured in minutes, and every read lands in an audit log.&lt;/p&gt;
&lt;p&gt;Pulling those values into pods is usually done through the External Secrets Operator or the Vault Secrets Operator, which sync a Vault path into a native Secret so workloads consume it the normal way.&lt;/p&gt;
&lt;p&gt;The cost is operational and concrete: an HA Vault means Raft storage across three or more nodes, an unseal strategy (auto-unseal via a cloud KMS, or human holders of Shamir key shares), tested backups, and an upgrade cadence to follow. Since Vault moved to the BSL license, OpenBao is the open-source fork worth putting on the table.&lt;/p&gt;
&lt;h2&gt;Common traps&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Treating base64 as a security boundary.&lt;/strong&gt; The single most frequent mistake. Restrict RBAC and enable encryption at rest; never read &lt;code&gt;data:&lt;/code&gt; as protection.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Not backing up the Sealed Secrets key.&lt;/strong&gt; Teams commit SealedSecrets, feel safe, then discover after a cluster rebuild that Git alone cannot restore them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deploying Vault for static secrets.&lt;/strong&gt; If nothing you store is dynamic and you never touch leasing or audit, you have taken on a distributed system&apos;s operational burden to do what one Sealed Secrets controller already does.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Forgetting that a Secret is referenced by name.&lt;/strong&gt; Rolling back a Deployment does not roll back the Secret it points to.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;How to choose&lt;/h2&gt;
&lt;p&gt;Team size and compliance requirements draw the line:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Small team, no external store, GitOps already in place:&lt;/strong&gt; native Secrets with encryption at rest, plus Sealed Secrets so the repository is the source of truth. Lowest operational cost, and it covers most small and mid-size shops.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Static secrets, a cloud provider available:&lt;/strong&gt; a managed secret store behind External Secrets Operator moves separation of environments into IAM, which is exactly where it belongs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dynamic credentials, real audit or rotation mandates, a mixed estate (VMs plus Kubernetes plus CI):&lt;/strong&gt; Vault earns its keep, but only when you can name today the person who will patch it in eighteen months.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;p&gt;Native Secrets are base64, not encryption; RBAC and etcd encryption at rest are what actually protect them. Sealed Secrets make a manifest safe to commit and fit a GitOps repository, at the price of one cluster-bound key you must never lose. Vault buys dynamic secrets, leasing and audit, at the price of running a stateful distributed system. Match the tool to a fact you can point at (a compliance clause, a rotation mandate, a hybrid estate), not to an aspiration. More material in &lt;a href=&quot;/blog/categorie/automatisation/&quot;&gt;the automatisation category&lt;/a&gt;, and this kind of arbitration is part of &lt;a href=&quot;/expertise/industrialisation-cicd/&quot;&gt;industrialising your CI/CD&lt;/a&gt;.&lt;/p&gt;</content:encoded><category>Automatisation</category><category>kubernetes</category><category>secrets</category><category>vault</category><category>sealed-secrets</category><category>security</category></item><item><title>State Terraform distant et verrouillage : comment ça marche</title><link>https://jm-dev.it/blog/terraform-state-locking-remote-backend-fr/</link><guid isPermaLink="true">https://jm-dev.it/blog/terraform-state-locking-remote-backend-fr/</guid><description>Pourquoi le state local casse en équipe, comment marchent les backends distants et le verrouillage, et comment récupérer proprement d&apos;un verrou bloqué.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- Justification lang=fr : cet article couvre la requête « terraform state verrouillage » et « backend distant terraform » côté francophone, où la concurrence SEO est faible face au contenu anglais. Il alimente aussi le relais LinkedIn FR vers l&apos;offre industrialisation CI/CD. La version EN existe pour la longue traîne technique mondiale (translationKey partagé).&lt;/p&gt;
&lt;p&gt;Note de contenu (directive JM 2026-07-24) : tutoriel de référence, générique et vérifiable de bout en bout contre la documentation Terraform et le comportement des backends. Aucune anecdote de production par choix, ce n&apos;est pas un REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;Deux ingénieurs lancent &lt;code&gt;terraform apply&lt;/code&gt; sur la même stack à quelques secondes d&apos;intervalle. Les deux lisent le state, les deux planifient dessus, les deux le réécrivent. La seconde écriture écrase la première en silence, et le state décrit désormais une infrastructure qui ne correspond plus au réel. Personne n&apos;a vu passer d&apos;erreur. C&apos;est précisément le scénario contre lequel le state local n&apos;a jamais été conçu pour protéger, et la raison d&apos;être des backends distants.&lt;/p&gt;
&lt;h2&gt;Pourquoi le state local ne tient pas en équipe&lt;/h2&gt;
&lt;p&gt;Terraform conserve un fichier de state qui associe chaque ressource de votre configuration à un objet réel chez votre fournisseur, avec les métadonnées et l&apos;ordre des dépendances. Par défaut, ce fichier est &lt;code&gt;terraform.tfstate&lt;/code&gt;, posé sur le poste de la dernière personne à avoir lancé un apply.&lt;/p&gt;
&lt;p&gt;Ce défaut lâche dès qu&apos;une deuxième personne arrive :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Il n&apos;est pas partagé.&lt;/strong&gt; Le state de votre collègue ignore les ressources que vous avez créées. Son prochain plan veut les recréer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Il n&apos;a aucun verrou.&lt;/strong&gt; Rien n&apos;empêche deux apply de tourner en même temps, et le cycle lecture-modification-écriture de Terraform n&apos;est pas atomique entre deux machines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Il expose des secrets.&lt;/strong&gt; Le state stocke les attributs des ressources en clair, y compris les mots de passe et clés générés, désormais éparpillés sur des postes et parfois dans Git.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Un backend distant règle les trois d&apos;un coup : une copie qui fait autorité, un stockage à accès contrôlé, et un mécanisme de verrouillage.&lt;/p&gt;
&lt;h2&gt;Ce qu&apos;apporte un backend distant&lt;/h2&gt;
&lt;p&gt;Le backend se déclare dans le bloc &lt;code&gt;terraform&lt;/code&gt;. Le backend S3 sert de référence courante :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;terraform {
  backend &quot;s3&quot; {
    bucket       = &quot;acme-tfstate&quot;
    key          = &quot;platform/prod/terraform.tfstate&quot;
    region       = &quot;eu-west-1&quot;
    encrypt      = true
    use_lockfile = true   # verrouillage S3 natif, Terraform 1.10+
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le state vit maintenant dans S3, versionné et chiffré, et le verrou est géré par un fichier de lock écrit à côté, via les écritures conditionnelles de S3. Historiquement ce verrou reposait sur une table DynamoDB dédiée (clé primaire &lt;code&gt;LockID&lt;/code&gt;) ; cette approche fonctionne toujours, mais le lock file natif a supprimé cette dépendance supplémentaire pour les nouvelles stacks.&lt;/p&gt;
&lt;p&gt;Les autres backends déclinent la même idée autrement. Le backend GCS intègre le verrouillage nativement, à partir des numéros de génération d&apos;objet, sans table annexe. HCP Terraform (ex-Terraform Cloud) prend en charge le stockage du state et le verrou pour vous, et ajoute par-dessus l&apos;historique des runs et les politiques d&apos;accès.&lt;/p&gt;
&lt;h2&gt;Comment le verrou fonctionne réellement&lt;/h2&gt;
&lt;p&gt;Avant toute opération susceptible d&apos;écrire le state (&lt;code&gt;apply&lt;/code&gt;, et &lt;code&gt;plan&lt;/code&gt; quand il rafraîchit), Terraform acquiert un verrou. Ce verrou est un petit enregistrement qui retient qui l&apos;a pris, pour quelle opération, quand, et sous quel identifiant. Tant qu&apos;il est détenu, un second run refuse de démarrer :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ terraform apply
╷
│ Error: Error acquiring the state lock
│
│ Lock Info:
│   ID:        a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
│   Operation: OperationTypeApply
│   Who:       alice@build-runner-07
│   Created:   2026-07-24 09:14:22 UTC
╵&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C&apos;est le système qui fait exactement son travail. Le verrou est relâché automatiquement à la fin de l&apos;opération, qu&apos;elle réussisse ou qu&apos;elle échoue proprement.&lt;/p&gt;
&lt;h2&gt;Le piège classique : le verrou fantôme&lt;/h2&gt;
&lt;p&gt;Le verrou n&apos;est relâché proprement que si Terraform va jusqu&apos;au bout de sa libération. Coupez le processus avec deux Ctrl-C, perdez le réseau en plein apply, ou faites évincer un runner de CI, et le verrou reste détenu sans propriétaire derrière. Chaque run suivant échoue alors sur le même &lt;code&gt;Error acquiring the state lock&lt;/code&gt;, ce qui bloque toute l&apos;équipe.&lt;/p&gt;
&lt;p&gt;La porte de sortie est &lt;code&gt;force-unlock&lt;/code&gt;, avec l&apos;ID lu dans l&apos;erreur :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;terraform force-unlock a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;À manier comme une arme chargée. Forcer le déverrouillage alors qu&apos;un apply tourne encore réellement réintroduit exactement la corruption par écriture concurrente que le verrou existait pour empêcher. Assurez-vous que le processus propriétaire est bien mort (vérifiez le job CI, interrogez la personne nommée dans &lt;code&gt;Who&lt;/code&gt;) avant de déverrouiller. Ne câblez jamais &lt;code&gt;force-unlock&lt;/code&gt; dans une automatisation, et ne dégainez pas &lt;code&gt;-lock=false&lt;/code&gt; pour faire disparaître l&apos;erreur : cela désactive le verrouillage pour de vrais apply.&lt;/p&gt;
&lt;h2&gt;La dérive, et pourquoi plan est votre détecteur&lt;/h2&gt;
&lt;p&gt;Le state part du principe qu&apos;il est seul à modifier votre infrastructure. Quand quelqu&apos;un édite une ressource à la main dans une console, ou qu&apos;un autre outil y touche, le réel s&apos;écarte de ce que le state enregistre. Cet écart, c&apos;est la &lt;strong&gt;dérive&lt;/strong&gt; (drift).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;terraform plan&lt;/code&gt; est le détecteur : il rafraîchit le state à partir des attributs réels du fournisseur, puis compare la configuration, le state et la réalité. Un run en mode rafraîchissement seul isole la dérive pure des changements que vous voulez vraiment appliquer :&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;terraform plan -refresh-only&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si ce plan montre des différences sans aucune modification de configuration de votre côté, c&apos;est que quelque chose a bougé hors de Terraform. Réconciliez de façon délibérée, soit en important et en codifiant le changement, soit en appliquant pour ramener le réel vers l&apos;état déclaré. Faire tourner la détection de dérive de façon planifiée, comme une étape de votre &lt;a href=&quot;/glossaire/ci-cd-pipeline/&quot;&gt;pipeline CI/CD&lt;/a&gt;, transforme une divergence silencieuse en alerte, plutôt qu&apos;en surprise lors du prochain déploiement sans rapport.&lt;/p&gt;
&lt;h2&gt;À retenir&lt;/h2&gt;
&lt;p&gt;Le state local est un défaut mono-joueur. Dès qu&apos;une seconde personne ou un pipeline touche la même infrastructure, il faut un backend distant pour une source de vérité partagée et un verrou pour sérialiser les écritures. Le verrouillage n&apos;est pas un obstacle à contourner : le flag &lt;code&gt;-lock=false&lt;/code&gt; et le &lt;code&gt;force-unlock&lt;/code&gt; réflexe sont la façon dont les équipes corrompent leur state. Et comme le state se croit seul acteur, planifiez &lt;code&gt;plan&lt;/code&gt; pour attraper la dérive avant qu&apos;elle ne vous attrape. C&apos;est un socle de l&apos;&lt;a href=&quot;/expertise/industrialisation-cicd/&quot;&gt;industrialisation de votre CI/CD&lt;/a&gt; posée sur de l&apos;&lt;a href=&quot;/glossaire/infrastructure-as-code/&quot;&gt;infrastructure as code&lt;/a&gt; qui tient sous charge.&lt;/p&gt;</content:encoded><category>Automatisation</category><category>terraform</category><category>state</category><category>backend-distant</category><category>iac</category></item><item><title>Terraform Remote State and Locking: How It Actually Works</title><link>https://jm-dev.it/blog/terraform-state-locking-remote-backend/</link><guid isPermaLink="true">https://jm-dev.it/blog/terraform-state-locking-remote-backend/</guid><description>Why local Terraform state breaks a team, how remote backends and state locking work on S3, GCS and HCP, and how to recover from a stale lock safely.</description><pubDate>Fri, 24 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&amp;lt;!-- lang=en justification: &quot;terraform state lock&quot;, &quot;error acquiring the state lock&quot; and &quot;remote backend s3 dynamodb&quot; are global technical queries typed almost entirely in English by engineers mid-apply. Consistent with publishing pointed reference tutorials in English.&lt;/p&gt;
&lt;p&gt;Content note (JM directive 2026-07-24): reference tutorial, generic and verifiable end to end against Terraform documentation and backend behavior. No production anecdotes by design, this is not a REX. --&amp;gt;&lt;/p&gt;
&lt;p&gt;Two engineers run &lt;code&gt;terraform apply&lt;/code&gt; on the same stack within the same minute. Both read the state, both plan against it, both write back. The second write silently overwrites the first, and now the state file describes infrastructure that no longer matches reality. Nobody gets an error. This is the failure mode local state was never built to prevent, and the reason remote backends exist.&lt;/p&gt;
&lt;h2&gt;Why local state does not survive a team&lt;/h2&gt;
&lt;p&gt;Terraform keeps a &lt;a href=&quot;/glossaire/infrastructure-as-code/&quot;&gt;state file&lt;/a&gt; that maps every resource in your configuration to a real object in your provider, plus metadata and dependency order. By default that file is &lt;code&gt;terraform.tfstate&lt;/code&gt;, sitting on the laptop of whoever ran the last apply.&lt;/p&gt;
&lt;p&gt;That default breaks the moment a second person joins:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It is not shared.&lt;/strong&gt; Your colleague&apos;s state does not know about the resources you created. Their next plan wants to recreate them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It has no lock.&lt;/strong&gt; Nothing stops two applies from running at once, and Terraform&apos;s read-modify-write cycle is not atomic across machines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It leaks secrets.&lt;/strong&gt; State stores resource attributes in plaintext, including generated passwords and keys, now scattered across laptops and possibly Git.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A remote backend fixes all three: one authoritative copy, an access-controlled store, and a locking mechanism.&lt;/p&gt;
&lt;h2&gt;What a remote backend gives you&lt;/h2&gt;
&lt;p&gt;A backend is declared in the &lt;code&gt;terraform&lt;/code&gt; block. The S3 backend is the common reference:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;terraform {
  backend &quot;s3&quot; {
    bucket       = &quot;acme-tfstate&quot;
    key          = &quot;platform/prod/terraform.tfstate&quot;
    region       = &quot;eu-west-1&quot;
    encrypt      = true
    use_lockfile = true   # native S3 locking, Terraform 1.10+
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;State now lives in S3, versioned and encrypted, and locking is handled by a lock file written next to it using S3 conditional writes. Historically that lock lived in a separate DynamoDB table (&lt;code&gt;dynamodb_table&lt;/code&gt; with a &lt;code&gt;LockID&lt;/code&gt; primary key); the DynamoDB approach still works, but native lock files removed the extra dependency for new stacks.&lt;/p&gt;
&lt;p&gt;Other backends bundle the same idea differently. The GCS backend has locking built in, using object generation numbers, no side table required. HCP Terraform (formerly Terraform Cloud) manages the state store and the lock for you, and adds run history and access policy on top.&lt;/p&gt;
&lt;h2&gt;How the lock actually works&lt;/h2&gt;
&lt;p&gt;Before any operation that could write state (&lt;code&gt;apply&lt;/code&gt;, and &lt;code&gt;plan&lt;/code&gt; when it refreshes), Terraform acquires a lock. The lock is a small record holding who took it, which operation, when, and a unique ID. While it is held, a second run refuses to start:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ terraform apply
╷
│ Error: Error acquiring the state lock
│
│ Lock Info:
│   ID:        a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
│   Operation: OperationTypeApply
│   Who:       alice@build-runner-07
│   Created:   2026-07-24 09:14:22 UTC
╵&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the system working correctly. The lock is released automatically when the operation finishes, whether it succeeds or fails cleanly.&lt;/p&gt;
&lt;h2&gt;The classic trap: the stale lock&lt;/h2&gt;
&lt;p&gt;The lock is only released cleanly if Terraform gets to release it. Kill the process with Ctrl-C twice, lose the network mid-apply, or have a CI runner evicted, and the lock stays held with no owner behind it. Every subsequent run then fails with the same &lt;code&gt;Error acquiring the state lock&lt;/code&gt;, blocking the whole team.&lt;/p&gt;
&lt;p&gt;The escape hatch is &lt;code&gt;force-unlock&lt;/code&gt;, using the ID from the error:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;terraform force-unlock a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Treat this as a loaded gun. Force-unlocking while another apply is genuinely still running reintroduces the exact concurrent-write corruption the lock existed to prevent. Confirm the owning process is truly dead (check the CI job, ask the person named in &lt;code&gt;Who&lt;/code&gt;) before you unlock. Never wire &lt;code&gt;force-unlock&lt;/code&gt; into automation, and never reach for &lt;code&gt;-lock=false&lt;/code&gt; to make the error go away, that disables locking for real applies.&lt;/p&gt;
&lt;h2&gt;Drift, and why plan is your detector&lt;/h2&gt;
&lt;p&gt;State assumes it is the only thing changing your infrastructure. When someone edits a resource by hand in a console, or another tool touches it, the real world diverges from what state records. That gap is &lt;strong&gt;drift&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;terraform plan&lt;/code&gt; is the detector: it refreshes state against the provider&apos;s real attributes, then compares config, state, and reality. A refresh-only run isolates pure drift from changes you actually intend:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;terraform plan -refresh-only&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If that shows differences with no config changes on your side, something outside Terraform moved. Reconcile deliberately, either by importing and codifying the change or by applying to pull reality back to the declared state. Running drift detection on a schedule, as a step in your &lt;a href=&quot;/glossaire/ci-cd-pipeline/&quot;&gt;CI/CD pipeline&lt;/a&gt;, turns silent divergence into an alert instead of a surprise during the next unrelated deploy.&lt;/p&gt;
&lt;h2&gt;What to remember&lt;/h2&gt;
&lt;p&gt;Local state is a single-player default. The moment a second person or a pipeline touches the same infrastructure, you need a remote backend for a shared source of truth and a lock to serialize writes. Locking is not an obstacle to route around: the &lt;code&gt;-lock=false&lt;/code&gt; flag and a reflexive &lt;code&gt;force-unlock&lt;/code&gt; are how teams corrupt state. And because state trusts that it is the only actor, schedule &lt;code&gt;plan&lt;/code&gt; to catch drift before it catches you. Getting this right is foundational to &lt;a href=&quot;/expertise/industrialisation-cicd/&quot;&gt;industrializing your CI/CD&lt;/a&gt; on top of infrastructure as code.&lt;/p&gt;</content:encoded><category>Automatisation</category><category>terraform</category><category>state</category><category>remote-backend</category><category>iac</category></item></channel></rss>