Sprint 4 â ObservabilitĂ© (Day-2 operations)
Incidents du Sprint 4 : GitOps ArgoCD, observabilité, densité de pods.
INC-053 : "Too many pods" â densitĂ© limitĂ©e par le VPC CNI (t3.medium)
Contexte : Installation d'ArgoCD (#72), 7 pods ajoutés sur le node unique.
SymptÎme : 4 pods ArgoCD bloqués en Pending, FailedScheduling: 0/1 nodes are available: 1 Too many pods. Ce n'est pas un manque de CPU/mémoire (mesurés à 34 %).
Cause : Le VPC CNI d'AWS alloue une IP secondaire d'ENI par pod. Le max-pods est donc plafonnĂ© par le rĂ©seau, pas par le CPU/RAM. Sur t3.medium : 3 ENI Ă 6 IP â 1 = max-pods 17, dĂ©jĂ atteint par les pods systĂšme + opĂ©rateurs (Envoy, cert-manager, ESO, ExternalDNS, metrics-server) + fastapi.
Fix : Prefix Delegation du VPC CNI (ENABLE_PREFIX_DELEGATION=true, chaque ENI alloue des /28) couplĂ©e Ă un launch template nodeadm AL2023 forçant maxPods: 110. RĂ©sultat : max-pods 17 â 110. DĂ©tails et alternatives Ă©cartĂ©es : ADR 012.
PiÚge d'ordre rencontré : aprÚs le 1er apply, les pods restaient en ContainerCreating avec aws-cni ... failed to assign an IP address. Cause : l'addon CNI avait depends_on = [node_group], donc le node a booté ses ENI en mode IP classique avant que la Prefix Delegation soit active ; l'activer aprÚs coup ne reconfigure pas rétroactivement les ENI attachées (maxPods=110 mais le CNI ne fournit que ~17 IP). Déblocage immédiat : recycler le node (terminer l'instance, le node group en relance une qui boote en mode prefix). Fix durable : inverser la dépendance Terraform (node group depends_on aws_eks_addon.vpc_cni) pour que l'addon précÚde le boot des nodes au from-scratch. Validé le 2026-06-12 (aws-start from-scratch) : node Ready avec max-pods=110 dÚs le boot, aucun pod bloqué, aucun recyclage manuel.
Leçon : Sur EKS, max-pods est une limite rĂ©seau (ENI/IP), pas de ressources. Deux piĂšges en cascade : (1) un managed node group simple garde max-pods bas mĂȘme avec la Prefix Delegation activĂ©e â launch template avec override maxPods obligatoire ; (2) l'addon CNI doit ĂȘtre configurĂ© avant le boot des nodes, sinon ils restent en mode IP classique et il faut les recycler. Ă anticiper avant toute montĂ©e en densitĂ© (observabilitĂ© #74/#75). Solution AWS-native ici, Ă terme remplacĂ©e par Cilium (overlay = plus de contrainte d'IP par ENI). â fait en #80 / ADR 019 : la Prefix Delegation est retirĂ©e, l'overlay Cilium affranchit la densitĂ© du plafond ENI.
INC-054 : flapping de pods â ArgoCD selfHeal vs HPA sur spec.replicas
Contexte : Validation from-scratch du déploiement GitOps (#72) aprÚs l'infra-up. Le Deployment fastapi est piloté par un HPA (minReplicas: 1, maxReplicas: 5) et déployé par ArgoCD avec selfHeal: true.
SymptÎme : Pods fastapi créés puis détruits en boucle. Le pod le plus ancien reste Running, les pods récents passent en Error (terminés en plein scale-down). L'Application ArgoCD fastapi oscille entre Synced et Progressing, sans jamais se stabiliser en Healthy.
Cause : Conflit de propriĂ©tĂ© sur spec.replicas. Le manifeste Git (k8s/base/deployment.yaml) fixait replicas: 2. Ă charge faible, le HPA descend le Deployment Ă replicas: 1. ArgoCD compare alors cluster (1) â Git (2), juge le Deployment OutOfSync, et selfHeal réécrit replicas: 2. Le HPA retue aussitĂŽt le pod surnumĂ©raire. Retour Ă 1, ArgoCD remet 2⊠boucle infinie. Deux contrĂŽleurs se disputent le mĂȘme champ.
Fix : Retirer spec.replicas du manifeste (quand un HPA pilote le nombre de pods, le manifeste ne doit pas le fixer ; le HPA part de minReplicas) et ajouter un ignoreDifferences sur /spec/replicas dans l'Application ArgoCD (robustesse : si le champ réapparaissait, ArgoCD ne le réconcilierait pas). Best practice officielle ArgoCD (« ArgoCD fights with HPA »).
Leçon : En GitOps, un champ ne peut avoir qu'une seule source de vĂ©ritĂ©. Si un contrĂŽleur dynamique (HPA, VPA, certains opĂ©rateurs) gĂšre un champ, il faut le retirer de Git et dire Ă ArgoCD de l'ignorer, sinon selfHeal lui fait la guerre. SymptĂŽme Ă reconnaĂźtre : une ressource qui ne se stabilise jamais en Healthy + un churn de pods sans cause applicative. Le rĂ©flexe : chercher les champs partagĂ©s entre Git et un contrĂŽleur (replicas, annotations injectĂ©esâŠ).
Effet de bord (kube-linter / PDB), corrigĂ© via #83 : retirer replicas du Deployment a dĂ©clenchĂ© le check kube-linter pdb-min-available. En analyse statique, kube-linter ne lit pas le HPA : il suppose replicas: 1 (dĂ©faut) et le compare au PDB minAvailable: 1, concluant que le PDB bloquerait tout drain. CorrigĂ© en passant le PDB en maxUnavailable: 1 (s'exprime relativement au nombre courant de pods, pas de blocage Ă 1 pod) et le HPA en minReplicas: 2 (toujours â„2 pods â zĂ©ro downtime sur rolling update et drain de node). Trou de process dĂ©couvert : la MR #82 a Ă©tĂ© mergĂ©e alors que le ci_config_path Ă©tait sur .gitlab-ci-infra.yml, donc le job kube-linter-scan (dĂ©fini dans le pipeline app) ne s'est jamais exĂ©cutĂ© sur le changement de manifeste. Une MR qui touche k8s/ doit tourner sous le config path app, sinon les lints K8s sont contournĂ©s silencieusement.
INC-055 : loki-0 CrashLoopBackOff â /var/loki en read-only filesystem
Contexte : Premier déploiement de la stack logs (#75) sur le cluster via ArgoCD. Loki en mode SingleBinary, persistence: false (cluster éphémÚre, choix no-PVC de l'ADR 015).
SymptÎme : loki-0 en CrashLoopBackOff (1/2 Ready, redémarrages répétés). Log : error running loki ... err="mkdir /var/loki: read-only file system\nerror initialising module: ruler-storage".
Cause : En SingleBinary sans persistence, le chart grafana/loki ne monte aucun volume sur le data path /var/loki (le PVC storage n'existe que si persistence.enabled: true). Le conteneur loki ayant readOnlyRootFilesystem: true, /var/loki retombe sur le rootfs en lecture seule â Loki ne peut pas crĂ©er ses rĂ©pertoires de donnĂ©es (chunks, rules, wal, compactor) et plante au dĂ©marrage.
Fix : Monter un emptyDir inscriptible sur /var/loki via singleBinary.extraVolumes + extraVolumeMounts (Ă©phĂ©mĂšre, cohĂ©rent avec le choix no-persistence : les logs meurent au teardown). On garde readOnlyRootFilesystem: true (durcissement prĂ©servĂ©), seul le data path devient inscriptible. ValidĂ© par helm template (volume data â /var/loki prĂ©sent sur le conteneur).
Leçon : helm template valide le manifeste rendu, pas le comportement runtime. Le rendu Ă©tait parfait (StatefulSet correct), mais le crash ne se voit qu'au dĂ©marrage rĂ©el du process (Ă©criture sur un fs read-only). MĂȘme classe que INC-045/046 (« rendu/configurĂ© statiquement â fonctionne au runtime »). RĂ©flexe pour tout composant stateful dĂ©ployĂ© sans PVC : vĂ©rifier qu'un volume inscriptible (emptyDir) est bien montĂ© sur son data path, surtout quand le conteneur a readOnlyRootFilesystem: true.
INC-056 : Grafana OOMKilled au dĂ©marrage â limite mĂ©moire trop basse
Contexte : Session de validation live from-scratch (2026-06-14, #76/#77/#70). Stack observabilité posée par ArgoCD. Grafana configuré avec limits.memory: 200Mi (values #74, dimensionnement volontairement serré pour le mono-node éphémÚre).
SymptÎme : kube-prometheus-stack-grafana en CrashLoopBackOff. Conteneur grafana OOMKilled (Exit Code 137, Restart Count 3), readiness puis liveness en échec (connection refused au boot, puis context deadline exceeded). Un log d'erreur trompeur Failed to install plugin pluginId=elasticsearch ... unlinkat ...: permission denied a d'abord orienté le diagnostic vers un problÚme de plugin/permissions (red herring).
Cause : La limite mĂ©moire de Grafana (200Mi) est trop basse pour Grafana 11+. En rĂ©gime stable il tourne Ă ~161Mi, mais le pic au dĂ©marrage (migrations DB + gestion des plugins bundled) dĂ©passe 200Mi â le kernel tue le conteneur (OOMKill, 137) avant qu'il devienne Ready â CrashLoop. Le node Ă©tait Ă 82% mais n'est pas en cause : l'OOM est au niveau conteneur (limite), pas node. L'erreur de plugin est non fatale.
Fix : Remonter grafana.resources : limits.memory 200Mi â 768Mi (plafond anti-OOM large, absorbe le pic de boot), requests.memory 100Mi â 128Mi (modĂ©rĂ©, pas de sur-rĂ©servation sur le node). DĂ©ployĂ© en GitOps (MR values â merge develop â ArgoCD resync), mĂȘme pattern que INC-055.
Leçon : La limite mĂ©moire â consommation. Une limite trop serrĂ©e tue un process sur un pic transitoire (boot, migration) mĂȘme si le rĂ©gime stable est largement sous la limite. Dimensionner la limite sur le pic, pas sur le rĂ©gime, pour les composants qui chargent au dĂ©marrage (Grafana, JVM...). Monter une limite ne coĂ»te rien au node en rĂ©gime stable (seuls l'usage rĂ©el + les requests pĂšsent), donc autant la garder large. Et lire le bon signal : Exit Code 137 = OOMKill, c'Ă©tait le vrai tueur ; le log d'erreur plugin Ă©tait un red herring qui a fait perdre du temps au dĂ©but (mĂȘme rĂ©flexe que INC-044 : i/o timeout = rĂ©seau, 401 = auth, 137 = mĂ©moire).
Validation alerting bout-en-bout + 2 limites détectées (2026-06-14)
Contexte : Session de test des alertes en conditions rĂ©elles (infra up), doublĂ©e d'une prise en main de Grafana. But : dĂ©clencher de vraies alertes FastAPI (#77) et observer toute la chaĂźne Prometheus â Alertmanager â Slack.
Ce qui a été validé (live) :
- Cycle de vie complet d'une alerte : Normal â Pending â Firing â Resolved, et le rĂŽle du for: (dĂ©lai anti-faux-positif) observĂ© en direct.
- FastAPIPodCrashLooping ET FastAPITargetDown se déclenchent sur un CrashLoop provoqué, routées vers Slack par sévérité (critical), puis [RESOLVED] au rétablissement.
- Diagnostic en direct via Grafana Explore (PromQL : up{...}, increase(kube_pod_container_status_restarts_total[10m])).
Limite 1 â up == 0 ne couvre pas la disparition totale de la cible. Au premier essai (scale Ă 0 du deployment), FastAPITargetDown ne s'est pas dĂ©clenchĂ©e : sans pod, il n'y a plus d'endpoint, donc plus de cible, donc la sĂ©rie up disparaĂźt (elle ne vaut pas 0, elle n'existe plus). up == 0 n'a alors rien Ă Ă©valuer. La mĂȘme rĂšgle se dĂ©clenche en revanche trĂšs bien quand le pod existe mais plante (CrashLoop : cible prĂ©sente, scrape KO â up = 0). Distinction « sĂ©rie absente » vs « sĂ©rie Ă 0 ». â CorrigĂ© (#93) : rĂšgle FastAPITargetMissing (absent(up{namespace="fastapi", endpoint="http"}), severity: critical, for: 2m) ajoutĂ©e en complĂ©ment de FastAPITargetDown. Les deux couvrent les deux faces de la panne (cible disparue vs cible prĂ©sente qui ne rĂ©pond pas). ValidĂ© live le 2026-06-16 : scale-Ă -0 rĂ©el du deployment â la sĂ©rie up disparaĂźt de Prometheus â FastAPITargetMissing (critical) tire dans Slack aprĂšs for: 2m, puis [RESOLVED] au retour des pods. La preuve par le comportement, pas par le YAML.
Limite 2 â InfoInhibitor remonte dans Slack. Lors du test live, l'alerte systĂšme InfoInhibitor (rouage interne du mĂ©canisme d'inhibition) est arrivĂ©e dans Slack. Elle devrait ĂȘtre routĂ©e vers null comme le Watchdog. â CorrigĂ© (#94) : matcher alertname = "InfoInhibitor" â receiver null ajoutĂ© dans le routing Alertmanager, comme le Watchdog. ValidĂ© live le 2026-06-16 : pendant le test #93, InfoInhibitor n'est pas remontĂ© dans Slack (seul FastAPITargetMissing y est arrivĂ©).
Bonus ArgoCD (vĂ©cu pendant la manip) : pour provoquer un CrashLoop durable il a fallu couper le selfHeal. Deux enseignements confirmĂ©s en test : (1) cascade app-of-apps â couper le selfHeal de fastapi ne suffit pas, le root-app apps le rĂ©pare ; il faut museler le parent d'abord ; (2) ArgoCD est additif â un champ ajoutĂ© hors-bande (command: injectĂ© par kubectl patch, absent du Git) n'est pas vu comme un drift : l'Application reste Synced et le selfHeal ne le retire pas. C'est voulu (coexistence avec HPA/webhooks/opĂ©rateurs, cf. INC-054). Retrait du champ â manuel (ou Replace=true).
Leçon : valider une alerte, ce n'est pas vĂ©rifier qu'elle est Ă©crite, c'est la dĂ©clencher pour de vrai. Le test live a rĂ©vĂ©lĂ© un angle mort (up == 0) qu'aucune relecture du YAML n'aurait montrĂ©. MĂȘme principe que pour les manifests (INC-055 : helm template valide le rendu, pas le runtime) : la preuve, c'est le comportement, pas l'intention.
#81 â /metrics retirĂ© de l'exposition publique (2026-06-15)
Contexte : depuis #73 l'app expose /metrics (Prometheus). BranchĂ© sur l'app principale, il Ă©tait routĂ© publiquement par effet de bord (la rule HTTPRoute PathPrefix: / attrape tout). Or /metrics est scrapĂ© en interne par Prometheus (ServiceMonitor â ClusterIP, #74), jamais via le Gateway, et exposĂ© publiquement il divulgue routes/latences/codes d'erreur (reconnaissance).
Correctif (ADR 018) : rule HTTPRoute dĂ©diĂ©e PathPrefix: /metrics â HTTPRouteFilter directResponse 404 (CRD Envoy Gateway), rĂ©fĂ©rencĂ© via ExtensionRef. Gateway API choisit le match le plus spĂ©cifique, donc /metrics tombe sur le 404 sans atteindre le backend ; la rule / reste pour le reste du trafic. 404 et non 403 : ne divulgue pas l'existence de l'endpoint. Aucune modif de l'app ni du scrape interne (le ServiceMonitor tape le ClusterIP, hors Gateway).
ValidĂ© hors infra : kustomize build k8s/base (exit 0), refs HTTPRoute â HTTPRouteFilter cohĂ©rentes, mkdocs build. Comme #70, pas d'outil hors cluster pour le CRD HTTPRouteFilter.
ValidĂ© live (2026-06-16) : curl https://api.devopsyouss.com/metrics â 404, et target fastapi toujours UP dans Prometheus (up{namespace="fastapi", endpoint="http"} == 1, scrape interne ClusterIP intact). â
INC-057 : kube-linter-scan rouge sur un download latest flaky (2026-06-15)
SymptĂŽme : le pipeline develop passe rouge sur kube-linter-scan, stages suivants (build, promote, pages) skipped, sans aucun changement de nos manifests. Log : gzip: invalid magic / tar: Child returned status 1 Ă l'extraction de kube-linter.tar.gz.
Cause : le job téléchargeait le binaire via curl -sSL .../releases/latest/download/kube-linter-linux.tar.gz. Deux défauts cumulés :
curlsans-f: sur une rĂ©ponse HTTP d'erreur transitoire (hoquet GitHub/rĂ©seau), curl ne sort pas en erreur, il sauve le corps (HTML d'erreur) dans le.tar.gz.tarplante ensuite sur un fichier qui n'est pas un gzip â message obscur, vraie cause masquĂ©e (cf. INC-044 « lire le bon signal »).latestnon pinnĂ© : non reproductible. Un bump de kube-linter peut introduire de nouvelles rĂšgles qui rendraientdeveloprouge sans changement de notre part.
Correctif (#92, ADR 005 §8) :
- Version pinnée
KUBE_LINTER_VERSION: v0.8.3(= ce quelatestservait dĂ©jĂ , version en usage depuis #83 â aucun changement de comportement). URLreleases/download/${KUBE_LINTER_VERSION}/.... - curl durci :
curl -fsSL --retry 3 --retry-delay 2 --retry-connrefused. Le-ffait échouer proprement sur une erreur HTTP ;--retryencaisse le hoquet transitoire (la cause vécue).
DĂ©blocage immĂ©diat : retry du job kube-linter-scan (l'URL refonctionnait), le pipeline a repris build â promote â pages.
Leçon : un outil téléchargé en latest au runtime est une dépendance externe non pinnée, exactement ce que combat l'ADR 005 §8. Tout curl qui alimente un tar/install doit porter -f (échouer sur erreur HTTP) et --retry (tolérer le transitoire).
#95 â patch de 4 CVE HIGH Python + arbitrage starlette/instrumentator (2026-06-16)
SymptĂŽme : trivy-image-scan rouge sur la branche de !168 (alerting), sans rapport avec son contenu. La base de vulnĂ©rabilitĂ©s Trivy s'Ă©tait enrichie depuis le dernier build et rĂ©vĂ©lait 4 CVE HIGH fixables dans des dĂ©pendances Python (mĂȘme mĂ©canique que #71 openssl) :
| DĂ©pendance | Type | InstallĂ© â Fix | CVE |
|---|---|---|---|
| PyJWT | directe | 2.12.1 â 2.13.0 | CVE-2026-48526 (auth bypass JWT forgĂ©) |
| python-multipart | directe | 0.0.28 â 0.0.32 | CVE-2026-53539 (DoS querystring) |
| starlette | transitive (via fastapi) | 1.0.0 â 1.3.1 | CVE-2026-48818 (SSRF/NTLM StaticFiles), CVE-2026-54283 (DoS form) |
Le piÚge : PyJWT et python-multipart se patchent seuls (deps directes). Mais starlette 1.3.1 exige fastapi 0.137, et fastapi 0.137 + starlette 1.3 cassent la derniÚre version de prometheus-fastapi-instrumentator (8.0.0) : '_IncludedRouter' object has no attribute 'path' (starlette 1.3 a introduit un type de route que l'instrumentator ne sait pas parser). Bumper starlette = perdre /metrics (#73, milestone Observabilité). Aucune version plus récente de l'instrumentator n'existe.
Décision (patch partiel + ignore justifié) :
- Patcher PyJWT (la plus sérieuse : l'app fait de l'auth JWT) et python-multipart, via
pip-compile --upgrade-packageciblĂ© (diff de 2 lignes, fastapi/starlette intacts â instrumentator prĂ©servĂ©). - Accepter temporairement les 2 CVE starlette dans
.trivyignore, aprÚs analyse d'exploitabilité : - CVE-2026-48818 : non exploitable ici (l'app n'utilise ni
StaticFilesnimount()). - CVE-2026-54283 : exposition limitée au
/login(OAuth2PasswordRequestForm), DoS CPU uniquement, pas de fuite/RCE. - Tracer la dette : issue #96 (bump starlette quand l'instrumentator supporte starlette ℠1.3, ou remplacement de la lib de métriques) + retrait des 2 entrées
.trivyignoreĂ ce moment.
Validation (offline, mĂ©thode projet) : build + Trivy local (--ignore-unfixed --ignorefile) â 0 CVE fixable ; 39 tests verts (fastapi/starlette inchangĂ©s â instrumentator OK) ; pip check propre.
Leçon : un correctif de sĂ©curitĂ© peut ĂȘtre bloquĂ© par une incompatibilitĂ© transitive (ici une lib d'observabilitĂ© non maintenue au rythme de starlette). PlutĂŽt qu'un .trivyignore aveugle, la bonne dĂ©marche est : (1) isoler ce qui se patche sans casser (PyJWT, python-multipart), (2) analyser l'exploitabilitĂ© rĂ©elle du reste (le code path existe-t-il ?), (3) accepter le risque rĂ©siduel de façon documentĂ©e et tracĂ©e (justification par CVE + issue de revue). Un HIGH « fixable » ignorĂ© n'est dĂ©fendable qu'avec cette analyse, pas par dĂ©faut.
#68 â SBOM CycloneDX (2026-06-16)
Quoi : génération d'un SBOM CycloneDX de l'image à chaque build, intégrée au job trivy-image-scan (trivy image --format cyclonedx), exposé en artifact (reports:cyclonedx + paths).
Choix d'implĂ©mentation : pas de job dĂ©diĂ©. L'image candidate est dĂ©jĂ pull et le cache de la base Trivy dĂ©jĂ chaud â une invocation de plus, zĂ©ro job supplĂ©mentaire, et le SBOM porte exactement sur l'image scannĂ©e/promue. Inventaire pur (ni --exit-code ni --severity) : ne fait jamais Ă©chouer le pipeline.
Limite tier : la Dependency List UI qui consomme reports:cyclonedx est Ultimate-only. En Free, la valeur est l'artifact SBOM (téléchargeable, conservable, base d'un futur scan VEX / vuln management continu). Détail ADR 005 §9.
#80 â Migration VPC CNI â Cilium (overlay, eBPF) (2026-06-18)
Quoi : remplacement du VPC CNI d'AWS par Cilium en mode overlay VXLAN (ipam: cluster-pool, pod CIDR 10.42.0.0/16 hors VPC), avec kubeProxyReplacement eBPF (suppression de kube-proxy) et Hubble + Hubble UI (observabilité réseau interne). Rend la Prefix Delegation #79 obsolÚte (l'overlay affranchit la densité du plafond ENI). ADR 019, guide comprendre/cilium.md.
Ordre de boot (le point dur) : un managed node group EKS exige des nodes Ready, et ArgoCD ne peut pas poser Cilium (il a lui-mĂȘme besoin d'un CNI). On distingue le composant vpc-cni (DaemonSet aws-node, installĂ© par dĂ©faut par EKS) de l'addon managĂ© : on retire la gestion managĂ©e (ressource Terraform aws_eks_addon.vpc_cni), le composant reste en self-managed et amĂšne les nodes Ready au boot. Cilium est ensuite posĂ© en premier par l'amorce Ansible, puis cutover : suppression d'aws-node + kube-proxy (self-managed â non recréés), flush des iptables rĂ©siduelles de kube-proxy (DaemonSet one-shot), restart de coredns (reprend une IP overlay).
Pourquoi self-managed et pas l'addon managĂ© : un addon managĂ© est rĂ©conciliĂ© par un contrĂŽleur EKS qui recrĂ©erait aws-node aprĂšs suppression â conflit avec Cilium.
Conservé : launch template maxPods: 110 (désormais servi par l'overlay Cilium), AmazonEKS_CNI_Policy (le vpc-cni self-managed s'en sert à l'amorce), NetworkPolicy standard allow-fastapi (#55, Cilium enforce les NetworkPolicy K8s).
Validation : hors infra tfsec 0 HIGH, terraform fmt, ansible --syntax-check, helm template cilium (values rendues). ValidĂ© LIVE le 2026-06-18 sur un aws-start from-scratch : plus d'aws-node/kube-proxy, coredns en 10.42.x, kubeProxyReplacement: True, DNS OK, NetworkPolicy allow-fastapi nĂ©gatif (egress 80 â Network unreachable) + positif (/healthz/ready 200) prouvant l'enforcement Cilium sans addon managĂ©, Hubble UI/relay Running, api.devopsyouss.com â 200, ArgoCD 6/6 Synced/Healthy, ESO SecretSynced, observabilitĂ© debout. Deux piĂšges levĂ©s en cours de route avant d'y arriver : INC-058 (webhooks d'admission) puis INC-059 (install ArgoCD). #80 fermĂ©e.
INC-058 : webhooks d'admission injoignables sous overlay Cilium (2026-06-18)
Quoi : Ă la premiĂšre validation live de la migration Cilium (#80), l'amorce Ansible a Ă©chouĂ© sur Install cert-manager (failed post-install: timed out, le startupapicheck ne joint pas le webhook) puis sur la crĂ©ation du ClusterIssuer (Address is not allowed). Le cutover rĂ©seau lui-mĂȘme Ă©tait sain (DNS, kubeProxyReplacement, Hubble validĂ©s).
Cause racine : sous Cilium overlay (pods en 10.42.x, hors VPC), le control plane EKS managé (ses ENI vivent dans le VPC 10.0.x, il ne participe pas à l'overlay VXLAN) ne sait pas router vers une IP de pod overlay. Tout webhook d'admission self-hosted dont le pod a une IP 10.42.x devient donc injoignable depuis l'API server. Avec failurePolicy: Fail, l'appel échoue et bloque l'opération (Address is not allowed).
Cartographie des webhooks (qui casse, qui non) :
| Webhook | failurePolicy | Touché ? |
|---|---|---|
cert-manager-webhook |
Fail | â oui (corrigĂ©) |
ESO externalsecret / secretstore-validate |
Fail | â oui (corrigĂ©) |
envoy-gateway-topology-injector |
Ignore | â non (no-op si injoignable) |
vpc-resource-*, pod-identity-webhook |
â | â non (managĂ©s EKS, hors overlay) |
Fix : passer les webhooks impactĂ©s en hostNetwork: true â le pod prend l'IP du node (10.0.x, dans le VPC), de nouveau routable par le control plane. CĂąblĂ© dans ansible/bootstrap.yml (values Helm) :
- cert-manager :
webhook.hostNetwork: true+webhook.securePort: 10260(10250 = conflit kubelet sur le réseau du node). - ESO :
webhook.hostNetwork: true+webhook.port: 10261. Le cluster est mono-node : les deux webhooks bindent le mĂȘme rĂ©seau de node, donc deux ports distincts obligatoires (et tous deux hors du 10250 du kubelet).
Validé live le 2026-06-18 (MR !178) : au re-run from-scratch, les deux pods webhook portent bien l'IP du node (10.0.3.89), l'amorce passe cert-manager + ClusterIssuer + ESO sans Address is not allowed, et les ExternalSecrets sont SecretSynced.
Pourquoi pas un patch live : le fix testĂ© live en helm upgrade --set ne survit pas, car la tĂąche Ansible repose le chart avec --reset-values (le helm du module reset les values non dĂ©clarĂ©es) â le fix doit vivre dans le playbook. PiĂšge helm notĂ© au passage : --reuse-values + chart latest â nil pointer networkPolicy.enabled ; Ă©pingler la version (v1.16.2).
Leçon : un overlay non-natif sur control plane managĂ© qu'on ne contrĂŽle pas (EKS) rompt le routage control-plane â pod. Tout composant que l'API server doit joindre en retour (webhooks d'admission, conversion, agrĂ©gation d'API) doit ĂȘtre en hostNetwork ou exposĂ© par un Service routable cĂŽtĂ© VPC. Ce n'est pas une question de version K8s (vrai en 1.32 comme en 1.34), c'est du routage rĂ©seau. MĂȘme classe que INC-055/INC-046 : « configurĂ© â joignable au runtime », la preuve c'est le comportement live.
INC-059 : install ArgoCD bloquée par une course de helm --wait (2026-06-18)
Quoi : une fois le trou des webhooks (INC-058) franchi, l'amorce est allée jusqu'à Install ArgoCD et a échoué : Error: services "argocd-applicationset-controller" not found. L'install helm était en --wait.
Cause : course connue du chart argo-cd avec helm --wait. Helm applique les manifests puis attend que chaque ressource soit prĂȘte ; il interroge le Service argocd-applicationset-controller avant qu'il soit enregistrĂ© dans l'API, reçoit NotFound et abandonne (sur un NotFound, helm ne retente pas, contrairement Ă un « pas encore Ready »). Rendu plus probable par un cluster fraĂźchement bootĂ© (API briĂšvement lente juste aprĂšs le cutover Cilium + restart de coredns). Sans rapport avec Cilium (vĂ©rification API, pas datapath) ni avec le teardown (erreur helm propre, pas une coupure de connexion).
Fix : wait: false sur la tùche Install ArgoCD. On ne délÚgue plus la disponibilité à helm : la tùche Wait for ArgoCD server pod (k8s_info, condition Ready) qui suit la gÚre déjà , exactement comme pour les autres composants de l'amorce. La séquence (root-app appliqué aprÚs que le serveur ArgoCD soit Ready) est préservée.
Validé live le 2026-06-18 (MR !179) : au re-run from-scratch, l'amorce passe Install ArgoCD puis pose le root-app ; les 6 Applications ressortent Synced/Healthy.
Leçon : helm --wait traite un NotFound transitoire comme un échec définitif. Pour une ressource créée en parallÚle sur un cluster cold-boot, préférer un gate de disponibilité idempotent et patient (k8s_info wait_condition Ready) plutÎt que le --wait du chart. Suivi (hors live) : ajouter un resource_group aux jobs infra pour interdire tout chevauchement d'un pipeline start avec le destroy de 20h.