infra: le déploiement pose MLflow, le collecteur et les migrations #114
No reviewers
Labels
No labels
Compat/Breaking
EC01
EC02
EC03
EC04
EC05
EC06
Kind/BDD
Kind/Back
Kind/Bug
Kind/CICD
Kind/Cloud
Kind/Contenu
Kind/Data
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Front
Kind/IA
Kind/Infra
Kind/Monitoring
Kind/Security
Kind/Testing
Portée/Post-jury
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
ops/alerte
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
g2/enervision!114
Loading…
Reference in a new issue
No description provided.
Delete branch "lenaic/113-deploiement-complet"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Ce que ça change
Le déploiement pose désormais MLflow, le collecteur et les migrations. Une mise
en production redevient un geste, au lieu d'une liste de rappels.
Closes #113
Le constat de départ. Le clone du serveur datait du 2 septembre à 14 h 06.
Tout ce qui a été fusionné depuis — PostgreSQL sur Docker, MLflow, les sept
tables de la zone or, le collecteur — n'existait que dans
develop, et ce quitourne sur la machine y avait été posé à la main, service par service. Rien de
cela ne se rejouait.
Les trois lots
MLflow entre dans
app_stacks. Il tournait parce que quelqu'un l'avaitlancé. Sa composition ne porte aucune interpolation
${...}et son fichier desecrets est en
0600 root:root, parce qu'il contient les identifiants MinIOdédiés du #30 : le lui passer par
docker composeferait échouer la tâche,deployn'y ayant pas accès. D'où un drapeauenvpar pile.Avec, une garde qui refuse de démarrer une pile dont le fichier de secrets
manque. Mieux vaut échouer là qu'au démarrage du conteneur : un
env_fileabsent fait partir MLflow avec un boto3 sans identifiants, qui se rabat sur le
vrai AWS S3 et met deux minutes à renoncer sans dire où il est allé. Le
piège est déjà documenté dans sa composition, autant l'attraper avant.
Le collecteur obtient ce que l'ADR 0008 annonçait comme « le ticket
suivant » : un environnement Python isolé, le bit d'exécution sur ses
lanceurs, et ses lignes de crontab.
L'environnement est isolé et non installé sur le système parce qu'Ubuntu 24.04
refuse l'installation globale (PEP 668). Les lanceurs prennent leur interpréteur
par
ENERVISION_PYTHON, avecpython3en repli pour un lancement à la main.Le rôle ne planifie que les lanceurs réellement présents dans le dépôt.
Ceux du #107 arriveront avec lui et seront ramassés d'eux-mêmes au déploiement
suivant ; une ligne de crontab pointant vers un fichier absent échouerait chaque
minute en silence, dans un journal que personne ne lit.
Les migrations en attente sont appliquées sur
enervision_prodetenervision_preprod, sous le rôle applicatif et jamais souspostgres: unetable posée en superutilisateur échappe à la clause de droits par défaut du
schéma et devient invisible de Grafana sans le moindre message. Le piège s'est
déjà refermé une fois, il est décrit dans
docs/POSTGRESQL.md.Le mot de passe passe par
PGPASSWORDet non par le DSN : une ligne de commandeest lisible par n'importe qui via
psle temps de son exécution.Preuve
Joué en local, exactement comme la chaîne le fait :
L'exécution réelle sur le serveur reste à jouer, c'est la mise en production
elle-même.
docs/runbooks/deploiement.mdporte la vérification qui compte, etelle ne se triche pas : le nombre d'objets sous
endpoint=currentdoitaugmenter tout seul entre deux relevés espacés de trois minutes.
Ce qui reste manuel après ça
mlflow.env, qui n'est pas rendu depuis le coffre. Il porte desidentifiants MinIO créés à la main lors du #30, que le coffre ne connaît pas.
Le déploiement échoue proprement s'il manque, avec le message qui dit quoi
faire.
délibérément hors du déploiement continu.
infra/compose/.C'est le dernier vrai écart au « fusionner suffit », et il est d'une autre
taille. Il mérite son ticket.
Relecture
Ce qui suit le code
docs/runbooks/deploiement.mdcomplété : ce que le déploiement pose, cequi reste manuel, comment vérifier que la collecte est repartie, et quoi
faire quand les migrations échouent sous
no_logOù regarder en priorité
La tâche des migrations. C'est la seule qui écrit dans une base de production, et
le choix du rôle qui les applique n'est pas cosmétique.
Relu en entier : le rôle
app, les deux compositions concernées,db/migrate.py, le lanceur et le runbook. Le ticket vise juste et les trois lots sont les bons — la distinction--env-file/env_file:, lechanged_whensurstderrplutôt que surstdout, la reprise de propriété avant les migrations : ce sont de vraies trouvailles, et les commentaires qui les portent valent d'être gardés tels quels.Mais trois défauts empêchent le déploiement d'atteindre son objectif, et l'un d'eux annule le critère central du #113. D'où la demande de changements ; chacun tient en quelques lignes.
Bloquants
1. La pile MLflow ne démarrera pas :
deployne peut pas liremlflow.envLa prémisse du drapeau
envest fausse. Unenv_file:déclaré dans un service n'est pas lu par le démon Docker — il est lu par le clientdocker compose, qui en injecte le contenu dans la configuration du conteneur avant d'appeler l'API. Vérifié :Or
infra/compose/mlflow/docker-compose.yml:69déclare/etc/enervision/mlflow.env, quegenere-identifiants.sh:243-244pose en0600 root:root, et « Démarrer les piles » tourne enbecome_user: deploy. La tâche échouera sur l'itemmlflow.Le compose le dit d'ailleurs à rebours : si
docker inspectmontre les variables lues parenv_file, c'est bien le client qui les y a mises. Le commentaire devars.yml:30-32est donc à réécrire, etenv: falsene résout rien du problème qu'il prétend résoudre.2. La garde verrouille toute reconstruction depuis zéro
La garde est posée avant « Démarrer les piles » et couvre les trois piles d'un coup. Sur une machine neuve :
mlflow.envn'existe pas ;genere-identifiants.shexigemccontre MinIO déjà démarré (lignes 38-48) ;Ni PostgreSQL, ni MinIO, ni les migrations, ni la collecte. C'est exactement le scénario « une réinstallation du serveur perdrait la collecte » que le ticket veut fermer.
Les points 1 et 2 se règlent ensemble : rendre
mlflow.envdepuis le coffre en0640 root:deploy, commepostgres.envetminio.env. Le seul obstacle est que les identifiants MinIO dédiés du #30 n'y sont pas encore — c'est un ajout au coffre, pas un problème de conception. À défaut, évaluer la garde pile par pile, juste avant le démarrage de la pile concernée.3.
ENERVISION_RACINEn'est jamais posé : la relève échouera chaque minuteLa ligne de crontab produite est :
Mais
collecte-current.sh:12faitRACINE="${ENERVISION_RACINE:-/opt/enervision}"puiscd "$RACINE/services/collector"— soit/opt/enervision/services/collector, qui n'existe pas :app_repo_dirvaut{{ app_root }}/.repo. Avecset -eu, lecdéchoue et le script sort. Et cecdn'est pas décoratif :pipn'installe que les dépendances, pas le paquetcollector, doncpython -m collector.currentne résout son paquet que par le répertoire courant.Le défaut est d'autant plus net que la même passe a ajouté
ENERVISION_PYTHONà ce script sans traiterENERVISION_RACINE, qui a exactement le même statut.Correctif : ajouter
ENERVISION_RACINE={{ app_repo_dir }}à la ligne de crontab.C'est ce point qui fait tomber le critère « le nombre d'objets sous
endpoint=currentaugmente entre deux relevés » — la vérification que la demande présente elle-même, à juste titre, comme la seule qui compte.Importants
4. Le mot de passe passe en clair dans l'argv de
docker exec. « Aligner le mot de passe » construitpsql … -c "alter role x with password '…'": cette chaîne est un argument du clientdockersur l'hôte, lisible par n'importe qui viapsle temps de son exécution. C'est le piège que le dépôt évite partout ailleurs en le disant —10-roles-bases-droits.shavec\getenv,genere-identifiants.shavec l'entrée standard — et c'est l'argument même que la demande avance pour choisirPGPASSWORDcôté migrations. Les deux tâches devraient tenir la même ligne. →docker exec -i+argv:+ le SQL surstdin:du modulecommand.Nuance sur
PGPASSWORD, pendant qu'on y est : le mot de passe n'échappe pas aupsde la cible. Le mot-cléenvironment:d'Ansible est réalisé en préfixant la commande du module, exécutée par/bin/sh -c 'PGPASSWORD=… /usr/bin/python …'. L'exposition n'est pas supprimée, elle est déplacée. Leno_logreste justifié ; la justification écrite ne l'est qu'à moitié.5. La réparation de droits est incomplète : il manque
create on database.run_migrationscommence parcreate schema if not exists ops(services/api/enervision_api/db.py:23-29). PostgreSQL vérifieACL_CREATEsur la base avant le court-circuitIF NOT EXISTS—schemacmds.cle dit explicitement en commentaire. Surenervision_prodle rôle est propriétaire de la base et l'a implicitement ; mais le constat porté par la demande (« surenervision_preprod, il n'avait pascreatesurpublic») signale que cette base n'a pas été créée par ce chemin. Si elle ne lui appartient pas, la première migration échoue surpermission denied for database, sousno_log. → ajoutergrant create on database {{ item.nom }} to {{ item.role }}à la tâche de réparation.6. Rien n'attend que PostgreSQL soit disponible.
docker_compose_v2sanswait: truerend la main dès les conteneurs créés. Sur un volume neuf, initdb +CREATE EXTENSION timescaledbsur deux bases prend du temps ; les tâches intercalées (apt, pip) achètent quelques dizaines de secondes, ce qui suffira le plus souvent — et échouera un jour enthe database system is starting up, sousno_log. →until: … rc == 0avecretriessur le premierdocker exec psql, ouwait: truesur la pile postgres.7. Idempotence non tenue — c'est un critère d'acceptation explicite.
collecte-current.shest commité en100644. La tâche pose0755, mais la tâchegiten amont aforce: true:git status --porcelainvoit le changement de mode, le module conclut à des modifications locales et remet0644. Chaque exécution rapportera « changed » sur le clone et sur lechmod. → le correctif propre supprime la tâche :git update-index --chmod=+x services/collector/bin/*.sh, et le dépôt livre les lanceurs exécutables.Mineurs et documentation
collector_paquetsest mort. Défini envars.yml:72-76, jamais lu : la tâchepiputiliseapp_deps.stdout_lines. Conséquence de fond :psycopg, dont les migrations dépendent, n'arrive dans le venv que parce que lepyproject.tomlde l'API le déclare. Le jour où l'API change de client, les migrations cassent sans que rien ne relie les deux. Soit on supprime la variable, soit on l'ajoute à la liste installée — ce qui semble l'intention d'origine.rattrapage-readings.sh:vars.yml:83-85annonce qu'il sera « ramassé de lui-même au déploiement suivant », mais il n'est pas danscollector_taches. Le commentaire promet plus que le code.journal:en dur (/var/log/enervision/collector.log) alors quecollector_log_direst défini deux lignes plus haut. Ansible résout les auto-références sans difficulté./var/log/enervision/*.log, alimentés chaque minute, sur un disque de 63 Go partagé à six groupes. Une entréelogrotate, ou l'admettre comme écart ouvert dans le runbook..forgejo/workflows/deploy.yml:117:for chemin in postgres .n'a pas été mis à jour avecmlflow, alors que son commentaire dit reprendreapp_stacks. La preuve d'après-déploiement ne montrera pas la pile que ce ticket ajoute.ps— ce que la tâche évite délibérément. ÉcrirePGPASSWORD=… python …/migrate.py "postgresql://enervision_prod@127.0.0.1:5433/enervision_prod". Et ligne 132, «no_log, qui protège le mot de passe présent dans le DSN » contredit le commentaire du code, qui explique justement qu'il n'y est pas.changed_when: falsesur les deux tâches SQL : une dérive réellement corrigée (mot de passe réaligné, table réattribuée) reste invisible au compte rendu. Défendable pour rester idempotent au rapport, mais le runbook devrait alors dire comment constater qu'une correction a eu lieu.pg_tablesne couvre ni vues, ni vues matérialisées, ni séquences autonomes. Aucun cas dansdb/migrations/aujourd'hui, mais une vue ajoutée demain échapperait silencieusement à la réparation — le symptôme exact que la tâche existe pour empêcher.pg_classfiltré surrelkindserait durable. À vérifier aussi : la propagation dealter table … owner toaux chunks TimescaleDB depublic.mesure.Ce qui est juste et que je garderais tel quel
changed_when: "'application de ' in app_migrations.stderr": le registre est bien disponible par itération dans une boucle,loggingécrit bien sur stderr, et chercherApplieddans stdout aurait effectivement rendu la tâche éternellement « ok ». Juste, et non trivial.env_files: >- {{ [...] if … else omit }}fonctionne : Ansible retire l'argument dont la valeur vaut le jetonomit, y compris rendu par une expression. Le mécanisme est bon — c'est sa justification qui ne l'est pas (point 1).ALTER DEFAULT PRIVILEGES FOR ROLE, et la tâche est rejouable.state: absentpour les autres : la boucle se nettoie toute seule.Critères d'acceptation du #113
mlflowdansapp_stackset démarré par le playbookLes points 1 à 3 corrigés, l'exécution réelle sur le serveur départagera — et c'est bien la preuve
endpoint=currentqui tranche, comme la demande le dit elle-même.d9829ef84d39e5494837Relecture juste de bout en bout, et sur le point 1 tu as démonté ma prémisse
centrale, pas un détail. Tout est traité, poussé.
Les trois bloquants
1.
env_fileest lu par le client, pas par le démon. Je croyais l'inverse, et toutmon drapeau
envreposait là-dessus. La pile MLflow aurait échoué. Le rôle rendmaintenant les fichiers de secrets des piles lisibles par le groupe
deploy, et lecommentaire de
vars.ymldit la vérité au lieu du contraire. Rendremlflow.envdepuisle coffre reste la bonne cible ; il lui manque les identifiants MinIO dédiés du #30, donc
c'est un ajout au coffre et son propre ticket.
2. La garde verrouillait la reconstruction. Ton scénario est imparable :
genere-identifiants.sha besoin de MinIO démarré, et la garde arrêtait le playbookavant MinIO. Une pile sans secrets est désormais sautée avec un avertissement, pas
fatale : le socle part, et le déploiement suivant la ramasse. Le runbook écrit l'ordre
d'une reconstruction.
3.
ENERVISION_RACINE. Tu as raison sur les deux points, y compris celui que jen'aurais pas vu : le
cdn'est pas décoratif,pipn'installe pas le paquetcollector.La même passe avait ajouté
ENERVISION_PYTHONsans traiter son jumeau. Ajouté à la lignede crontab.
Les importants
4. Le SQL passe par
stdinavecargv:, plus rien dans la ligne de commande. Tu asraison de dire que les deux tâches devaient tenir la même ligne. Et ta nuance sur
PGPASSWORDest exacte :environment:est réalisé en préfixant la commande, l'expositionest déplacée et non supprimée. Le runbook le dit maintenant, au lieu de laisser croire
l'inverse.
5.
create on databaseajouté. Le court-circuit duIF NOT EXISTSvérifié après ledroit sur la base, je ne le savais pas.
6. Attente explicite par
pg_isready,untilet 30 tentatives, au lieu du pari surles secondes achetées par les tâches intercalées.
7. Idempotence. Ton diagnostic est le bon :
force: truevoyait le changement de modeet le remettait. La tâche
chmoddisparaît, et le dépôt livre les trois lanceurs en100755.Les mineurs
collector_paquetsétait bien mort, et ta remarque de fond porte plus loin que lavariable :
psycopgn'arrivait dans le venv que par lepyprojectde l'API. Il estmaintenant nommé là où il sert, dans
collector_paquets_migrations, ajouté à ce quedéclarent les services.
Le commentaire sur
rattrapage-readings.shpromettait en effet plus que le code — maisc'est le commentaire qui avait tort : le rattrapage se lance à la demande, sur une
profondeur choisie, le planifier n'aurait pas de sens. Réécrit.
journal:passe parcollector_log_dir. Une entréelogrotateest posée, quotidienne,sept jours.
deploy.ymlcouvremlflowdans sa preuve. Le runbook ne dit plus queno_logprotège un mot de passe qui n'est pas dans le DSN, et il explique commentconstater qu'une réparation a eu lieu, puisque les deux tâches SQL rapportent
ok.pg_tablesdevientpg_classfiltré surrelkind, avec l'alterqui convient pour uneséquence, une vue ou une vue matérialisée. Sur les chunks TimescaleDB :
alter tablesurl'hypertable propage aux chunks, mais je le vérifierai à l'exécution réelle plutôt que de
l'affirmer ici.
Vérifié
yamllint,ansible-lintau profil production, les trois contrôles de syntaxe. Et sur leserveur, les deux gestes que ta relecture a changés : le SQL par
stdinpasse, etpg_isreadyrépond.L'exécution réelle départagera, et c'est bien la croissance de
endpoint=currentquitranche — tu as raison de la remettre au centre, c'est le seul critère qui ne se triche
pas.
Relu après le correctif
39e5494: le rôleapp,vars.yml, les deux compositions,db/migrate.py,deps-services.py, le lanceur et le runbook. La relecture d'Olivier était juste et les corrections vont dans le bon sens. Mais le correctif laisse passer un bloquant et introduit deux scories.Bloquant restant — le « saut » de MLflow n'est pas câblé
Le correctif remplace la garde
assertpar undebug, mais « Démarrer les piles » boucle toujours surapp_stackssans filtre (roles/app/tasks/main.yml:186).Sur une machine neuve, sans
mlflow.env:mlflow;docker composelitservices.mlflow.env_file: [/etc/enervision/mlflow.env]depuis le fichier de composition (indépendamment de--env-file/ du drapeauenv), le fichier est absent → la tâche échoue, sansfailed_when;postgresetminiosont montés (items précédents), mais tout ce qui suit — venv, crontab, propriété, migrations — ne tourne pas.On retombe sur le scénario du point 2 d'Olivier, décalé de quelques tâches, et le runbook décrit un « premier déploiement qui saute MLflow » qui, lui, se solde par un échec non nul.
Correctif : filtrer le démarrage sur la présence du secret.
À vérifier au passage :
mlflowaenv: falsemais sa composition exige quand mêmemlflow.env(sonenv_file:interne). La condition de saut pourmlflowreste donc «mlflow.envprésent », pas «env: true».Importants
1.
when: item.name != 'minio' or true(main.yml:148) — tautologie (X or trueest toujours vrai), manifestement un reliquat d'édition. À supprimer.2.
failed_when: falsesur lechmoddes fichiers de secrets (main.yml:149) — la tâche « Rendre lisibles par le groupe » avale toute erreur, sur des fichiers sensibles. Si lechmodéchoue en silence,mlflow.envreste0600 root:rootet la pile MLflow échoue plus loin avec le message boto3 → vrai AWS S3 que ce ticket veut précisément éviter, sans rien dans la sortie. Mieux : placer lestatavant, puiswhen: item.stat.existssansfailed_when.3. « Attendre que PostgreSQL réponde » fait doublon. « Démarrer les piles » porte déjà
wait: true+wait_timeout(présent surdevelop), et la pilepostgresa un healthcheckpg_isready -p 5433. Le module bloque donc déjà jusqu'à healthy. Le point 6 d'Olivier était en réalité déjà couvert — autant le dire en commentaire, ou retirer la tâcheuntil.4. Première exécution des migrations : l'état du registre est une hypothèse.
run_migrationsse fie àops.schema_migrations. Les migrations0007+sont idempotentes (create table if not exists) et la reprise de propriété passe avant — le risque est donc contenu. Mais0001_auth.sqlfaitcreate table ops.userssansif not exists: si une migration ancienne a été appliquée à la main sans passer parmigrate.py, elle n'est pas dans le registre et la tâche échoue surrelation already exists, sousno_log. À confirmer sur le serveur réel avant merge :select version from ops.schema_migrationssurenervision_prodetenervision_preprodreflète-t-il tout ce qui est réellement en place ?Mineurs
collector_paquets(vars.yml:83) est toujours mort. Le correctif a ajoutécollector_paquets_migrations(utilisé) mais laissé l'ancien à côté, avecpsycopgdupliqué. Deux variables au nom quasi identique, une seule lue → à supprimer.PGPASSWORD(main.yml:~483, « un environnement de processus ne l'est que par son propriétaire et par root ») garde la formulation qu'Olivier a nuancée. Le runbook est corrigé, pas ce commentaire —environment:est réalisé ensh -c 'PGPASSWORD=… …', visible enpssur la cible.mlflow.envde0600 root:rootà0640 root:deployà chaque passage, en silence. Cohérent avecpostgres.env(même mode, mot de passe superutilisateur), mais ça contredit la « décision C » du #30 citée dans la composition. Une ligne dans le runbook, et prévenir qui a lancégenere-identifiants.sh.debugn'est pas « bruyant ». Dans un log de CI il défile. Pour une pile non démarrée en déploiement continu, envisager unfailconditionné au contexte CD, ou au moins un marqueur::warning::.no_log: unrescue:qui imprime un message générique (« migrations en échec sur {{ item.nom }}, voir runbook ») sans exposer de secret rendrait le CD lisible sans commande manuelle.public.mesure:alter table … owner tone propage pas aux chunks dans toutes les versions. Déjà noté comme « à vérifier à l'exécution » — à garder ouvert explicitement, c'est le symptôme exact (invisible de Grafana) que la tâche existe pour empêcher.Ce qui est bon
env_files … else omit,changed_whensurstderr, SQL parstdin,grant create on database,pg_class/relkindà la place depg_tables,ENERVISION_RACINEsur la ligne de crontab, la rotation des journaux,rattrapage-readings.shretiré decollector_taches: tout ça est juste.Verdict :
REQUEST_CHANGES. Le bloquant (démarrage non filtré) annule encore le critère « une reconstruction depuis zéro remonte la collecte ». Il tient en trois lignes. Les points importants 1–2 sont du même acabit.Relu à
badc710, après la rebase surdevelop(le #111 est passé au milieu, ma revue précédente est devenue périmée). Sept points sur dix de la première passe sont réellement corrigés, et deux d'entre eux mieux que ce que je proposais —wait: true+wait_timeoutsur les piles vaut mieux que la seule attentepg_isready, et surrattrapage-readings.shtu as raison contre moi : c'est le commentaire qui mentait, planifier un rattrapage à profondeur choisie n'aurait aucun sens.Mais il reste deux bloquants, et le premier est de moi.
1. La boucle
pg_classéchoue sur les séquences d'identité — régressionC'est ma remarque sur
pg_tablesqui l'a introduite, et je ne l'avais pas vue venir. Rejouée telle quelle sur un PostgreSQL 16 neuf, en reproduisant0001et0012:ALTER SEQUENCE … OWNER TOest refusé sur une séquence liée à une colonne, quel que soit son propriétaire actuel — le test ne porte pas sur le propriétaire mais sur le lien. Quatre migrations posentbigint generated always as identity:0001,0002,0012,0014. Les deux bases sont donc touchées, avecON_ERROR_STOP=1, sur la tâche même qui existe pour réparer la propriété — et elle tourne avant les migrations, qui ne s'appliquent plus.La version
pg_tablesfonctionnait, précisément parce qu'elle ne voyait pas les séquences.Le correctif, vérifié dans la foulée : écarter les séquences liées.
ALTER TABLE … OWNER TOles emporte de toute façon, donc rien n'est perdu.Avec ça la boucle passe, et les séquences appartiennent bien au rôle applicatif à l'arrivée — donc le
attendu : 0du runbook tient toujours.2. La garde est neutralisée, mais la boucle des piles n'est pas filtrée
main.yml:186boucle toujours surapp_stacksentier. Or unenv_fileabsent fait sortirdocker composeen 1 :Donc sur une machine neuve : le
debugaffiche « pile mlflow NON démarrée », puis la pile est tentée quand même, la tâche échoue et le playbook s'arrête. PostgreSQL et MinIO partent — c'est un vrai progrès sur l'état d'avant — mais venv, crontab, migrations et collecte sont sautés.Deux conséquences à corriger ensemble :
mlflow.envmanque.Il manque un
whensurapp_secrets_piles.results, ou unloopfiltré.Sur
badc710— le réalignement de grafanaLe fond est juste et la vérification est sérieuse : le contre-essai qui montre qu'un mauvais mot de passe est bien rejeté est exactement ce qu'il fallait faire pour ne pas confondre un refus avec un artefact. J'ai vérifié la #115 :
supervision.env.j2rendPG_GRAFANA_PASSWORDetDATA_SOURCE_PASSdepuisvault_pg_grafana_password, etdatasources.ymll'interpole. Sans ce réalignement, la source de données etpostgres-exporteréchouent bien tous les deux au démarrage. Le besoin est réel.Trois choses à traiter quand même.
Le commentaire dit maintenant le contraire du code.
main.yml:339-341, juste au-dessus de la tâche :La boucle vaut désormais
collector_bases + collector_roles_lecture, etgrafanay est.docs/runbooks/deploiement.md:92-94porte la même phrase, au mot près. Dans un dépôt dont toute la discipline est que le commentaire porte la raison du choix, un commentaire qui interdit ce que la ligne suivante fait est pire que pas de commentaire — c'est celui-là qu'on lira en panne. La raison a changé (le fichier de secrets de grafana vient du coffre avec la #115, contrairement à celui de mlflow) : c'est elle qu'il faut écrire, aux deux endroits.Séquencement avec la #115. Elle est ouverte, pas fusionnée, et elle touche les deux mêmes fichiers —
roles/app/tasks/main.ymletgroup_vars/all/vars.yml. Le conflit est certain, autant décider maintenant laquelle passe devant. Et une question que tu es seul à pouvoir trancher : y a-t-il aujourd'hui un Grafana installé à la main sur le serveur ? Le dépôt n'en porte aucune trace avant la #115, donc probablement non — mais s'il y en a un, il utilise le mot de passe actuel du serveur, et ce réalignement le coupe de sa base jusqu'à ce que la #115 arrive. C'est le risque exact que ton commentaire d'origine nommait.pg_monitormérite une phrase de plus. « Le rôle reste en lecture seule sur les données métier » est exact pour les tables, maispg_monitordonne aussipg_stat_activity.query— le texte des requêtes des autres sessions, qui porte des valeurs littérales : une adresse dans unwhere, les valeurs d'uninsert into mesure. Ce n'est pas une table, donc la phrase reste vraie, et l'échange vaut probablement le coup pour la #115. Mais avec un Grafana exposé, ça se décide et ça s'écrit dans le manuel plutôt que de passer pour de la plomberie.Restes de la passe précédente
main.yml:152:when: item.name != 'minio' or trueest une tautologie — reliquat d'édition.ansible-lintne l'attrape pas.collector_paquetsest toujours mort.collector_paquets_migrationsa été ajouté à côté sans que l'ancien parte : deux listes,psycopgdéclaré dans les deux, une seule lue. C'est le doublon que la remarque visait à supprimer.mlflow.env. Le rôle force0640 root:deploy, maisdocs/runbooks/mlflow.md:15,infra/compose/mlflow/docker-compose.yml:5etgenere-identifiants.sh:244disent toujours0600 root. L'élargissement est le bon choix, mais il change la posture — les identifiants MinIO dédiés du #30 deviennent lisibles par le groupedeploy— et ça doit être écrit là où le contraire est encore affirmé.main.yml:424: « sur les deux tâches SQL » — elles sont trois depuisbadc710.collector_roles_lectureporte le préfixe du collecteur sans avoir de rapport avec lui. Détail.Sur les preuves
yamllint,ansible-lintau profil production et les contrôles de syntaxe passent, et je te crois — mais aucun des trois défauts ci-dessus n'est atteignable par un linter : une condition tautologique, une boucle non filtrée et une erreur SQL à l'exécution leur sont invisibles. Ce qui départage reste l'exécution réelle, et la croissance deendpoint=currentderrière.Les deux bloquants tiennent en quelques lignes chacun, et le SQL de remplacement ci-dessus est déjà testé.
Vous avez raison tous les deux, et sur le même point : mon « saut » de MLflow n'était
pas câblé. J'avais remplacé la garde par un avertissement en croyant avoir aussi filtré
la boucle de démarrage. Mon remplacement n'avait rien trouvé à remplacer et je ne l'ai pas
vérifié. La pile était donc tentée malgré l'avertissement,
docker composesortait en 1sur son
env_file:absent, et tout ce qui suit ne tournait pas. On retombait exactementsur le scénario du point 2 d'Olivier, décalé de quelques tâches, avec un runbook qui
décrivait un saut qui n'existait pas.
La boucle porte maintenant sur
app_secrets_piles.resultsavecwhen: item.stat.exists.Et la condition est bien la présence du secret, pas le drapeau
env— Gabriel a raisonde le souligner, MLflow a
env: falseet exige pourtant son fichier.Olivier, ta régression sur les séquences est de moi, pas de toi. C'est ma réponse à ta
remarque sur
pg_tablesqui l'a introduite, et ton diagnostic est exact :alter sequence … owner toporte sur le lien, pas sur la propriété. Vérifié sur la préproduction avant depousser :
Les quatre que tu annonçais, et le
attendu : 0du runbook tient toujours puisquealter table … owner toles emporte. J'ai repris tonpg_dependtel quel.Gabriel, tes deux scories étaient bien des scories de mon correctif. La tautologie
X or trueest un reliquat d'édition, retirée. Et tu as raison sur lefailed_when: false: avaler toute erreur sur des fichiers de secrets aurait laissémlflow.enven0600 root:rootet fait échouer la pile bien plus loin, avec le message boto3 vers levrai AWS S3 que ce ticket veut précisément éviter. Seule l'absence du fichier est
tolérée maintenant, le reste remonte.
Deux choses que j'ajoute, trouvées en relisant la #115.
Le mot de passe du rôle
grafanadans le coffre était refusé par le serveur, même dériveque pour les rôles applicatifs. Ses deux consommateurs de la #115 le lisent : la source de
données et
postgres-exporter. Il entre dans le réalignement.mlflowreste volontairementdehors, il tourne et lit son mot de passe dans
mlflow.env.Et
grafanadevient membre depg_monitor. Mesuré avant et après : les requêtes lisiblesdans
pg_stat_activitypassent de 0 sur 23 à 23 sur 23.Sur ce que vous validez tous les deux, je garde tel quel :
changed_whensurstderr,les migrations sous le rôle applicatif, la reprise de propriété qui les précède, et le fait
de ne planifier que les lanceurs présents.
Et Olivier, merci d'avoir écrit que sur
rattrapage-readings.shc'est le commentaire quimentait. Tu aurais pu me le compter comme une erreur, tu as regardé ce que le code devait
faire plutôt que ce que j'avais écrit.
La chaîne repasse. L'exécution réelle départagera, et c'est bien
endpoint=currentquitranche : à cette minute il contient 0 objet, parce que le clone du serveur date du
2 septembre 14 h 06 et ne connaît pas encore le collecteur.
Relu à
414bacc. Mes deux bloquants sont bien corrigés : l'exclusionpg_dependest reprise telle quelle, et la boucle de démarrage porte maintenant surapp_secrets_piles.resultsavecwhen: item.stat.exists. Rejoué sous ansible-core 2.15 : le filtrage fonctionne,mlflowest sauté et le playbook continue. La réécriture enitem.item.*et l'expressionenv_filessur deux lignes tiennent aussi.Mais le correctif du
failed_whenréintroduit le blocage, quelques tâches plus tôt.main.yml:156-158teste le message d'erreur :Le module
filene dit pas « does not exist ». Exécuté sur un fichier absent :La condition est donc vraie et la tâche échoue. Sur une machine neuve, le playbook meurt sur ce
chmodavant lestat, avant l'avertissement et avant la boucle filtrée : tout le dispositif « sauter et continuer » n'a jamais l'occasion de tourner. C'est le scénario du point 2, déplacé en amont — celui-là même que ta réponse décrit.Le correctif, testé : lire l'état plutôt que le message, il est déjà dans le résultat.
Avec ça,
postgrespasse,mlflowest toléré puis sauté, la suite s'exécute. L'intention de Gabriel est préservée : toute autre erreur remonte.À faire avant de merger, non bloquant
collector_paquetsest toujours mort (vars.yml:83) :collector_paquets_migrationsa été ajouté à côté sans que l'ancien parte, etpsycopgest déclaré dans les deux.main.yml:357etdocs/runbooks/deploiement.md:92interdisent toujours par écrit — « surtout pasmlflownigrafana» — le réalignement degrafanaque le code fait maintenant. La raison a changé et c'est elle qu'il faut écrire, aux deux endroits.docs/runbooks/mlflow.md:15etinfra/compose/mlflow/docker-compose.yml:5annoncent encoremlflow.enven0600 rootalors que le rôle le pose en0640 root:deploy. L'élargissement est le bon choix, mais il change la posture — les identifiants MinIO dédiés du #30 deviennent lisibles par le groupedeploy— et ça doit être écrit là où le contraire est encore affirmé.main.yml:442parle de « deux tâches SQL » ; elles sont trois depuisbadc710.Sur
pg_monitorLe besoin est réel, j'ai vérifié la #115 :
supervision.env.j2rendPG_GRAFANA_PASSWORDetDATA_SOURCE_PASSdepuis le coffre, sans le réalignement la source de données etpostgres-exporteréchouent tous deux au démarrage. Une phrase à ajouter au manuel quand même :pg_monitordonne aussi le texte des requêtes des autres sessions danspg_stat_activity, qui porte des valeurs littérales. « Lecture seule sur les données métier » reste vrai pour les tables ; l'échange vaut probablement le coup, mais il se décide et il s'écrit.Le critère qui reste
endpoint=currentà 0 objet, tu le dis toi-même. Une ligne à changer, puis l'exécution réelle : c'est elle qui tranche, et rien dans la relecture ne la remplacera.I got it
Relu à
c20fc38.f2dd1c2ne me laisse plus rien à redire sur le code. Lefailed_whenteste l'état, les trois contradictions écrit/code sont levées avec la raison réécrite plutôt que rafistolée,collector_paquetsest parti, et tu as vérifié qu'aucun Grafana ne tourne sur le serveur avant d'ajouter le réalignement — c'était ma question ouverte, elle est fermée.Je ne trouve plus de défaut de correction dans la logique Ansible. Ce qui suit n'est plus du même ordre.
Le banc d'essai ne protège pas ce qu'il annonce
Joué avec
ansible-core 2.21.3, la version derequirements-ci.txt: dix tâches au vert, ta mesure est exacte. J'ai ensuite réintroduit les trois défauts dansmain.yml— lefailed_whenfautif,ENERVISION_RACINEretiré de la ligne de crontab, la boucle de démarrage dé-filtrée — et rejoué :Aucun des trois. Quatre contrôles sur cinq rejouent une copie de l'expression, écrite dans le banc :
env_fileset la ligne de crontab sont réécrits dansvars:, le filtre des piles est reconstruit sur des données locales, et lefailed_whenest recopié sur une tâchefiledu banc. Seule la détection de tautologie lit vraiment le rôle, parlookup('file', ...).C'est donc une documentation exécutable des trois défauts passés — ce qui a de la valeur — mais pas un garde-fou contre leur retour, alors que le message de commit (« il rejoue les expressions du rôle ») et le commentaire de l'étape CI (« Ce playbook les exécute ») affirment l'inverse. Un banc qui passe au vert sur un rôle cassé est pire qu'une absence de banc : le prochain lui fera confiance.
Le correctif est celui que tu appliques déjà au cinquième contrôle : lire
main.ymlet éprouver ce qui y est écrit, pas une copie.Deux détails au passage :
ansible-core ≤ 2.18:sans_env is not sequenceest faux tant qu'omitest une chaîne, et une chaîne est une séquence en Jinja. Sans effet sur la chaîne, qui épingle 2.21.3 — mais un poste sur une version plus ancienne verra un faux rouge.sans_env == omitserait insensible à la version ;infra/compose/mlflow/genere-identifiants.sh:217,243-244écrit toujours/etc/enervisionen0700 root:rootetmlflow.enven0600 root:root, ce qui contredit la doc quef2dd1c2vient d'aligner. Le rôle rattrape au déploiement suivant, mais c'est ce script-là qu'un exploitant lance à la main.Pourquoi je n'approuve pas encore
Le code est bon. Ce qui manque est le critère d'acceptation du #113 lui-même : « une exécution complète est jouée sur le serveur », « le playbook est idempotent : deux exécutions de suite », et le décompte bronze à trois minutes d'écart. Aucun des trois n'a été fait, et
endpoint=currentest à 0 objet.Ce n'est pas une exigence de forme. En quatre tours, le raisonnement a paru solide et l'exécution a tranché autrement à chaque fois : la prémisse sur
env_file, la boucle non filtrée, lefailed_whenqui testait le mauvais champ, et maintenant un banc au vert sur un rôle cassé. Une demande dont le sujet est « le déploiement fonctionne » ne peut pas entrer sans avoir été déployée — et le rôle écrit dans deux bases de production, réaligne des mots de passe et change des propriétaires d'objets. La première exécution mérite d'être regardée.Et elle est atteignable maintenant, sans merger :
deploy.ymlporte unworkflow_dispatchdont l'en-tête dit exactement à quoi il sert — « valider la chaîne hors d'un merge, la première fois notamment ». Il passe-e repo_version="${{ github.sha }}", donc il déploie la révision choisie.Ce qu'il me faut pour approuver : la sortie des deux exécutions, la seconde sans
changedsur le cron ni sur les paquets, et les deux relevés du décompte bronze. Le banc peut être corrigé dans la foulée ou dans son propre ticket — il n'empêche rien, il rassure à tort. Dès que la preuve est là, j'approuve.Tu avais raison de refuser sans exécution. Elle est faite, deux fois, par
workflow_dispatchsur cette branche et sans fusionner.Le critère qui tranche
Sept sites par minute, trois minutes, vingt et un objets. La zone bronze grossit d'elle-même
depuis 16 h 15, après trois jours à zéro.
Le journal de la relève, sur le serveur :
Et la ligne de crontab telle qu'elle est posée, avec la variable dont l'absence faisait
sortir la relève chaque minute :
État des deux bases après le déploiement
enervision_prodenervision_preprodLe
attendu : 0du runbook tient : ton exclusionpg_dependfonctionne, etalter table … owner toemporte bien les quatre séquences d'identité.Idempotence : partielle, et je le dis plutôt que de l'arrondir
Ce qui est rejouable sans effet :
cronpose par nom ;Ce qui ne l'est pas, et ce n'est pas de mon fait :
postgresetmlflowsont recréés à chaque passage.ev-postgresaffichait« Up About a minute » après la seconde exécution. La cause est
build: 'always' if item.build else 'never'dans la boucle de démarrage, antérieure à cette demande :les deux piles ont
build: true, donc l'image est reconstruite et le conteneurrecréé à chaque déploiement.
Recréer la base à chaque déploiement n'est pas anodin — le volume survit, mais les
connexions tombent. Ça mérite son ticket plutôt qu'un correctif glissé ici : le choix
build: alwaysa sans doute une raison que je ne connais pas, et c'est le rôle deGabriel.
Et MLflow a démarré
Il tourne maintenant par le playbook, alors qu'il n'était dans aucune pile déployée avant
cette demande. C'est le critère 1 du #113, celui que tu avais vu tomber en premier avec
l'histoire du
env_filelu côté client.Le banc d'essai
Refait après ta démonstration. Il lit maintenant
main.ymlau lieu d'en recopier lesexpressions, avec les accolades neutralisées à la lecture — sans ça Ansible retemplate et
l'assertion porte sur le résultat au lieu du texte, ce qui a fait repasser deux contrôles
au vert pendant que je les écrivais.
Éprouvé dans les deux sens : vert sur le rôle sain, rouge sur les cinq défauts
réintroduits un par un. Mon script d'injection a dû être corrigé deux fois lui-même — il
frappait la mauvaise occurrence d'une expression présente deux fois, et son vert ne
prouvait rien. C'est exactement le piège que tu avais nommé.
Le reste de tes remarques est traité :
genere-identifiants.shpose/etc/enervisionen0710 root:deployetmlflow.enven0640, au lieu de contredire la documentation.