Aller au contenu

Troubleshooting

Cette page est rangée par symptôme observable, jamais par composant. Un opérateur qui débogue ne sait pas que sa panne vient de CoreDNS : il sait qu'il voit un 401, une page qui ne charge pas, un pod qui reste 0/1. L'entrée doit se trouver à partir de ce qu'il a sous les yeux.

Chaque entrée porte cinq blocs, dans cet ordre :

Bloc Ce qu'il donne
Symptôme ce que l'on voit, et depuis où on le voit
La fausse piste l'attribution erronée que ce symptôme provoque, et ce qui permet de l'écarter
Cause le mécanisme réel
Correctif les commandes, dans l'ordre
Réparé quand l'effet qu'il faut constater pour conclure. Pas « la commande est passée »

Les entrées ci-dessous décrivent des pannes qui se sont réellement produites sur l'instance de référence. La fausse piste n'est pas une précaution rhétorique : c'est le chemin qu'a effectivement pris le diagnostic la première fois, et celui que reprendra un opérateur qui voit la même chose.

Trouver son entrée

Ce que vous voyez Entrée
Aucun host ne répond, le navigateur refuse tout contournement 1
Tout marchait, puis plus rien depuis l'intérieur du cluster, après un redémarrage 2
Les APIs .NET répondent 401, les portails fonctionnent 3
IDX10205 dans les journaux, 401 après une connexion réussie 4
Un ExternalSecret en SecretSyncedError, ou un pod qui ne trouve pas son Secret 5
Les images des articles ne s'affichent pas, et pas sur tous les postes 6
La section des taux de change est vide, et le redevient après chaque redémarrage 7
Les portails appellent encore l'ancien domaine après un renommage 8
Les images du hub mettent des dizaines de secondes à s'afficher 9
Le nombre de pods explose, l'ordonnancement se bloque 10
Un host renvoie 404 par intermittence alors que le pod répond 11
kubectl devient injoignable pendant que les applications continuent de servir 12
kubectl top ne rend rien, les HPA restent en <unknown> 13

Avant de dérouler une entrée, poser la question qui trie la moitié d'entre elles : le symptôme se voit-il depuis un poste, depuis l'intérieur du cluster, depuis internet, ou depuis les trois ? Un défaut visible depuis un poste mais pas depuis un pod, ou l'inverse, n'a pas la même famille de causes.

1. Aucun host ne répond, et le navigateur n'offre aucun contournement

Symptôme. Tous les hosts, publics comme internes, sont injoignables en HTTPS. Le navigateur affiche une erreur de sécurité du type Did Not Connect: Potential Security Issue - this website requires a secure connection, sans bouton pour passer outre. Le comportement est identique sur tous les postes et dans tous les navigateurs. En ligne de commande, curl sort en erreur TLS ou rend le code 000.

La fausse piste. Chercher du côté du DNS ou de Traefik. Le nom résout, le port 443 accepte la connexion, Traefik sert : il sert simplement son certificat auto-signé interne. Puis tenter de « continuer malgré tout » dans le navigateur, ce qui n'existe pas si ${TV_DOMAIN} est sur la liste HSTS preload embarquée dans les navigateurs, ce qui est le cas de tout domaine en .app. La panne n'est donc pas une dégradation avec contournement, c'est une coupure totale.

Cause. Le certificat wildcard n'est pas Ready. Le TLSStore default de Traefik n'a rien à présenter et retombe sur le certificat auto-signé. En amont, deux causes couvrent la quasi totalité des cas :

  • le Secret cloudflare-dns-token est absent du namespace cert-manager, ou le jeton n'a pas les portées Zone:DNS:Edit et Zone:Zone:Read sur la zone du client ;
  • les deux arguments DNS-01 manquent sur le Deployment cert-manager. Ils ne sont pas dans les manifests amont : un kubectl apply du Deployment amont les efface en silence. Sans eux, cert-manager vérifie la propagation du challenge en interrogeant CoreDNS, qui renvoie tout *.${TV_DOMAIN} vers Traefik. Le message dans le challenge est alors Could not determine the zone for "_acme-challenge.${TV_DOMAIN}.": ... SERVFAIL.

Correctif.

kubectl get certificate -A
kubectl describe certificate wildcard-tradeval-app -n traefik-ingress
kubectl get secret cloudflare-dns-token -n cert-manager
kubectl get deploy cert-manager -n cert-manager \
  -o jsonpath='{.spec.template.spec.containers[0].args}' | tr ',' '\n'

La sortie de la dernière commande doit porter --dns01-recursive-nameservers-only et --dns01-recursive-nameservers=1.1.1.1:53,8.8.8.8:53. Si elles manquent, les remettre par le même patch qu'à l'étape 3 du Pas-à-pas :

kubectl patch deploy cert-manager -n cert-manager --type=json -p='[
  {"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--dns01-recursive-nameservers-only"},
  {"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--dns01-recursive-nameservers=1.1.1.1:53,8.8.8.8:53"}
]'

Ce patch ajoute les deux arguments à la fin de la liste : le relancer sur un Deployment qui les porte déjà les inscrit deux fois. Relire la liste d'arguments avant de patcher, avec la commande ci-dessus. Puis forcer une nouvelle tentative, sans attendre le report exponentiel de cert-manager :

kubectl -n traefik-ingress delete certificaterequest \
  -l cert-manager.io/certificate-name=wildcard-tradeval-app

Ces noms d'objets ne portent pas le domaine du client

wildcard-tradeval-app et son Secret wildcard-tradeval-app-tls sont des noms de ressources Kubernetes, pas des noms dérivés de ${TV_DOMAIN} : le paramétrage de la fiche client ne les réécrit pas. Sur une instance dont le domaine est tout autre, le certificat porte quand même ces noms-là. Ne pas chercher un objet nommé d'après le domaine du client, il n'existe pas.

Réparé quand. Trois constats, dans cet ordre :

kubectl get certificate wildcard-tradeval-app -n traefik-ingress
kubectl get secret wildcard-tradeval-app-tls -n traefik-ingress
openssl s_client -connect ${TV_VIP}:443 -servername hub.${TV_DOMAIN} </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -enddate -ext subjectAltName

READY=True, le Secret existe, et surtout la troisième commande rend un émetteur Let's Encrypt, une date d'expiration à plus de trente jours et un SAN *.${TV_DOMAIN}. Tant qu'elle rend l'émetteur par défaut de Traefik, rien n'est réparé, quoi que dise l'objet Certificate.

Le navigateur qui a échoué une fois garde la panne en mémoire

Un navigateur qui a rencontré l'erreur a mémorisé le HSTS pour ce host. Il continuera de refuser la connexion après la réparation. Faire « Oublier ce site » sous Firefox, ou tester depuis une fenêtre privée, ou depuis un navigateur qui n'a jamais ouvert l'URL. Sans cela, on conclut à tort que le correctif n'a rien donné, et on repart chercher ailleurs.

2. La résolution interne de *.${TV_DOMAIN} a disparu après un redémarrage

Symptôme. Tout fonctionnait. Après un redémarrage de control-plane, une mise à niveau Talos ou un talosctl apply-config, les services .NET repartent en 401, tandis que les portails continuent de servir des pages. Depuis un pod, getent hosts auth.${TV_DOMAIN} ne rend rien, ou rend une adresse publique au lieu de ${TV_TRAEFIK_CLUSTERIP}.

La fausse piste. Croire à un problème de certificat ou de Keycloak. Rien ne dit « DNS » : le service .NET journalise un échec d'authentification, pas un échec de résolution. Et la plateforme reste parfaitement joignable depuis un poste pendant toute la panne, puisqu'un poste interroge le résolveur du LAN, pas CoreDNS. La panne semble donc ne toucher que certains services, et seulement de l'intérieur.

Cause. Le bloc rewrite stop ... answer auto, qui renvoie *.${TV_DOMAIN} vers le Service ingress, a disparu du Corefile. Jusqu'au 2026-09-10 la cause habituelle était Talos, qui possédait la ConfigMap par son annotation config.k8s.io/owning-inventory et la réécrivait. Ce n'est plus le cas : Talos ne pose plus CoreDNS. Chercher donc du côté d'un kubectl apply malheureux ou d'une régression de core/coredns/coredns-stack.yaml.

Correctif.

kubectl get configmap coredns -n kube-system -o yaml | grep -A4 'rewrite stop'
kubectl apply -f core/coredns/coredns-configmap.yaml
kubectl rollout restart deployment/coredns -n kube-system

Réparé quand. Depuis un pod glibc, et pas depuis busybox, dont l'applet getent n'existe pas :

kubectl run dns-check --rm -i --restart=Never --image=debian:stable-slim \
  -- getent hosts auth.${TV_DOMAIN}
kubectl run dns-check --rm -i --restart=Never --image=debian:stable-slim \
  -- getent hosts kubernetes.default.svc.cluster.local

La première doit rendre une adresse A égale à ${TV_TRAEFIK_CLUSTERIP}. La seconde est le contrôle de non-régression : un Corefile mal formé casse toute la résolution du cluster, et ce second appel est le seul qui le montre. Les pods reprennent d'eux-mêmes dès que la résolution répond ; si des 401 persistent au-delà de quelques minutes sur un service .NET, le redémarrer, son client OIDC gardant en cache le document de découverte.

Cette panne ne revient plus après une opération Talos, depuis le 2026-09-10

Elle se rejouait à chaque reboot de control-plane, montée de version ou talosctl apply-config, parce que Talos possédait la ConfigMap. Ce n'est plus le cas : cluster.coreDNS.disabled: true est dans talos/controlplane.yaml et dans la configuration des trois nœuds, et core/coredns/coredns-stack.yaml porte les six objets. Réappliquer la configuration machine du dépôt est donc sans danger. Contrôle qui le prouve : talosctl get manifests ne rend plus aucune entrée coredns. La réparation ci-dessus reste valable pour les autres causes, un kubectl apply malheureux ou une régression du fichier versionné, à ceci près qu'elle se joue désormais sur coredns-stack.yaml. Deux règles Prometheus du groupe dns signalent la disparition du bloc rewrite en cinq minutes.

3. Les services .NET répondent 401 sur tous les appels, les portails fonctionnent

Symptôme. Tous les appels authentifiés vers les APIs .NET échouent en 401. Les portails Next.js fonctionnent, les utilisateurs se connectent sur Keycloak sans problème. Keycloak est sain : 1/1, aucun redémarrage.

La fausse piste. Soupçonner Keycloak, ou la configuration OIDC des APIs. Deux choses alimentent l'erreur. D'abord, /api/auth/profile renvoie 401 sans jeton, ce qui est le comportement normal : le symptôme ressemble à un défaut de configuration d'API. Ensuite, chaque endpoint testé isolément répond 200. Vérifier chacun séparément ne prouve rien ici, et fait perdre du temps.

Cause. CoreDNS rend une chaîne CNAME au lieu d'un enregistrement A. La glibc, bibliothèque C des conteneurs .NET, refuse de la suivre : l'appel TLS et OIDC ne part jamais. La musl, utilisée par les images Alpine et Node, est moins stricte et résout quand même. D'où une panne qui semble ne toucher que certains services, et c'est le fait que les portails Node fonctionnent qui la désigne.

Un second mécanisme a existé et produisait le même 401 : la confiance dans un CA interne, distribué aux charges par trust-manager sous forme de ConfigMaps montées via SSL_CERT_FILE. Il a été retiré le 2026-09-10 avec le CA lui-même, *.${TV_DOMAIN} étant signé par Let's Encrypt, présent dans le magasin par défaut de toutes les images. Un cluster installé depuis ce dépôt n'a ni CA interne, ni trust-manager, ni ces ConfigMaps ; sur un cluster plus ancien, kubectl get bundles qui répond encore quelque chose signale un reste à retirer, pas une cause à traiter.

Correctif. Vérifier la résolution depuis un pod .NET, pas depuis le poste :

kubectl -n api exec deploy/trade-valuation-svc -- getent hosts auth.${TV_DOMAIN}

Si getent ne rend pas ${TV_TRAEFIK_CLUSTERIP}, c'est le DNS : traiter l'entrée 2.

Réparé quand. Les journaux du service .NET ne servent à rien ici : ils journalisent un échec d'authentification, jamais sa cause. Les deux constats utiles sont :

  • getent hosts auth.${TV_DOMAIN} depuis un pod .NET rend ${TV_TRAEFIK_CLUSTERIP} ;
  • un parcours réel : une connexion suivie d'un appel d'API depuis un portail rend autre chose qu'un 401.

Un 401 qui persiste alors que ces deux points sont vrais n'est plus ce défaut. Passer à l'entrée 4.

4. Tous les appels authentifiés échouent avec IDX10205

Symptôme. IDX10205 dans les journaux des services .NET, et 401 sur tous les appels d'API juste après une connexion réussie. L'utilisateur se connecte, obtient un jeton, et ce jeton est refusé partout.

La fausse piste. Croire à un Keycloak en panne. Il est parfaitement sain : 1/1, aucun redémarrage, et la seule ligne d'erreur dans ses journaux est une déconnexion client (ClosedChannelException) sans rapport. Le document de découverte répond 200 quand on l'appelle depuis le poste, et il répond 200 quand on l'appelle depuis le cluster. Les deux réponses ne sont simplement pas d'accord entre elles, et aucun test qui les prend séparément ne peut le montrer.

Cause. KC_HOSTNAME porte la forme courte (auth.${TV_DOMAIN}) au lieu de la forme URL complète. Keycloak accepte les deux, et elles ne se comportent pas pareil :

Forme de KC_HOSTNAME Comportement
auth.${TV_DOMAIN} le nom d'hôte est figé, le schéma est déduit de chaque requête
https://auth.${TV_DOMAIN} schéma, hôte et port figés, issuer invariant

Avec KC_PROXY_HEADERS=xforwarded, la déduction s'appuie sur X-Forwarded-Proto. Traefik l'ajoute, un appel direct au Service keycloak ne l'a pas. Le même serveur annonce donc https://... sur le chemin public et http://... sur le chemin interne. Les jetons remis aux navigateurs portent iss en https ; les APIs, qui valident contre le document interne, attendent http ; le iss ne correspond pas et tout est rejeté.

Seconde cause, et c'est celle qu'un opérateur rencontrera réellement : après un renommage de domaine, les clés Vault qui portent l'issuer (AUTH_KEYCLOAK_ISSUER, NEXTAUTH_URL, NEXTAUTH_URL_INTERNAL, l'authority OIDC des services .NET) n'ont pas été modifiées en même temps que KC_HOSTNAME. Ces clés changent ensemble ou pas du tout.

La forme courte n'est pas atteignable sur une installation neuve

apps/keycloak/keycloak.yaml fixe déjà KC_HOSTNAME à la forme URL complète, et render-config.sh n'y substitue que le domaine. La première cause ci-dessus est donc l'histoire du défaut, pas son chemin d'arrivée le plus probable : elle ne redeviendra atteignable que si quelqu'un raccourcit cette valeur à la main. Sur une installation conforme, vérifier d'abord les clés Vault de la seconde cause. La mécanique de la première reste à connaître, parce que c'est elle qui explique pourquoi comparer les deux chemins est le seul test qui conclut.

Correctif. Le test qui tranche compare les deux chemins d'accès au même endpoint :

curl -sS https://auth.${TV_DOMAIN}/realms/${TV_KC_REALM}/.well-known/openid-configuration \
  | grep -o '"issuer":"[^"]*"'

kubectl -n auth port-forward svc/keycloak 8080:80 &
curl -sS http://127.0.0.1:8080/realms/${TV_KC_REALM}/.well-known/openid-configuration \
  | grep -o '"issuer":"[^"]*"'

Le port-forward n'ajoute aucun en-tête X-Forwarded-Proto : c'est exactement le chemin que prennent les APIs. Si les deux sorties diffèrent, corriger KC_HOSTNAME en forme URL complète dans apps/keycloak/keycloak.yaml, réappliquer, puis redémarrer les services .NET :

for z in web internal api auth; do kubectl rollout restart deployment -n "$z"; done

Ce redémarrage n'est pas optionnel : le client OIDC de .NET met en cache le document de découverte, avec un rafraîchissement par défaut de l'ordre de douze à vingt-quatre heures. Sans lui, les APIs continuent de valider contre l'ancien issuer longtemps après le correctif, et le symptôme paraît intact.

Réparé quand. Les deux commandes ci-dessus rendent exactement la même chaîne, https://auth.${TV_DOMAIN}/realms/${TV_KC_REALM}, et un parcours réel de connexion suivi d'un appel d'API rend autre chose qu'un 401.

La recette de conformité ne détecte pas ce défaut

Le contrôle 6 de Validation compare l'issuer annoncé sur le chemin public à celui attendu. Sur ce défaut précis, le chemin public annonce justement la bonne valeur : la recette passe alors que les APIs, qui interrogent le chemin interne, reçoivent l'autre. La comparaison des deux chemins est le seul test qui conclut.

5. Un ExternalSecret reste en SecretSyncedError

Symptôme. kubectl get externalsecret -A montre un ou plusieurs objets autrement qu'en SecretSynced. En aval, les pods qui consomment le Secret concerné restent en CreateContainerConfigError s'il n'existe pas du tout, ou démarrent normalement avec des valeurs périmées s'il existe encore.

La fausse piste. Deux, également coûteuses. La première est de conclure qu'External Secrets Operator est cassé, alors qu'un seul chemin Vault l'est : le nombre d'objets en erreur est le diagnostic, lire la liste avant de toucher quoi que ce soit. La seconde est de corriger le Secret Kubernetes à la main, par kubectl edit ou kubectl create secret : creationPolicy: Owner le réécrit à la synchronisation suivante, une minute plus tard, sans rien signaler. Le seul point d'écriture est Vault.

Cause. Elle se lit dans la forme de la sortie :

Ce que montre kubectl get externalsecret -A Cause Où confirmer
Tous en erreur le ClusterSecretStore n'est pas prêt, ou Vault s'est rescellé après un redémarrage kubectl get clustersecretstore vault-store doit valoir READY=True ; kubectl exec -n vault vault-0 -- vault status doit rendre Sealed: false
Un ou deux objets nommés le chemin Vault est absent ou mal orthographié kubectl describe externalsecret <nom> -n <ns> nomme le chemin ; vault kv get -mount=secrets prod/<service> dit s'il existe
Aucun objet retourné du tout ESO n'est pas installé, ses CRD manquent
Tous SecretSynced, mais un service se comporte mal un data: énuméré au lieu de dataFrom: extract : l'oubli d'une clé est silencieux, le pod démarre et n'échoue qu'à l'usage comparer les clés du Secret produit à celles du chemin Vault

Correctif. Selon la cause : desceller Vault (page Exploitation), créer le chemin Vault manquant avec vault kv patch, ou réécrire l'ExternalSecret sous la forme obligatoire (dataFrom: - extract:, creationPolicy: Owner, deletionPolicy: Retain). Dans tous les cas, terminer par un redémarrage des Deployments concernés : les variables d'environnement sont figées au démarrage du conteneur, et un Secret corrigé n'atteint pas un pod déjà en cours.

kubectl rollout restart deployment/<nom> -n <zone>

vault kv put sur un chemin partagé casse un service sans erreur

put remplace l'intégralité du contenu du chemin : toute clé absente de la commande disparaît, donc disparaît du Secret à la synchronisation suivante. L'ExternalSecret reste SecretSynced, puisque le chemin existe toujours : il a seulement maigri. deletionPolicy: Retain ne protège pas de ce cas. Les pods en cours continuent de tourner sur leurs variables figées, et la panne apparaît au redémarrage suivant, découplée de la commande qui l'a causée. Sur tout chemin prod/<service>, c'est vault kv patch.

Réparé quand.

kubectl get externalsecret -A
kubectl get secret <nom> -n <ns> -o jsonpath='{.data}' | tr ',' '\n' | cut -d'"' -f2
kubectl get pods -n <zone> -l app=<nom>

Tous les objets en SecretSynced, et le Secret cible porte bien tous les noms de clés attendus, et le pod qui le consomme est 1/1 après redémarrage. Un SecretSynced seul ne prouve pas que le Secret porte les bonnes clés : c'est le décompte des noms de clés qui le prouve. La commande ci-dessus n'affiche que des noms, jamais des valeurs : ne jamais la remplacer par un -o yaml.

6. Les images des articles ne s'affichent pas, 403 selon le poste

Symptôme. Les images des actualités s'affichent sur un poste et pas sur un autre. La page elle-même est servie normalement. Dans les journaux du content-service : You are not allowed to perform this action., plusieurs dizaines de fois par jour.

La fausse piste. Partir sur le poste, parce que la différence entre les postes est réelle et saute aux yeux. Elle est une conséquence, pas la cause. Le réflexe qui remet dans le bon sens : un symptôme identique sur hub et sur portal désigne la source, c'est-à-dire les données ou le content-service ; un symptôme différent selon le poste ou le navigateur désigne la résolution de *.${TV_DOMAIN} sur le poste. Ici les deux se combinent, et c'est la source qu'il faut corriger.

Cause. SERVER_URL (ou PAYLOAD_PUBLIC_SERVER_URL) est définie sur le content-service. Payload renvoie alors des URLs de média absolues. La balise <img> du navigateur appelle le content-service en direct, sans clé API ni cookie, et l'accès aux médias exige une autorisation : une lecture anonyme reçoit un 403. Absente, Payload émet des URLs relatives, que le navigateur résout depuis l'origine du portail ; elles arrivent donc sur le proxy /api/media de hub et de portal, qui ajoute la clé API côté serveur.

Correctif.

kubectl exec -n api deploy/trade-content-service-svc -- \
  env | grep -E 'SERVER_URL|PAYLOAD_PUBLIC'
vault kv patch -mount=secrets prod/content-service SERVER_URL=
kubectl rollout restart deployment/trade-content-service-svc -n api

La variable doit rester vide, jamais renseignée. patch et non put : ce chemin porte toutes les autres clés du service.

Réparé quand.

  • Sur le nouveau pod, kubectl exec ... env | grep -E 'SERVER_URL|PAYLOAD_PUBLIC' ne rend rien, ou rend une valeur vide. L'environnement d'un pod déjà démarré est figé : mesurer sur le pod d'avant ne prouve rien.
  • Dans l'inspecteur réseau du navigateur, sur une page qui porte des images, les requêtes média partent vers https://hub.${TV_DOMAIN}/api/media/... et non plus vers https://content.${TV_DOMAIN}/..., et elles rendent 200.
  • Le compteur de 403 cesse d'augmenter. C'est vérifiable, et c'est précisément ce qu'il faut chercher dans les journaux :
kubectl logs -n api deploy/trade-content-service-svc --since=10m \
  | grep -c 'not allowed to perform this action'

Attendu : 0.

SERVER_URL sert aussi ailleurs

Elle alimente les liens des courriels, la prévisualisation et certains liens absolus de la console d'administration Payload. Vérifier ces trois usages après le retrait. Si l'un casse, le correctif est applicatif, côté portails, jamais le retour de la variable : la remettre ramène les 403 à l'identique.

7. La section des taux de change est vide, et le redevient à chaque redémarrage

Symptôme. La section des taux de change est vide sur le hub et sur le portal, en même temps. En base, tous les taux portent exchangeStatus = "expired" alors que leur validTo est dans le futur, donc qu'ils sont encore valides. Signature caractéristique : expiredBy vaut NULL, c'est-à-dire une écriture système et non une action humaine. Et le symptôme revient après chaque redémarrage de pod, y compris après une réparation des données.

La fausse piste. Deux. La première est de réparer les données et de considérer l'affaire close : tant que le correctif applicatif n'est pas déployé, chaque redémarrage recorrompt les taux, et une réparation faite avant ce déploiement est effacée au prochain redémarrage. La seconde est de chercher dans les journaux : le catch du seed masque l'erreur d'unicité sous un message du genre « already exists, skipping ». Rien n'apparaît dans les journaux. C'est ce silence qui rend la panne coûteuse.

Cause. PAYLOAD_AUTO_SEED est définie. Le seed complet s'exécute alors à chaque démarrage de pod, sans garde ni verrou. L'enchaînement : le seed recrée un taux avec le même validFrom pour chaque devise ; un hook expire le taux existant qui chevauche, dans une transaction séparée qui valide immédiatement ; le create échoue ensuite sur l'index unique et est annulé, mais l'expiration est déjà validée. Il reste une ligne expirée sans remplaçante, répété sur toutes les devises. Un HPA qui démarre plusieurs pods amplifie la corruption, chaque pod lançant son propre seed.

Correctif.

L'état sûr est l'absence de la clé, pas une valeur vide. Or vault kv patch ne sait qu'ajouter ou remplacer une clé, jamais en retirer une : le retrait passe par une réécriture délibérée du chemin, à partir de son contenu relu juste avant.

kubectl exec -n api deploy/trade-content-service-svc -- env | grep PAYLOAD_AUTO_SEED
vault kv get -format=json -mount=secrets prod/content-service | jq '.data.data' > cs.json
# retirer la ligne PAYLOAD_AUTO_SEED de cs.json, et rien d'autre
vault kv put -mount=secrets prod/content-service @cs.json
vault kv get -mount=secrets prod/content-service
rm -f cs.json
kubectl rollout restart deployment/trade-content-service-svc -n api

C'est le seul put légitime sur un chemin partagé, et il ne l'est que parce que le fichier porte toutes les autres clés, relues du chemin lui-même une commande plus tôt. Le vault kv get qui suit le put n'est pas décoratif : il constate que les autres clés sont toujours là. Le fichier intermédiaire contient des valeurs de secret en clair : le supprimer, et ne jamais l'écrire dans le clone git.

Puis réparer les données, et seulement les lignes expirées par le système. expired_by_id IS NULL les distingue d'une expiration décidée par un administrateur, par exemple le retrait d'un taux erroné, qu'il ne faut surtout pas remettre en service. Passer d'abord la lecture seule, puis le même WHERE en UPDATE, dans une transaction :

SELECT id, currency_id, valid_from, valid_to FROM exchange_rates
WHERE exchange_status = 'expired' AND expired_by_id IS NULL
  AND valid_from <= NOW() AND valid_to >= NOW()
  AND NOT EXISTS (
    SELECT 1 FROM exchange_rates e2
    WHERE e2.currency_id = exchange_rates.currency_id
      AND e2.administration_id = exchange_rates.administration_id
      AND e2.exchange_status = 'active' AND e2.id <> exchange_rates.id
      AND e2.valid_from <= NOW() AND e2.valid_to >= NOW());

Réparé quand. La ligne Running auto-seed... n'apparaît plus au démarrage. C'est le seul signal positif que les journaux donnent sur ce défaut :

kubectl logs -n api deploy/trade-content-service-svc --since=10m | grep -i 'auto-seed'

Attendu : aucune sortie. Puis le contrôle décisif, le seul qui prouve le correctif et non la réparation : redémarrer le pod une seconde fois et vérifier que la section reste peuplée sur les deux portails. Une réparation de données suivie d'un redémarrage sans corruption est la preuve ; la réparation seule ne prouve rien.

8. Les portails appellent encore l'ancien domaine après un renommage

Symptôme. Après un changement de ${TV_DOMAIN}, la fiche client rejouée et les manifests réappliqués, les pages des portails se chargent, mais les appels émis par le navigateur partent vers l'ancien domaine : requêtes en échec dans la console du navigateur, connexion qui redirige vers l'ancien host auth. Côté Kubernetes, rien : les pods sont Running, Ready, sans redémarrage, sans erreur dans les journaux.

La fausse piste. Redéployer, faire un kubectl rollout restart, modifier une variable d'environnement sur le Deployment, patcher la valeur dans Vault. Aucune de ces actions n'a le moindre effet, et l'absence d'effet fait chercher ailleurs alors que la cause est identifiée dès la première tentative.

Cause. Les variables NEXT_PUBLIC_* sont résolues au moment où l'image est construite, jamais au démarrage du conteneur. Rien au runtime ne peut les changer. Corollaire : un kubectl rollout undo ne répare pas non plus, il restaure une image qui porte elle aussi l'ancien domaine.

Correctif. Reconstruire les trois images de portail avec les bonnes valeurs, les publier sur ${TV_REGISTRY} sous les noms et tags attendus, puis redéployer. C'est le seul correctif. Vérifier dans le même mouvement les valeurs qui, elles, vivent au runtime et demandent seulement un redémarrage : AUTH_KEYCLOAK_ISSUER, NEXTAUTH_URL, NEXTAUTH_URL_INTERNAL, l'authority OIDC des services .NET, les *_API_BASE_URL, CONTENT_SERVICE_URL, ALLOWED_ORIGINS. Et hors du cluster : les Valid Redirect URIs et Web Origins du realm Keycloak, ainsi que la résolution DNS des postes.

Réparé quand. Deux constats, dont aucun ne passe par l'état des pods :

kubectl get deploy hub-portail -n web \
  -o jsonpath='{.spec.template.spec.containers[0].image}'; echo

Le tag lu doit être différent de celui d'avant. La plupart des images applicatives portent latest : avec un tag identique, rien ne prouve que le nouveau contenu a été tiré, et le pod peut servir l'ancienne image indéfiniment. Utiliser un tag distinct par version, ou forcer imagePullPolicy: Always.

Ensuite, dans le navigateur, page rechargée sans cache : l'inspecteur réseau ne montre aucune requête vers l'ancien domaine, et un parcours de connexion complet aboutit. Un pod Running et Ready ne prouve rien ici, et c'est exactement pour cela que ce défaut survit à un redéploiement.

9. Les images du hub mettent des dizaines de secondes à s'afficher

Symptôme. Sur la page d'accueil du hub, une image peut mettre jusqu'à une trentaine de secondes avant le premier octet, en interne comme en public. Les fichiers statiques de la même page reviennent en quelques dizaines de millisecondes. Aucun redémarrage, aucun OOMKilled, aucun évènement Kubernetes, aucune erreur dans les journaux. Le pod est Running et Ready pendant toute la durée du problème.

La fausse piste. Soupçonner l'ingress, un WAF ou le tunnel Cloudflare, surtout si l'un d'eux a été activé le même jour. Le témoin qui les écarte tous en une mesure : un fichier statique du même host répond en 25 ms. Comparer un chemin instrumenté à un chemin témoin évite de chercher au mauvais endroit. Seconde fausse piste, le réseau : curl -w montre time_starttransfer presque égal à time_total, donc du temps passé avant le premier octet, donc du calcul et non du transfert.

Cause. Un plafond CPU trop bas sur une charge de calcul par rafales. Le portail redimensionne et ré-encode ses images à la volée (/_next/image) ; avec limits.cpu: 250m, le cgroup étale sur quatre fois plus de temps un travail qui demande un coeur entier pendant quelques centaines de millisecondes. Le conteneur n'est pas tué, il est ralenti. Contrairement à une limite mémoire, qui tue le conteneur et laisse une trace (OOMKilled, exit 137), le throttling CPU est invisible dans l'état des objets Kubernetes. Trois facteurs amplifient : un fichier source de plusieurs mébioctets, huit variantes demandées dans plusieurs formats selon le navigateur, et un cache éphémère et par réplique, vidé à chaque rollout.

Correctif. Porter la limite CPU du portail à 2000m et la limite mémoire à 512Mi, les tampons d'encodage AVIF étant gourmands. Laisser les requests inchangées : rien de plus n'est alors réservé sur le noeud. Le faire sur les trois portails, pas seulement celui qui a été signalé : même runtime, même défaut latent, et un portail derrière authentification ne montre pas sa lenteur depuis l'extérieur, raison de plus pour ne pas attendre qu'un utilisateur la signale. Le correctif de fond est applicatif : alléger les fichiers sources, un WebP de 1920 px de qualité équivalente pesant de l'ordre de 200 Ko contre plusieurs mébioctets.

Réparé quand. Deux mesures de la même commande, l'une avant le changement, l'autre après, toutes deux à froid, c'est-à-dire juste après un rollout restart qui vide le cache :

kubectl rollout restart deployment/hub-portail -n web
kubectl rollout status deployment/hub-portail -n web
curl -sS -o /dev/null -w 'starttransfer=%{time_starttransfer} total=%{time_total}\n' \
  'https://hub.${TV_DOMAIN}/_next/image?url=%2Fimages%2Fhero-slide-1.png&w=1920&q=75'

C'est le rapport entre les deux mesures qui conclut, pas une valeur absolue. Passer de 250m à 2000m lève un facteur d'étalement du même ordre : un temps inchangé, ou qui ne gagne qu'un tiers, signifie que le plafond n'a pas bougé. Deux raisons de ne transposer aucun chiffre : le matériel du client est inconnu, et curl n'annonce pas Accept: image/avif, donc il ne reçoit pas la variante AVIF, la plus coûteuse et celle qu'un navigateur demande. La mesure par curl est un indicateur relatif, jamais le temps que voit l'utilisateur. Un second appel, cache chaud, ne prouve rien : il répond vite dans les deux cas.

Contrôle complémentaire, celui qui prouve que la nouvelle limite est réellement en vigueur : pendant la requête,

kubectl top pod -n web -l app=hub-portail

doit montrer une consommation au-dessus de 250m. Si elle plafonne exactement à 250m, le manifeste modifié n'a pas été appliqué, quelle que soit la latence observée.

Aucun tableau de bord ni aucune alerte n'aurait signalé cette panne. Superviser la latence, pas seulement la disponibilité : c'est le tableau tradeval-red de Observabilité qui la porte.

10. Le nombre de pods explose et l'ordonnancement se bloque

Symptôme. Peu après l'installation de metrics-server, le nombre de pods bondit, le CPU des workers monte à 98-99 %, des pods restent en Pending avec 0/6 nodes are available: 3 Insufficient cpu, les mises à jour en cours se bloquent, et des 404 intermittents apparaissent sur les hosts publics.

La fausse piste. Conclure que metrics-server a cassé quelque chose, et le désinstaller. Il n'a rien cassé : il a rendu exécutable une configuration erronée qui dormait. Jusque-là les HPA affichaient <unknown> et n'avaient jamais rien fait, donc leur configuration n'avait jamais été confrontée au réel. Le désinstaller recache le défaut au lieu de le corriger, et le prochain opérateur le retrouvera intact.

Cause. Une HPA basée sur la mémoire, avec un request inférieur à l'usage au repos, monte indéfiniment. Les services .NET consomment nettement plus que leur request, donc structurellement au-dessus de la cible : ajouter des répliques ne réduit pas la mémoire par pod d'un runtime à ramasse-miettes, qui occupe ce qu'on lui laisse. Le système monte jusqu'à maxReplicas quelle que soit la charge réelle. Facteur aggravant, un maxReplicas très élevé sur un cluster de trois workers.

Correctif. Commencer par situer ce que l'on regarde : les dix HPA livrés portent déjà le correctif de cet incident. Tous sont à maxReplicas: 3, sauf celui de hub-portail à 6, tous n'ont qu'une métrique cpu, aucune métrique memory, et chaque manifeste porte l'explication à l'endroit où la valeur se lit.

kubectl get hpa -A -o custom-columns=\
'NS:.metadata.namespace,NOM:.metadata.name,MAX:.spec.maxReplicas,METRIQUES:.spec.metrics[*].resource.name'

Ce qui se lit dans cette sortie décide de la suite :

Ce que la sortie montre Ce que cela signifie
MAX à 3, ou 6 pour hub-portail, et METRIQUES à cpu seul l'état livré. Le problème est ailleurs, ce n'est pas cet incident
Une métrique memory, ou un MAX très supérieur une divergence, pas l'état livré. Quelqu'un a modifié le HPA dans le cluster, ou un manifeste plus ancien a été réappliqué

Dans le second cas, réappliquer les manifests du dépôt, qui portent les bonnes valeurs. Si la divergence vient du dépôt lui-même, la corriger dans les deux : sinon un kubectl apply ultérieur réapplique les anciennes valeurs. Règle générale à retenir au-delà de ce cas : ne jamais activer une HPA mémoire sur un runtime à ramasse-miettes (.NET, JVM, Node) sans avoir vérifié que le request reflète l'usage au repos.

Réparé quand.

kubectl get hpa -A
kubectl get pods -A --field-selector=status.phase=Pending
kubectl top nodes

La colonne TARGETS ne porte plus de métrique mémoire, REPLICAS est revenu au nominal et y reste sur plusieurs minutes, aucun pod n'est Pending, et le CPU des workers est redescendu.

Un HPA silencieux n'est pas forcément un HPA réparé

Un HPA qui affiche <unknown> dans TARGETS n'est pas stabilisé, il est aveugle : la source de métriques ne lui répond pas. kubectl top nodes doit rendre des valeurs. Un cluster calme parce que ses autoscalers ne voient rien est exactement l'état qui a précédé cet incident, et c'est une panne à part entière : entrée 13.

11. Un pod répond mais Kubernetes le déclare indisponible, et l'host renvoie 404

Symptôme. Un host d'ingress renvoie 404 par intermittence, en interne comme par le tunnel. Le pod correspondant est Running mais 0/1, parfois depuis des heures, sans jamais redémarrer. Depuis l'intérieur du conteneur, le chemin de santé répond 200.

La fausse piste. Soupçonner Cloudflare ou l'ingress, puisque le symptôme est un 404 sur une route. Le comportement identique en interne et par le tunnel écarte le tunnel en une mesure. Seconde fausse piste, plus tenace : mesurer le chemin de santé depuis l'intérieur du conteneur, constater 200, et conclure que le pod est sain. Il l'est. Ce n'est pas ce chemin que la sonde interroge.

Cause. La sonde de disponibilité pointe un chemin couvert par le middleware d'authentification. Ce chemin répond une redirection vers la page de connexion, et le kubelet suit les redirections HTTP : la sonde mesure donc le temps de rendu de la page de connexion, variable, régulièrement supérieur au timeoutSeconds accordé. Le pod perd sa disponibilité, il est retiré du Service, et Traefik répond 404 faute d'endpoint. Une sonde qui échoue une fois sur cinq produit un pod qui oscille, bien plus difficile à diagnostiquer qu'une panne franche.

Le signal révélateur n'est pas dans les journaux du pod, qui ne disent rien : il est dans les évènements du kubelet.

kubectl describe pod <nom> -n <zone> | sed -n '/Events/,$p'

Chercher une ligne Readiness probe failed: Get "http://<ip>:3000/api/auth/signin?callbackUrl=%2Fhealth": context deadline exceeded. L'URL redirigée qui apparaît dans le message est le diagnostic complet : la sonde n'interroge pas ce que l'on croit.

Correctif. Pointer les sondes de disponibilité et de démarrage sur un chemin qui répond 200 sans rediriger (/api/health côté portails Next.js, /health et /health/ready côté .NET), et porter timeoutSeconds à 5 s pour une application Next.js en rendu serveur. Un chemin de sonde ne doit jamais rediriger, ni s'authentifier, ni dépendre d'un service externe : sinon il ne mesure pas la santé du pod, mais celle de la chaîne qu'il traverse.

Un seul manifeste livré ne respecte pas encore la seconde moitié de cette règle

Les trois portails pointent bien /api/health, qui ne redirige pas : la première moitié du défaut est corrigée partout. La seconde ne l'est pas. apps/web/portal/portal-portail.yaml et apps/web/internal/internal-portail.yaml portent timeoutSeconds: 5 sur leur sonde de disponibilité ; apps/web/hub/hub-portail.yaml n'en porte aucun, il prend donc le défaut de Kubernetes, 1 seconde, exactement la valeur que l'incident incrimine, pour un rendu serveur mesuré à 40-80 ms avec des pointes à 2,3 s. hub-portail est le seul portail public sans authentification, donc celui dont un battement se voit immédiatement de l'extérieur. Aligner sa sonde à 5 s avant la mise en service.

Réparé quand. Un défaut intermittent ne se constate pas en un appel.

kubectl get endpoints <service> -n <zone>
kubectl get pods -n <zone> -l app=<nom> -w    # observer au moins deux minutes
for i in $(seq 1 20); do
  curl -s -o /dev/null -w '%{http_code} ' https://portal.${TV_DOMAIN}/
done; echo

L'IP du pod figure bien dans les endpoints, c'est elle qui manquait quand Traefik répondait 404. Le pod reste 1/1 sur toute la durée d'observation, sans osciller. Et les vingt requêtes rendent vingt fois le même code de succès, sans un seul 404 et sans un seul 000 : ce dernier n'est pas un code de réponse, c'est curl qui n'a pas abouti, et vingt 000 ne sont donc pas vingt codes identiques. C'est le seul endroit du guide où curl reste volontairement muet, pour que les vingt codes restent lisibles : devant un 000, rejouer une seule requête en curl -sS -o /dev/null -w '%{http_code}\n' https://portal.${TV_DOMAIN}/ donne la raison que la boucle taisait. Enfin, kubectl describe pod ne montre plus aucun évènement Readiness probe failed récent.

12. kubectl devient injoignable et les hosts tombent par intermittence

Symptôme. Les hosts d'ingress deviennent injoignables par intermittence, puis kubectl cesse complètement de répondre pendant que les applications continuent de servir. Les kube-apiserver de deux des trois control-planes sont en CrashLoopBackOff.

La fausse piste. Croire le cluster perdu. Le plan de données survit à la perte de l'API : les workloads continuent de servir tout du long. Et le diagnostic reste possible en visant directement le control-plane survivant :

kubectl --server=https://<ip-du-control-plane-survivant>:6443 get pods -A

C'est la commande à connaître avant d'en avoir besoin : avec un endpoint de kubeconfig pointé sur une machine tombée, tout paraît mort.

Cause. L'activation d'un WAF de niveau 2 (Coraza) sur les Ingress Traefik, par trois mécanismes distincts qui se sont enchaînés :

  1. Un middleware invalide casse le routage, il ne le laisse pas passer. Mal typé, le plugin ne se contente pas d'être inactif : les routers qui le référencent deviennent inopérants et les hosts publics tombent. Un WAF mal configuré est une panne, pas une absence de protection.
  2. La consommation mémoire est pilotée par la concurrence, le host WASM instanciant un module par requête concurrente. Une seule instance mesurée à 762 Mio pour une base d'environ 80 Mio.
  3. Relever la limite mémoire de Traefik alors qu'il tolère les taints control-plane sature les control-planes, dont le kube-apiserver est alors tué. C'est le mécanisme qui transforme une panne d'ingress en perte de l'API.

Correctif. Le WAF de niveau 2 n'est pas viable dans cette configuration et a été retiré de tous les Ingress : ne pas le réactiver. Traefik ne doit jamais pouvoir être ordonnancé sur un control-plane : l'épingler sur les workers par nodeSelector et retirer les tolerations. Garder les Ingress publics sur le seul entrypoint websecure, le port 80 redirigeant globalement vers 443. Et dimensionner les control-planes à 4 Gio : à 2 Gio, ils tournaient à 100-117 % d'utilisation mémoire en régime normal, avec six à sept redémarrages d'apiserver cumulés en soixante-sept jours.

Réparé quand.

kubectl get pods -n kube-system -l component=kube-apiserver -o wide
kubectl get pods -n traefik-ingress -o wide
kubectl top nodes

Les trois apiservers Running avec un compteur RESTARTS stable sur plusieurs minutes : un compteur qui continue de monter signifie que les noeuds sont toujours saturés. Aucun pod Traefik sur un noeud control-plane. Et la mémoire des control-planes nettement sous 100 %. Dernier constat, le plus simple : kubectl get nodes répond depuis l'endpoint normal du kubeconfig, et plus seulement en visant une machine précise.

Noter les trois adresses de control-plane avant d'en avoir besoin

Si l'endpoint du kubeconfig désigne une seule machine, la perte de cette machine supprime tout accès d'administration, y compris pour diagnostiquer. Consigner les adresses des trois control-planes dans le dossier d'exploitation du client, à côté des clés de descellement de Vault. Cette information ne se retrouve pas quand kubectl ne répond plus.

13. kubectl top ne rend rien et les HPA restent en <unknown>

Symptôme. kubectl get hpa -A affiche <unknown> dans la colonne TARGETS sur tous les HPA, et kubectl top nodes rend error: Metrics API not available ou ne rend rien. Rien d'autre ne se plaint : les pods applicatifs tournent, les hosts répondent, et la recette de conformité passe, puisqu'elle ne regarde pas la source de métriques.

La fausse piste. Conclure que c'est l'état normal, au motif que « les HPA sont inertes tant que metrics-server n'est pas installé ». C'était vrai avant l'installation. Le guide pose core/metrics-server/metrics-server.yaml à l'étape 10 du Pas-à-pas : après une installation conforme, <unknown> signifie que la source ne sert pas de métriques, pas qu'elle est absente par conception. La seconde fausse piste est de s'en accommoder, parce que rien ne casse tant qu'on ne cherche rien.

Cause. Deux, et deux commandes les séparent. L'APIService v1beta1.metrics.k8s.io est déclaré dans le même fichier que le Deployment, et ce fichier ne fait partie ni de la couche de base ni d'aucun autre kubectl apply de la procédure : son absence et sa présence disent donc la même chose que celles du pod.

kubectl get apiservice v1beta1.metrics.k8s.io
kubectl get pods -n kube-system -l k8s-app=metrics-server
Ce que les deux commandes rendent Cause
Error from server (NotFound) sur l'apiservice, et aucun pod 1. L'étape 10 n'a pas été jouée en entier, le manifeste n'a jamais été appliqué. C'est le cas le plus fréquent, ce manifeste ne faisant pas partie de la couche de base
L'apiservice existe en AVAILABLE=False, avec un pod 0/1 ou en attente 2. Le pod ne sert pas de métriques. FailedDiscoveryCheck sur l'apiservice est la conséquence de cet état, pas une troisième piste : il reste False tant que le pod n'est pas 1/1

Sur la cause 2, le motif connu sur Talos est le refus de valider les certificats de service des kubelets, qui ne portent pas d'IP SAN : x509: cannot validate certificate for <adresse> because it doesn't contain any IP SANs. Le manifeste du dépôt porte déjà le drapeau --kubelet-insecure-tls qui lève ce cas ; ce message dans les journaux signifie donc que le Deployment en place n'est pas celui du dépôt, typiquement parce qu'un manifeste amont a été appliqué par-dessus.

Ce que cette panne coûte, et pourquoi elle est classée ici. Elle rend muets quatre points de contrôle donnés comme décisifs ailleurs dans ce guide : le kubectl top pod de l'entrée 9, seul contrôle qui prouve qu'une nouvelle limite CPU est en vigueur ; le kubectl top nodes des entrées 10 et 12 ; et la ligne « Mémoire des control-planes » du tableau de surveillance quotidienne de Exploitation. Un opérateur qui croit surveiller ne surveille rien. Elle immobilise en outre les dix HPA du dépôt, sans le moindre message.

Correctif.

kubectl logs -n kube-system -l k8s-app=metrics-server --tail=50
kubectl apply -f core/metrics-server/metrics-server.yaml

Le même kubectl apply répare les deux causes : il pose le Deployment absent dans le premier cas, et il restaure dans le second le drapeau que le manifeste amont ne porte pas. Sur la cause 1, les journaux n'ont rien à dire, il n'y a pas de pod ; c'est sur la cause 2 qu'ils servent, et la ligne à y chercher est celle qui contient IP SANs.

Réparé quand.

kubectl get apiservice v1beta1.metrics.k8s.io
kubectl top nodes
kubectl get hpa -A

AVAILABLE=True sur l'apiservice, kubectl top nodes rend des chiffres pour tous les noeuds, et la colonne TARGETS des HPA porte un pourcentage au lieu de <unknown>. Laisser jusqu'à une minute après le démarrage du pod : la première fenêtre de collecte n'est pas instantanée. Un kubectl top nodes qui ne rend qu'une partie des noeuds n'est pas réparé, il est partiellement aveugle.

Ne pas rendre les métriques disponibles sans avoir lu l'entrée 10

Les rendre disponibles réveille d'un coup les dix HPA du dépôt, avec leur configuration telle qu'elle est écrite, sans période de rodage. C'est ce qui a saturé le CPU du cluster de référence en quelques minutes. Les valeurs livrées portent déjà le correctif de cet incident (entrée 10) : vérifier qu'elles n'ont pas été modifiées depuis avant de poursuivre.

14. Deux DaemonSets Longhorn redémarrent sans fin sur Talos

longhorn-iscsi-installation et longhorn-preflight-installer restent en Init:CrashLoopBackOff, avec un compteur de redémarrages qui monte indéfiniment. Sur ce cluster il avait atteint 24 000 par pod, sur 85 jours.

kubectl logs -n longhorn-system <pod-iscsi> -c iscsi-installation --tail=20

Le message est sans ambiguïté :

nsenter: can't execute 'apk': No such file or directory
nsenter: can't execute 'rc-update': No such file or directory
nsenter: can't execute 'service': No such file or directory

Ces deux DaemonSets installent open-iscsi sur l'hôte avec le gestionnaire de paquets d'Alpine. Talos est immuable : il n'a ni apk, ni rc-update, ni service. Ils ne peuvent structurellement jamais réussir, et aucun réglage ne les y aidera.

Ils sont aussi inutiles. Les workers embarquent l'extension Talos iscsi-tools (Prérequis), et Longhorn le confirme lui-même :

kubectl get nodes.longhorn.io -n longhorn-system -o custom-columns=\
'NOEUD:.metadata.name,PAQUETS:.status.conditions[?(@.type=="RequiredPackages")].status'

RequiredPackages=True signifie que le prérequis est déjà satisfait.

La correction est de les supprimer :

kubectl delete daemonset longhorn-iscsi-installation -n longhorn-system
kubectl delete daemonset longhorn-preflight-installer -n longhorn-system

Vérifier avant de supprimer qu'ils ne portent aucune étiquette Helm (app.kubernetes.io/managed-by) : appliqués à la main, rien ne les recrée. Gérés par une release, il faut au contraire les désactiver dans les values, sans quoi ils reviennent à la prochaine montée de version.

Ce qui n'est pas une panne

Deux anomalies apparaissent sur une installation conforme et n'ont pas à être corrigées. Les confondre avec un défaut fait perdre du temps, et les corriger « au cas où » en crée un vrai.

Ce que l'on voit Pourquoi c'est normal
Un Grafana vide, sans datasource, dans le namespace observability Stack héritée, posée par la couche de base, qui ne reçoit rien. Celle qui porte les données est la stack monitoring (Observabilité)

La liste s'arrête là, et c'est délibéré. Deux symptômes en particulier n'y figurent pas et ne doivent jamais y entrer :

  • Un ImagePullBackOff dans une des quatre zones applicatives n'est jamais normal. Il signifie que acr-pull-secret n'a pas été créé, que l'image n'a pas été publiée sur ${TV_REGISTRY}, que son tag ne correspond pas à ce qui a été publié, ou que la sortie HTTPS sur le port 443 est filtrée depuis les noeuds (Prérequis), cause qu'aucun contrôle de preflight.sh ne teste. Le geste qui sépare cette quatrième cause des trois autres est kubectl get pods -A | grep -E 'ImagePullBackOff|ErrImagePull' : les trois premières ne touchent que les quatre zones applicatives, alors qu'un port 443 fermé bloque aussi les images amont de la couche de base, dans kube-system, traefik-ingress ou cert-manager. Le kubectl describe pod le confirme, un délai dépassé ou un nom non résolu sur l'hôte du registre plutôt qu'un 401 ou un not found. Aucun pod de ce namespace n'est censé échouer à tirer son image.
  • <unknown> dans kubectl get hpa n'est pas normal non plus après une installation conforme : c'est une panne, et elle rend muets quatre contrôles de ces deux pages. Voir l'entrée 13.

En revanche, trois interfaces d'exploitation exposées sans authentification ne sont pas une anomalie bénigne : c'est une exposition connue, à traiter avant toute mise en service, selon la recette de Observabilité.

Collecter les éléments avant de demander de l'aide

Avant tout redémarrage, et dans cet ordre. Un rollout restart passé trop tôt détruit la preuve du défaut qu'on cherche à faire analyser.

scripts/validate-install.sh --env scripts/install.env > diag-validate.txt 2>&1
kubectl get events --sort-by=.lastTimestamp -A     > diag-events.txt
kubectl get pods -A -o wide                        > diag-pods.txt
kubectl get certificate,order,challenge -A         > diag-certs.txt
kubectl get externalsecret -A                      > diag-secrets.txt
kubectl get clustersecretstore vault-store -o wide >> diag-secrets.txt
kubectl describe pod <pod-concerne> -n <ns>        > diag-describe.txt
kubectl logs deploy/<service-concerne> -n <ns> --tail=500 --timestamps > diag-logs.txt
kubectl logs deploy/<service-concerne> -n <ns> --previous --tail=500 > diag-logs-precedent.txt 2>/dev/null

Quatre précautions qui décident de l'exploitabilité de ce que vous envoyez :

  • Les évènements d'abord. kubectl get events ne porte qu'une heure d'historique par défaut. C'est la première chose à collecter, et la seule qui contienne le message de sonde de l'entrée 11.
  • --previous est la seule façon de lire les journaux d'un conteneur qui a déjà redémarré. Après un second redémarrage, ils sont perdus aussi.
  • Aucune valeur de secret. Ne jamais joindre la sortie de vault kv get, ni un kubectl get secret -o yaml, ni le fichier produit par vault operator init. Envoyer les noms de clés :
    kubectl get secret <nom> -n <ns> -o jsonpath='{.data}' | tr ',' '\n' | cut -d'"' -f2
    
  • Dire depuis où le symptôme se voit : depuis un poste, depuis l'intérieur du cluster, depuis internet, ou depuis les trois. La moitié des entrées de cette page se départagent sur cette seule information.

Joindre enfin, en clair, ce qui n'est pas secret et sans quoi rien n'est interprétable : ${TV_DOMAIN}, ${TV_VIP}, ${TV_KC_REALM}, la date et l'heure de la dernière modification faite sur le cluster, et ce qui a été tenté depuis.