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 :
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 :
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é :
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.envest 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 applydu 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:EditetZone:Zone:Readsur 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>", erreur7003, et l'Orderrestependingavec un challengevalid. Cloudflare ne renvoie pluszone_iddans 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é.