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]>
Vault
- Les playbooks ansible configurent la base de données postgres et le minimum requis pour permetre au dépot "tools" d'appliquer via un workflow gitea action une configuration vault via tofu.
- Configuration des backend d'authentification et des roles pour postgres et kubernetes. Définition de rôles "${app}-ops" pour permettre au dépot d'une application de définir ses propres dépendances dans vault. Rotation de credentials postgres pour les applications.
- Le dépot de l'application webapp gère l'obtention de ses crédentials pour postgres.
flowchart LR
classDef playbook stroke:#0f0,fill:#440,stroke-width: 1px
classDef tofu stroke:#f00,fill:#404,stroke-width: 2px
classDef argocd stroke:#00f,fill:#044,stroke-width: 3px
classDef database stroke:#bb0,fill:#ff0,stroke-width: 5px,color: black
classDef secret stroke:#f00,fill:#f00,stroke-width: 5px,color: yellow
classDef secretOperator stroke:#f00,fill:DarkRed,stroke-width: 5px,color: Orange
subgraph git_code[factory.git]
subgraph ansible_collection
setup_playbook[playbook arcodange.factory.setup]:::playbook
tools_playbook[playbook arcodange.factory.tools]:::playbook
end
git_code_tofu_vault{{tofu}}:::tofu
end
subgraph git_tools[tools.git]
argocd_tools{{Argo CD Apps}}:::argocd
git_tools_tofu_vault{{tofu}}:::tofu
end
subgraph git_webapp[webapp.git]
webapp["Go(lang) web app"]
argocd_webapp{{Argo CD App}}:::argocd
git_webapp_tofu_vault{{tofu}}:::tofu
end
subgraph servers
subgraph k3s
subgraph k3s_ns_tools[ns:tools]
argocd{{Argo CD}}:::argocd
pgbouncer
subgraph vault
subgraph vault_auth[auth]
subgraph vault_auth_openid[openid]
end
vault_auth_jwt[jwt]
vault_auth_k8s[kubernetes]
vault_auth_jwt_role_gitea_cicd[gitea_cicd role]
vault_auth_jwt_role_gitea_cicd_webapp_ops[gitea_cicd_webapp ops role]
vault_auth_k8s_role_vso[vault-secret-operator role]
vault_auth_k8s_role_webapp[webapp role]
subgraph policies
policy_default[default]
policy_webapp[webapp]
policy_webapp_ops[webapp ops]
policy_admin[admin]
policy_vso[edit-vso-client-cache]
end
end
subgraph vault_secrets[secrets]
subgraph kvv2
google/credentials
webapp/config
end
end
subgraph vault_postgres[postgres]
creds/creds-editor
creds/webapp
end
subgraph vault_transit[transit]
end
end
vault-secret-operator:::secretOperator
end
subgraph k3s_ns_webapp[ns:webapp]
webapp_deployment[deployment:webapp]
webapp_postgres_creds_secret[secret:postgres creds]:::secret
webapp_config_secret[secret:config]:::secret
webapp_service_account[sa:webapp]
end
end
subgraph postgres
root_credentials
postgres_db[(postgres)]:::database
webapp_credentials:::secret
webapp_db[(webpp)]:::database
vault_creds_editor_role{{credentials_editor}}
end
end
setup_playbook -. setup postgres .-> postgres
tools_playbook -.-o git_code_tofu_vault
git_code_tofu_vault -..-> vault_auth_openid
git_code_tofu_vault -..-> vault_auth_jwt -- tofu:factory --- vault_auth_jwt_role_gitea_cicd
git_code_tofu_vault -..-> kvv2
git_code_tofu_vault -..-> google/credentials
linkStyle 0,1 stroke:#ff3,stroke-width:1px,color:DarkKhaki;
linkStyle 2,3,5,6 stroke:#f3f,stroke-width:2px,color:DarkOrange;
git_tools -.-o argocd_tools
argocd_tools -.-> pgbouncer
argocd_tools -.-> vault
argocd_tools -.-> vault-secret-operator
argocd_tools o--o argocd
linkStyle 7,8,9,10,11 stroke:#3ff,stroke-width:3px,color:DarkSlateBlue;
git_tools_tofu_vault -..-> vault_auth_k8s -- sa:vso --- vault_auth_k8s_role_vso
git_tools_tofu_vault -..-> webapp/config
git_tools_tofu_vault -..-> vault_transit
git_tools_tofu_vault -..-> vault_postgres
vault_auth_k8s ---> k3s
vault_postgres --> pgbouncer x==> postgres; webapp_deployment --> pgbouncer
linkStyle 12,14,15,16 stroke:#f3f,stroke-width:2px,color:DarkOrange;
linkStyle 18,19,20 stroke:gold,stroke-width:2px;
vault_transit x---x vault-secret-operator
vault-secret-operator x---x vault_auth_k8s_role_vso
vault_auth_jwt_role_gitea_cicd x--x policy_default
vault_auth_k8s_role_vso x--x policy_vso
creds/webapp -.-> webapp_credentials
creds/webapp -.-> vault-secret-operator
vault-secret-operator -.-> webapp_postgres_creds_secret
webapp/config -.-> vault-secret-operator
vault-secret-operator -.-> webapp_config_secret
argocd_webapp -.-> k3s_ns_webapp
webapp --o webapp_deployment
webapp_postgres_creds_secret --o webapp_deployment
webapp_deployment --> webapp_service_account
vault_auth_jwt -- tofu:tools --- vault_auth_jwt_role_gitea_cicd_webapp_ops
vault_auth_jwt_role_gitea_cicd_webapp_ops x--x policy_webapp_ops
vault_auth_k8s -- sa:webapp --- vault_auth_k8s_role_webapp x-- tofu:webapp --x policy_webapp
git_webapp_tofu_vault -.-> vault_auth_k8s_role_webapp
git_webapp_tofu_vault -.-> creds/webapp
root_credentials x--x postgres_db
webapp_credentials x--x webapp_db
tools_playbook --> vault_creds_editor_role
vault_creds_editor_role -. change password .-> webapp_credentials
vault_postgres x--x vault_creds_editor_role