Aller au contenu

Exploitation

Cette page couvre la plateforme une fois installée et recettée : sauvegarder et restaurer, renouveler les certificats, mettre à jour un service, en ajouter un, et savoir ce qu'il faut regarder tous les jours. Elle ne rejoue pas l'installation, qui vit au Pas-à-pas, ni le diagnostic d'une panne, qui vit à Troubleshooting.

C'est la dernière page du guide. Après elle, l'opérateur est seul avec l'instance : tout ce qui suit est écrit pour être relu six mois plus tard, sans le contexte de l'installation.

Sauvegardes et restauration

La procédure de descellement de Vault n'existe pas encore. Elle est bloquante.

Vault se rescelle à chaque redémarrage complet, et l'état de scellement est propre à chaque processus : les trois pods se descellent un par un, chacun avec trois des cinq clés. Tant que ce n'est pas fait, aucun ExternalSecret ne se synchronise, et rien d'autre dans la plateforme ne le signale : la recette de conformité ne le voit qu'indirectement, et seulement si un ExternalSecret tente une resynchronisation entre temps.

Ce guide ne porte aucune procédure de descellement ni de sauvegarde de Vault écrite et testée. C'est un manque connu. Elle doit être rédigée et exercée chez le client avant la mise en service, et elle doit répondre à quatre questions : qui détient les clés de descellement, combien de personnes en détiennent, où elles sont conservées, et qui est joignable la nuit. Sans cette procédure, un redémarrage complet du cluster arrête la plateforme jusqu'à ce qu'une personne détenant trois des cinq clés se connecte.

Ce qu'il faut sauvegarder, et à quel rythme

Objet Sauvegarde Restauration Fréquence
Données du coffre Vault (stockage Raft) vault operator raft snapshot save, depuis le poste et authentifié : la commande complète et son constat sont juste sous ce tableau vault operator raft snapshot restore, par le même chemin authentifié, sur un Vault initialisé et descellé quotidienne, et avant toute intervention sur le coffre
Clés de descellement et jeton racine ce n'est pas une commande : c'est le fichier produit par vault operator init, à conserver hors du dépôt et hors du cluster sans trois des cinq clés, aucune restauration n'est possible, l'instantané Raft étant chiffré une fois à l'installation, puis à chaque changement de clés
Base de données de Keycloak (realm, clients, utilisateurs) pg_dump -h <hote> -U <utilisateur> -Fc <base> > keycloak-$(date +%F).dump pg_restore -h <hote> -U <utilisateur> -d <base> --clean keycloak-<date>.dump quotidienne, et impérativement avant toute modification du realm
Volumes persistants du cluster par le contrôleur de stockage : déclarer une cible de sauvegarde externe (NFS ou S3), puis une tâche récurrente sur chaque volume restaurer le volume depuis la cible, puis relier le PersistentVolumeClaim au volume restauré quotidienne vers la cible externe
Manifests paramétrés et fiche client scripts/install.env copie hors du poste de l'opérateur, au même endroit que les clés de descellement remettre le fichier en place et rejouer render-config.sh à chaque modification

Sauvegarder Vault, en pratique

vault operator raft snapshot save frappe un endpoint authentifié de l'API Vault. Un kubectl exec dans le conteneur ne suffit donc pas : secrets/vault.yaml n'y pose aucun jeton, et la commande sort en missing client token. Le contraste avec vault operator unseal, que le pas-à-pas lance bien par kubectl exec, n'est pas une incohérence : le descellement, lui, n'est pas authentifié.

Le chemin qui marche est celui de l'étape 6.2 du Pas-à-pas, un port-forward vers le Service interne suivi d'une authentification :

kubectl port-forward -n vault svc/vault 8200:8200 &
export VAULT_ADDR=http://127.0.0.1:8200
vault login                     # jeton demande en interactif, jamais passe en argument
vault operator raft snapshot save "vault-$(date +%F).snap"

vault login sans argument réclame le jeton et ne l'affiche pas. Le passer en argument le laisserait dans l'historique du shell et dans la table des processus de la machine.

Constater que la sauvegarde a réussi. Sans ce constat, une tâche quotidienne peut produire des fichiers vides pendant des mois, et cela ne se découvre que le jour de la restauration :

ls -l "vault-$(date +%F).snap"
vault operator raft snapshot inspect "vault-$(date +%F).snap"

Le fichier a une taille non nulle, et inspect rend l'index Raft et le décompte des entrées qu'il contient. C'est le seul signal disponible : la présence du fichier ne prouve rien, et une archive tronquée fait échouer inspect alors que ls la montre.

La tâche automatisée est fournie : secrets/vault-backup.yaml. Un CronJob quotidien prend l'instantané, le vérifie par inspect, le dépose dans un bucket MinIO hors cluster (TV_MINIO_ENDPOINT de la fiche client), et purge ceux de plus de trente jours. Il ne porte aucun jeton : il s'authentifie par le ServiceAccount vault-backup sur le rôle Kubernetes du même nom, que scripts/build-vault.sh configure crée avec une politique qui n'ouvre que cet endpoint :

path "sys/storage/raft/snapshot" {
  capabilities = ["read", "sudo"]
}

La capacité sudo n'est pas optionnelle sur cet endpoint. Côté MinIO, un bucket et un compte qui n'écrit que dedans, dont les clés vont dans Vault sous prod/vault-backup (access-key, secret-key) par vault kv patch : l'ExternalSecret du même fichier les descend dans le namespace vault. La règle SauvegardeVaultEnRetard se déclenche après trente-six heures sans succès, et aussi tant que le premier succès n'a pas eu lieu.

Restaurer emprunte le même chemin authentifié, sur un Vault initialisé et descellé :

vault operator raft snapshot restore "vault-<date>.snap"

Un instantané est chiffré avec les clés de l'instance qui l'a produit. Les clés qui descelleront le Vault après restauration sont donc celles de l'instance d'origine, pas celles du Vault dans lequel on restaure. C'est ce qui rend le couple instantané plus clés indissociable, et ce qui fait de la ligne suivante du tableau autre chose qu'une formalité.

Quatre points de vigilance, chacun déjà responsable d'une sauvegarde inutilisable ailleurs :

  • Un instantané Vault sans les clés de descellement ne vaut rien. Il est chiffré avec elles. Conserver les clés ailleurs que les instantanés, et les deux ailleurs que le cluster.
  • Le realm Keycloak est créé à la main, jamais recréé par un manifeste. Perdre cette base, c'est recréer un par un chaque client OIDC, chaque Valid Redirect URI et chaque Web Origin. C'est la sauvegarde dont l'absence coûte le plus cher, et la seule dont le contenu n'existe nulle part ailleurs dans le dépôt.
  • Un instantané de volume qui reste sur les mêmes disques ne protège de rien. Sans cible de sauvegarde externe déclarée, l'instantané disparaît avec le noeud qu'il devait couvrir.
  • scripts/install.env est exclu de git et n'existe que sur le poste de l'opérateur. Le perdre oblige à redériver toutes les valeurs de l'instance.

La base de Keycloak n'est pas fournie par ce dépôt

Le déploiement Keycloak code en dur son URL JDBC vers un Service keycloak-db-rw sur le port 5432, qu'aucun manifeste de ce dépôt ne crée, et ce septième endpoint ne figure pas parmi les six endpoints de données externes de la fiche client. La base est donc fournie par le client, et la commande de sauvegarde dépend de ce qu'il fournit : pg_dump à travers le Service si la base est jointe par ce nom, ou le mécanisme propre à l'opérateur Postgres qui l'exploite, si elle est exploitée dans le cluster. Trancher ce point avec le client au moment de l'installation, pas au moment du premier incident. Dans les deux cas, pg_dump reste le recours portable, et c'est celui que le tableau ci-dessus donne.

Le contrôleur de stockage n'est pas fourni par ce dépôt non plus

Aucun manifeste de ce dépôt n'installe de contrôleur de stockage : core/platform-access/ ne porte que l'Ingress de l'interface Longhorn, pas son installation. Le nom longhorn apparaît pourtant dans la fiche client, en valeur par défaut de TV_STORAGE_CLASS : c'est le nom de la StorageClass par défaut que preflight.sh s'attend à trouver déjà posée sur le cluster, pas une installation que le guide effectuerait. La sauvegarde des volumes se configure donc dans le contrôleur retenu par le client. Si c'est Longhorn : sa console permet aussi de supprimer des volumes et n'a aucune authentification par défaut. Poser la basic-auth de Observabilité avant de l'ouvrir à quiconque.

Ce qu'une sauvegarde ne prouve pas

Une restauration qui n'a jamais été exécutée est une hypothèse, pas une sauvegarde. L'exercer au moins une fois avant la mise en service, sur un namespace ou un cluster jetable, et noter la durée mesurée : c'est le seul chiffre de temps de reprise dont on disposera le jour où la question se posera pour de vrai. La recette de conformité de Validation n'exerce ni les sauvegardes ni leur restauration, et ne prétend pas le faire.

Renouvellement des certificats

En régime nominal, il n'y a rien à faire. Let's Encrypt émet pour quatre-vingt-dix jours et cert-manager renouvelle aux deux tiers de la durée de vie, soit autour du soixantième jour.

Le contrôle manuel

kubectl get certificate -A
openssl s_client -connect ${TV_VIP}:443 -servername hub.${TV_DOMAIN} </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -enddate -ext subjectAltName

C'est la seconde commande qui compte. Elle lit ce que Traefik présente réellement sur le fil, pas ce que le cluster croit détenir. Un objet Certificate en READY=True et un certificat périmé servi aux navigateurs sont deux états parfaitement compatibles. La date à surveiller est celle rendue par -enddate, pas la colonne READY.

Le symptôme d'un renouvellement bloqué

Il n'y en a aucun, pendant jusqu'à soixante jours. C'est ce qui rend cette panne particulière : la cause et le symptôme sont séparés par deux mois. Rien dans les journaux, rien dans les évènements, rien dans les tableaux de bord, jusqu'au jour où le certificat expire et où tous les hosts deviennent injoignables d'un coup, sans contournement navigateur possible si ${TV_DOMAIN} est sur la liste HSTS preload.

Trois causes couvrent la quasi totalité des cas :

  • Les deux arguments DNS-01 ont été effacés du Deployment cert-manager, typiquement par un kubectl apply du manifeste amont, qui ne les porte pas. Symptôme, visible uniquement en inspectant le challenge : Could not determine the zone for "_acme-challenge.${TV_DOMAIN}.": ... SERVFAIL.
  • Le jeton API Cloudflare a été révoqué, a expiré, ou a perdu ses portées. Il lui faut Zone:DNS:Edit et Zone:Zone:Read sur la zone du client.
  • cert-manager antérieur à v1.16.4 avec Cloudflare. Le challenge passe, puis le nettoyage du TXT échoue en DELETE "/zones//dns_records/<id>", erreur 7003, et l'Order reste pending avec un challenge valid. Cloudflare ne renvoie plus zone_id dans ses enregistrements ; les versions plus anciennes en dépendaient. Monter de version suffit, le nettoyage en attente passe au rollout : core/cert-manager/README.md.

Après avoir corrigé la cause, forcer une nouvelle émission sans attendre le report exponentiel :

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

Le détail du diagnostic est à Troubleshooting, entrée 1.

Ce qui le signale désormais

Quatre règles couvrent les certificats sur l'instance de référence, dans le groupe certificats de observability/prometheus-values.yaml : Certificate non Ready, renouvellement en retard de plus de sept jours, expiration à moins de sept jours, et disparition des métriques certmanager_*. La dernière existe parce que les trois autres lisent des métriques collectées par des annotations que le chart de cert-manager pose sur son propre pod : une montée de version qui les déplace rendrait les trois premières muettes sans rien casser de visible. Le contrôle manuel ci-dessus reste utile avant une mise en service, il n'est plus le seul filet.

Mettre à jour un service

Le cas courant

kubectl set image deployment/<nom> <conteneur>=${TV_REGISTRY}/<image>:<tag> -n <zone>
kubectl rollout status deployment/<nom> -n <zone>

Retour arrière : kubectl rollout undo deployment/<nom> -n <zone>.

Ce qu'il faut voir. rollout status qui rend successfully rolled out ne suffit pas : il dit que le déploiement s'est terminé, pas que le nouveau contenu tourne.

kubectl get pods -n <zone> -l app=<nom> \
  -o jsonpath='{range .items[*]}{.spec.containers[0].image}{"\n"}{end}'
kubectl get pods -n <zone> -l app=<nom>

L'image effectivement en cours est celle attendue, et les pods sont 1/1 et le restent sur plusieurs minutes.

Un tag identique rend la mise à jour invérifiable et le retour arrière impossible

La plupart des images applicatives de ce dépôt portent le tag latest. Avec un tag inchangé, rollout status réussit sans qu'aucune preuve n'existe que le nouveau contenu a été tiré, et rollout undo ramène la même référence, donc rien. Publier chaque version sous un tag distinct, ou forcer imagePullPolicy: Always en sachant que le retour arrière restera impossible.

Changer une valeur de configuration

La valeur se change dans Vault, jamais dans le Secret Kubernetes, qui est réécrit à la synchronisation suivante. Joindre Vault depuis le poste demande d'abord un port-forward et une authentification, comme à l'étape 6.2 du Pas-à-pas : l'interface Vault n'a aucun enregistrement DNS public et le client vault ne sait pas parler à un Service Kubernetes tout seul.

kubectl port-forward -n vault svc/vault 8200:8200 &
export VAULT_ADDR=http://127.0.0.1:8200
vault login
vault kv patch -mount=secrets prod/<service> CLE=valeur
kubectl rollout restart deployment/<nom> -n <zone>

patch, jamais put sur un chemin partagé. Et le redémarrage n'est pas optionnel : les variables d'environnement sont figées au démarrage du conteneur. Sans lui, le Secret porte la nouvelle valeur et les pods en cours continuent de servir avec l'ancienne, sans qu'aucune erreur ne le signale.

Le cas particulier des portails

Les variables NEXT_PUBLIC_* des trois portails Next.js sont résolues au moment où l'image est construite. Aucune modification de Deployment, de Secret ou de variable d'environnement ne les atteint, et un retour arrière restaure une image qui porte l'ancienne valeur.

Conséquence opérationnelle : tout changement de ${TV_DOMAIN} ou d'une URL publique impose de reconstruire les trois images de portail avant le déploiement, pas après. Ce qu'on observe si on l'oublie : les pods sont Running, Ready, sans erreur, et seules les requêtes émises depuis le navigateur partent vers l'ancienne adresse. Le diagnostic complet est à Troubleshooting, entrée 8.

Ajouter un service

Cinq choses à écrire, aucune facultative. Les quatre premières sont des manifests, la dernière est ce qui empêche le nouveau service de devenir invisible pour la personne qui exploitera la plateforme après vous.

# À créer Où Ce qu'il porte, et le piège
1 Deployment apps/api/<service>/ ou apps/web/<portail>/ image sur ${TV_REGISTRY}, imagePullSecrets: acr-pull-secret (qu'aucun manifeste ne crée, il se pose à la main), envFrom sur le Secret produit par l'ExternalSecret, et des sondes de disponibilité et de vivacité distinctes, chacune sur un chemin direct qui ne redirige pas et ne s'authentifie pas
2 Service même répertoire ClusterIP, avec le nom exact auquel l'Ingress renvoie
3 Ingress même répertoire host <service>.${TV_DOMAIN}, sans spec.tls : le certificat vient du TLSStore par défaut qui sert le wildcard. Un Ingress qui déclare son propre spec.tls oblige à dupliquer le Secret wildcard, et sa clé privée, dans le namespace
4 ExternalSecret même répertoire, un seul fichier ClusterSecretStore/vault-store, dataFrom: extract sur prod/<service>, creationPolicy: Owner, deletionPolicy: Retain, refreshInterval: 1m. Un kubectl apply sur un répertoire applique par ordre alphabétique et la dernière définition l'emporte en silence : deux fichiers pour un même objet est un piège, pas une redondance
5 L'entrée dans la matrice de configuration Configuration service, namespace, chemin Vault, runtime. C'est le tableau que l'on relit avant d'écrire dans Vault

Rien à ajouter à la recette de validation, et c'est voulu. scripts/validate-install.sh ne porte aucune liste de hosts écrite à la main : le contrôle 8 dérive la sienne des manifestes Ingress du dépôt, moins les hosts routés par le tunnel. Poser l'Ingress de la ligne 3 suffit donc à faire entrer le nouveau host dans la recette. Le nombre de lignes ok du contrôle 8 augmente d'une unité : c'est ce qui prouve que l'ajout a pris.

Une sixième chose a existé jusqu'au 2026-09-10 : monter une ConfigMap de CA interne, dont la clé et la variable d'environnement dépendaient du runtime. Elle a disparu avec le CA interne. Un nouveau service n'a plus rien à monter pour faire confiance à *.${TV_DOMAIN} : la chaîne Let's Encrypt est déjà dans le magasin par défaut de son image.

Deux décisions qui ne sont pas des fichiers

Public ou interne ? Par défaut interne : aucun enregistrement DNS public, aucune route de tunnel. Trois noms seulement sont publics. Depuis l'abandon du TLD interne, ce cloisonnement ne repose plus que sur cette discipline, et son seul garde-fou automatique est le contrôle 8 de la recette, d'où la ligne 5 du tableau ci-dessus. Rendre un host public est une décision, pas une conséquence : elle se prend explicitement, et elle ajoute un enregistrement DNS et une route de tunnel, jamais l'un sans l'autre.

Les limites de ressources couvrent-elles la pointe ? Une limite CPU ne provoque pas d'erreur, elle provoque de la lenteur, invisible dans l'état des objets Kubernetes. Pour toute charge de calcul par rafales (optimisation d'images, génération de PDF, compression, cryptographie), la limite doit couvrir la pointe, pas la moyenne ; les requests restent le levier de la réservation. Et jamais d'autoscaler sur la mémoire pour un runtime à ramasse-miettes (.NET, JVM, Node) : il monte jusqu'à son maximum quelle que soit la charge réelle.

Vérifier après l'ajout

kubectl get externalsecret <nom> -n <zone>            # SecretSynced
kubectl get pods -n <zone> -l app=<nom>               # 1/1, stable
kubectl get endpoints <service> -n <zone>             # l'IP du pod y figure
dig +short <service>.${TV_DOMAIN} @1.1.1.1                 # aucune reponse si le host est interne
curl -sSI https://<service>.${TV_DOMAIN}                   # depuis un poste qui resout vers la VIP

L'endpoint est le contrôle qui manque le plus souvent : un pod Running dont l'IP n'apparaît pas dans les endpoints est un pod que Traefik ne voit pas, et l'host répondra 404.

Ce qu'il faut surveiller au quotidien

Deux tableaux de bord versionnés portent l'essentiel, tous deux à importer à la main dans Grafana, décrits à Observabilité : tradeval-red répond au débit, au taux d'erreur et à la latence par service, sur la seule source Traefik ; tradeval-ressources répond à la question de savoir si le cluster est le facteur limitant.

Ce que ces deux tableaux ne montrent pas doit être regardé autrement. Le tableau ci-dessous ne liste que des choses qui, sur cette plateforme, ne se signalent nulle part ailleurs.

Quoi Où le lire Ce qui doit alerter Pourquoi ce n'est visible qu'ici
Latence par service tableau tradeval-red une latence qui monte sans que le taux d'erreur bouge une limite CPU trop basse ne produit ni erreur, ni redémarrage, ni évènement : seule la latence bouge
Pods Running mais non Ready kubectl get pods -A tout pod 0/1 depuis plus de quelques minutes aucun tableau de bord classique ne le montre, ils comptent les pods Running. C'est ce défaut qui a laissé un portail en 404 pendant trois heures
Expiration du certificat wildcard la commande openssl ci-dessus moins de trente jours restants un renouvellement bloqué ne se voit qu'au renouvellement suivant, jusqu'à soixante jours après sa cause
État de scellement de Vault kubectl exec -n vault vault-0 -- vault status Sealed: true rien d'autre ne le signale ; la recette ne le voit qu'indirectement, par les ExternalSecret, et seulement s'ils resynchronisent
État des ExternalSecret kubectl get externalsecret -A tout autre chose que SecretSynced deletionPolicy: Retain transforme la disparition d'une clé côté Vault en dérive silencieuse : le Secret survit à sa source, et l'écart ne se voit que là
Redémarrages des apiservers et de Traefik kubectl get pods -n kube-system -o wide tout compteur RESTARTS qui monte avant l'incident de saturation, les apiservers cumulaient plusieurs redémarrages en deux mois, en régime réputé normal
Mémoire des control-planes kubectl top nodes au-delà de 80 % à 2 Gio ils tournaient à plus de 100 % en régime normal, ce qui ne se voit dans aucun symptôme applicatif jusqu'à la saturation
Hosts internes non publiés contrôle 8 de Validation tout host interne qui résout publiquement seul garde-fou automatique du cloisonnement public/interne

Une ligne de ce tableau tombe en silence si la source de métriques est muette

kubectl top nodes et les valeurs des HPA viennent de metrics-server, posé à l'étape 10 du Pas-à-pas. S'il ne sert plus de métriques, kubectl top ne rend rien et les HPA affichent <unknown> : la ligne « Mémoire des control-planes » ci-dessus ne surveille alors plus rien, et l'opérateur ne s'en aperçoit pas, puisque son absence de résultat ressemble à une absence de problème. Le contrôler en même temps que le reste, diagnostic à Troubleshooting, entrée 13.

Trente-deux règles sont posées, et le destinataire est une vraie boîte

Chaque ligne de ce tableau a désormais une règle qui la double : redémarrages, pods non prêts, mémoire des noeuds, latence, 5xx, cibles de collecte disparues. Le contrôle manuel garde sa valeur avant une mise en service, il n'est plus le seul filet. Les alertes partent par le SMTP du client vers TV_ACME_EMAIL, une fois le destinataire écrit dans le receiver (page Observabilité). Vérifier une fois, après l'installation, qu'un courriel arrive réellement : le plus simple est la commande amtool alert add de la page Observabilité, ou de lire le journal d'Alertmanager (kubectl -n monitoring logs sts/prometheus-alertmanager) : un notify retry répété y désigne le SMTP, et rien d'autre ne le signalera.

Avant la mise en service : ce qui reste à trancher avec le client

Le courrier sortant se vérifie de bout en bout, pour chacun des trois émetteurs

trade-notification-svc, Keycloak et Alertmanager envoient chacun avec leur propre configuration (étapes 11 et 14 du pas-à-pas, page Observabilité). Un SMTP mal réglé ne produit aucune erreur visible de l'utilisateur : une inscription qui n'envoie pas son courriel, une réinitialisation de mot de passe qui n'arrive jamais, une alerte qui ne part pas. Avant la mise en service, provoquer un envoi réel par chaque chemin et le voir arriver dans une boîte, puis vérifier chez le fournisseur que le domaine d'expéditeur est vérifié (DKIM, SPF) : sans cela, les courriels partent et finissent en indésirables.

Trois autres points doivent être clos avant de déclarer l'instance en service, et aucun n'est couvert par la recette de conformité :

  • La procédure de descellement et de sauvegarde de Vault, écrite et exercée, avec les quatre questions de l'encart en tête de page.
  • La restauration, exercée au moins une fois, durée mesurée notée.
  • L'authentification des interfaces d'exploitation. Grafana porte son login intégré et Vault son authentification par jeton ; les trois autres, Prometheus, Alertmanager et la console de stockage, sont exposées sans le moindre mot de passe, et la dernière permet de supprimer des volumes. La recette basic-auth est à Observabilité.