Vault est resté scellé onze jours en silence, et c'était le pod le plus faible du cluster
Helm Charts / Detect changed charts (pull_request) Successful in 1m27s
Helm Charts / Library charts tool (pull_request) Skipped
Helm Charts / Application charts alloy (pull_request) Skipped
Helm Charts / Application charts chart (pull_request) Skipped
Helm Charts / Application charts crowdsec (pull_request) Skipped
Helm Charts / Application charts grafana (pull_request) Skipped
Helm Charts / Application charts loki (pull_request) Skipped
Helm Charts / Application charts minio (pull_request) Skipped
Helm Charts / Application charts pgbouncer (pull_request) Skipped
Helm Charts / Application charts pgcat (pull_request) Skipped
Helm Charts / Application charts redis (pull_request) Skipped
Helm Charts / Application charts hashicorp-vault (pull_request) Successful in 18s
Helm Charts / Application charts prometheus (pull_request) Successful in 20s
Helm Charts / Detect changed charts (pull_request) Successful in 1m27s
Helm Charts / Library charts tool (pull_request) Skipped
Helm Charts / Application charts alloy (pull_request) Skipped
Helm Charts / Application charts chart (pull_request) Skipped
Helm Charts / Application charts crowdsec (pull_request) Skipped
Helm Charts / Application charts grafana (pull_request) Skipped
Helm Charts / Application charts loki (pull_request) Skipped
Helm Charts / Application charts minio (pull_request) Skipped
Helm Charts / Application charts pgbouncer (pull_request) Skipped
Helm Charts / Application charts pgcat (pull_request) Skipped
Helm Charts / Application charts redis (pull_request) Skipped
Helm Charts / Application charts hashicorp-vault (pull_request) Successful in 18s
Helm Charts / Application charts prometheus (pull_request) Successful in 20s
INCIDENT
Le 30/08 à 11:35, pi3 passe de 3,6 Gio de mémoire disponible à 1,7 en cinq
minutes. À 11:45 node-exporter ne répond plus, à 11:48 le nœud est NotReady, et
300 s plus tard — la tolérance `node.kubernetes.io/unreachable:NoExecute` — le
node-lifecycle controller supprime quatre pods, dont hashicorp-vault-0.
Vault repart SCELLÉ, comme le prévoit Shamir 1/1 sans auto-unseal. Personne ne
le redescelle. Pendant onze jours le VSO répond 503 « Vault is sealed » et plus
aucun Secret n'est réconcilié — découvert par hasard, en enquêtant sur autre
chose.
Ce qui a fonctionné ce jour-là : `NoeudInjoignable` a tiré à 11:46. L'infra a
parlé. Ce qui a manqué : la CONSÉQUENCE. Le nœud est revenu, les pods ont été
recréés, l'alerte s'est éteinte, et personne n'a su que Vault, lui, resterait
scellé.
Ce n'était ni un crash ni un OOMKill : `lastState: {}` et `restartCount: 0` sur
un pod vieux de 11 jours. Et le StatefulSet n'a aucune livenessProbe — seulement
une readiness `exec: vault status`, dont l'échec ne redémarre rien. D'où 196 906
events Unhealthy pour zéro redémarrage.
1. L'ALERTE QUI MANQUAIT — VaultIndisponible
`kube_pod_status_ready{namespace="tools", pod=~"hashicorp-vault-[0-9]+"} == 0`
pendant 10 min, avec `or absent(...)` pour couvrir la disparition du pod.
Pourquoi kube-state-metrics et pas `vault_core_unsealed` : Vault n'expose ses
métriques que via /v1/sys/metrics, qui exige un token ou l'ouverture d'un
endpoint non authentifié. Or la readinessProbe du chart est littéralement
`vault status` : un Vault scellé est NotReady. kube-state-metrics est déjà
scrapé, ça ne coûte rien et ça ne touche pas à la configuration de Vault.
⚠ Le sélecteur est ancré sur `[0-9]+`, et c'est une correction, pas un détail :
ma première version utilisait `hashicorp-vault-.*`, qui attrape aussi les pods
du Vault Secrets Operator — dont un exemplaire terminé traîne en permanence. La
règle tirait donc en continu. Vérifié contre le Prometheus du cluster : avec
`[0-9]+` elle ne rend que hashicorp-vault-0 et ne tire pas.
2. VAULT N'EST PLUS LE POD LE PLUS FAIBLE DU CLUSTER
Mesuré avant : `priorityClassName=""`, `priority=0`, QoS `BestEffort`. Dernier
servi par le scheduler, premier évincé par le kubelet. Sur 114 pods, 78 sont
BestEffort — Vault était noyé dedans, alors que tout le cluster dépend de lui
et qu'il exige une intervention humaine pour revenir.
- PriorityClass `vault-critical` à 900000000. Sous `longhorn-critical`
(1000000000) parce que Vault stocke sur un PVC : le stockage doit survivre à
Vault. Au-dessus de tout le reste.
- `requests: {memory: 512Mi, cpu: 100m}`. Vault consomme 175 Mio : très en
dessous de sa requête, donc tout en bas de la liste d'éviction.
AUCUNE LIMITE, DÉLIBÉRÉMENT. Une limite trop basse déclenche un OOMKill, et un
OOMKill sur ce pod coûte un descellement manuel. Or je n'ai pas pu établir de
pic mémoire fiable : les séries cAdvisor de ce cluster rendent huit valeurs
contradictoires pour ce pod, jusqu'à 2,3 Gio, ce qui est invraisemblable pour un
Vault en storage "file". On ne pose pas une limite sur un chiffre auquel on ne
croit pas.
3. LE PDB EXISTANT : GARDÉ, MAIS SON PIÈGE EST MAINTENANT ÉCRIT
`maxUnavailable: 0` sur un StatefulSet à un réplica donne ALLOWED DISRUPTIONS: 0
en permanence. Un `kubectl drain` du nœud qui héberge Vault ne se terminera donc
jamais. C'est le comportement voulu — il force à traiter Vault consciemment —
mais la sortie de secours (`--disable-eviction`, ou suppression manuelle du pod)
n'était écrite nulle part. Elle l'est.
CE QUE ÇA NE RÉSOUT PAS, ET IL FAUT LE DIRE
Ni la priorité ni les requests n'empêchent la suppression par le node-lifecycle
controller quand un nœud devient injoignable — c'est exactement ce qui est
arrivé le 30/08, et aucun réglage de pod ne l'évite. Ce cas-là est traité par
l'alerte, pas par la protection.
L'auto-unseal reste écarté : décision assumée, documentée dans factory
vibe/guidebooks/lab-ecosystem/secrets-and-vault.md. Le descellement restera
manuel ; c'est le délai de détection qui passe de onze jours à dix minutes.
⚠ APPLICATION : le StatefulSet est en `updateStrategy: OnDelete`. Les réglages
du point 2 ne prendront effet qu'à la prochaine suppression du pod — laquelle
rescellera Vault. À faire au moment choisi, clé sous la main.
Vérifié : `helm template` rend la PriorityClass, le PDB et un StatefulSet
portant `priorityClassName: vault-critical` et les requests ; `helm lint` passe ;
la requête de l'alerte testée contre le Prometheus du cluster.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
{{/*
|
||||
F—: VAULT NE DOIT PLUS ÊTRE LE POD LE PLUS FAIBLE DU CLUSTER.
|
||||
|
||||
Constat du 2026-09-11, mesuré : `priorityClassName=""`, `priority=0`, QoS `BestEffort`.
|
||||
Autrement dit Vault était littéralement dernier servi et premier évincé, alors que le
|
||||
Vault Secrets Operator en dépend pour TOUS les Secrets du cluster. Quand il tombe, il
|
||||
repart scellé (Shamir 1/1, pas d'auto-unseal — décision assumée) et plus rien ne se
|
||||
réconcilie jusqu'à ce qu'un humain vienne le desceller.
|
||||
|
||||
POURQUOI 900000000. Il existe déjà `longhorn-critical` à 1000000000. Vault stocke ses
|
||||
données sur un PVC : si Longhorn tombe, Vault ne repart pas. Le stockage doit donc rester
|
||||
AU-DESSUS. En dessous de Vault, tout le reste — applications comprises.
|
||||
|
||||
CE QUE ÇA ACHÈTE, ET CE QUE ÇA N'ACHÈTE PAS. La priorité joue sur la préemption par le
|
||||
scheduler et sert de critère de tri à l'éviction par le kubelet sous pression. Elle
|
||||
n'empêche PAS la suppression décidée par le node-lifecycle controller quand un nœud
|
||||
devient injoignable — c'est exactement ce qui s'est produit le 30/08 et aucun réglage de
|
||||
pod ne l'évite. Ce qui traite ce cas-là, c'est l'alerte VaultIndisponible.
|
||||
*/}}
|
||||
apiVersion: scheduling.k8s.io/v1
|
||||
kind: PriorityClass
|
||||
metadata:
|
||||
name: vault-critical
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/part-of: hashicorp-vault
|
||||
value: 900000000
|
||||
globalDefault: false
|
||||
preemptionPolicy: PreemptLowerPriority
|
||||
description: >-
|
||||
Vault et son opérateur : tout le cluster en dépend pour ses Secrets, et un Vault tombé
|
||||
repart scellé, donc muet jusqu'à intervention humaine. Sous longhorn-critical (le
|
||||
stockage doit survivre à Vault), au-dessus de tout le reste.
|
||||
@@ -8,6 +8,17 @@
|
||||
# d'une panne de nœud ni d'une suppression manuelle. Avec un seul réplica et
|
||||
# aucun auto-unseal, toute interruption exige un unseal humain ensuite ; ce PDB
|
||||
# réduit seulement les interruptions évitables.
|
||||
#
|
||||
# ⚠ CONSÉQUENCE À CONNAÎTRE AVANT LA PROCHAINE MAINTENANCE D'UN NŒUD.
|
||||
# `maxUnavailable: 0` sur un StatefulSet à UN réplica donne `ALLOWED DISRUPTIONS: 0`
|
||||
# en permanence — vérifié : `kubectl get pdb -n tools` le montre depuis 20 jours.
|
||||
# Un `kubectl drain <nœud>` sur le nœud qui héberge Vault ne se terminera donc JAMAIS,
|
||||
# il bouclera en silence sur « cannot evict pod as it would violate the budget ».
|
||||
# C'est le comportement voulu — il force à traiter Vault consciemment — mais il faut
|
||||
# savoir en sortir :
|
||||
# kubectl drain <nœud> --disable-eviction # contourne le PDB (supprime le pod)
|
||||
# ou kubectl delete pod -n tools hashicorp-vault-0 # puis descellement à la main
|
||||
# Dans les deux cas Vault repart scellé : avoir la clé sous la main AVANT de drainer.
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
|
||||
@@ -6,6 +6,36 @@ vault: &vault_config
|
||||
server:
|
||||
enabled: true
|
||||
logLevel: trace
|
||||
# ---- NE PLUS ÊTRE LE POD LE PLUS FAIBLE DU CLUSTER -----------------------------
|
||||
# Mesuré le 2026-09-11, avant ce changement : `priorityClassName=""`, `priority=0`,
|
||||
# QoS `BestEffort`. Vault était donc dernier servi par le scheduler et PREMIER évincé
|
||||
# par le kubelet sous pression mémoire — alors que le Vault Secrets Operator en dépend
|
||||
# pour tous les Secrets du cluster, et qu'un Vault tombé repart SCELLÉ, donc muet
|
||||
# jusqu'à ce qu'un humain vienne le desceller à la main.
|
||||
#
|
||||
# Contexte : 78 pods du cluster sont BestEffort contre 35 Burstable et 1 Guaranteed.
|
||||
# Vault était noyé dans le premier groupe.
|
||||
priorityClassName: vault-critical # cf. templates/priorityclass.yaml
|
||||
resources:
|
||||
# DES REQUESTS, ET DÉLIBÉRÉMENT AUCUNE LIMITE.
|
||||
#
|
||||
# Ce sont les `requests` qui protègent : le kubelet évince d'abord les BestEffort,
|
||||
# puis les Burstable qui DÉPASSENT leur requête, et en dernier ceux qui restent
|
||||
# dessous. Vault consomme ~175 Mio (kubectl top, 11/09) : avec 512 Mio demandés il
|
||||
# est très en dessous, donc tout en bas de la liste des candidats à l'éviction.
|
||||
#
|
||||
# Pourquoi pas de `limits.memory`, qui donnerait du Guaranteed : une limite trop
|
||||
# basse déclenche un OOMKill, et un OOMKill sur CE pod coûte un descellement manuel.
|
||||
# Or je n'ai pas pu établir de pic de mémoire fiable — les séries cAdvisor de ce
|
||||
# cluster rendent huit valeurs contradictoires pour ce pod (jusqu'à 2,3 Gio, ce qui
|
||||
# est invraisemblable pour un Vault en storage "file"). On ne pose pas une limite sur
|
||||
# un chiffre auquel on ne croit pas : le gain marginal ne vaut pas le risque.
|
||||
#
|
||||
# Pourquoi pas de `limits.cpu` : le throttling CPU sur un ARM64 à 4 cœurs ralentirait
|
||||
# les opérations de descellement et de scellement, pour un pod qui consomme 26 m.
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 100m
|
||||
auditStorage:
|
||||
enabled: true
|
||||
|
||||
|
||||
Reference in New Issue
Block a user