infra: le déploiement pose MLflow, le collecteur et les migrations #114

Merged
lenaic merged 13 commits from lenaic/113-deploiement-complet into develop 2026-09-03 14:34:07 +00:00
Owner

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 qui
tourne 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'avait
lancé. Sa composition ne porte aucune interpolation ${...} et son fichier de
secrets est en 0600 root:root, parce qu'il contient les identifiants MinIO
dédiés du #30 : le lui passer par docker compose ferait échouer la tâche,
deploy n'y ayant pas accès. D'où un drapeau env par 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_file
absent 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, avec python3 en 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_prod et
enervision_preprod, sous le rôle applicatif et jamais sous postgres : une
table 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 PGPASSWORD et non par le DSN : une ligne de commande
est lisible par n'importe qui via ps le temps de son exécution.

Preuve

Joué en local, exactement comme la chaîne le fait :

$ yamllint .
(rien)

$ ansible-lint
Passed: 0 failure(s), 0 warning(s) in 38 files processed of 43 encountered.
Profile 'moderate' was required, but 'production' profile passed.

$ ansible-playbook bootstrap.yml --syntax-check    OK
$ ansible-playbook site.yml      --syntax-check    OK
$ ansible-playbook restore.yml   --syntax-check    OK

L'exécution réelle sur le serveur reste à jouer, c'est la mise en production
elle-même. docs/runbooks/deploiement.md porte la vérification qui compte, et
elle ne se triche pas : le nombre d'objets sous endpoint=current doit
augmenter 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 des
    identifiants 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.
  • Le socle système — pare-feu, durcissement SSH, moteur Docker — qui reste
    délibérément hors du déploiement continu.
  • L'API et le tableau de bord, qui n'ont pas de pile dans infra/compose/.
    C'est le dernier vrai écart au « fusionner suffit », et il est d'une autre
    taille. Il mérite son ticket.

Relecture

  • Un pair a relu et laissé un commentaire, même court
  • Ses remarques sont traitées, ou une réponse explique pourquoi elles ne le sont pas

Ce qui suit le code

  • docs/runbooks/deploiement.md complété : ce que le déploiement pose, ce
    qui reste manuel, comment vérifier que la collecte est repartie, et quoi
    faire quand les migrations échouent sous no_log

Où 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.

## 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 qui tourne 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'avait lancé. Sa composition ne porte aucune interpolation `${...}` et son fichier de secrets est en `0600 root:root`, parce qu'il contient les identifiants MinIO dédiés du #30 : le lui passer par `docker compose` ferait échouer la tâche, `deploy` n'y ayant pas accès. D'où un drapeau `env` par 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_file` absent 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`, avec `python3` en 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_prod` et `enervision_preprod`, **sous le rôle applicatif et jamais sous `postgres`** : une table 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 `PGPASSWORD` et non par le DSN : une ligne de commande est lisible par n'importe qui via `ps` le temps de son exécution. ## Preuve Joué en local, exactement comme la chaîne le fait : ``` $ yamllint . (rien) $ ansible-lint Passed: 0 failure(s), 0 warning(s) in 38 files processed of 43 encountered. Profile 'moderate' was required, but 'production' profile passed. $ ansible-playbook bootstrap.yml --syntax-check OK $ ansible-playbook site.yml --syntax-check OK $ ansible-playbook restore.yml --syntax-check OK ``` L'exécution réelle sur le serveur reste à jouer, c'est la mise en production elle-même. `docs/runbooks/deploiement.md` porte la vérification qui compte, et elle ne se triche pas : le nombre d'objets sous `endpoint=current` doit augmenter 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 des identifiants 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. - **Le socle système** — pare-feu, durcissement SSH, moteur Docker — qui reste délibérément hors du déploiement continu. - **L'API et le tableau de bord**, qui n'ont pas de pile dans `infra/compose/`. C'est le dernier vrai écart au « fusionner suffit », et il est d'une autre taille. Il mérite son ticket. ## Relecture - [ ] Un pair a relu et laissé un commentaire, même court - [ ] Ses remarques sont traitées, ou une réponse explique pourquoi elles ne le sont pas ## Ce qui suit le code - [x] `docs/runbooks/deploiement.md` complété : ce que le déploiement pose, ce qui reste manuel, comment vérifier que la collecte est repartie, et quoi faire quand les migrations échouent sous `no_log` ## Où 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.
lenaic self-assigned this 2026-09-03 11:27:16 +00:00
infra: le déploiement pose MLflow, le collecteur et les migrations
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 48s
Intégration / Tests unitaires et couverture (pull_request) Successful in 53s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 13s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 17s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m27s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m6s
355c914d43
Le clone du serveur datait du 2 septembre à 14 h 06. Tout ce qui a été fusionné
depuis n'existait que dans develop, et ce qui tourne sur la machine y avait été
posé à la main, service par service. Ce lot rend le déploiement complet pour la
chaîne de données.

MLflow entre dans app_stacks. Il tournait parce que quelqu'un l'avait lancé ;
une réinstallation le perdait. Sa composition ne porte aucune interpolation, et
son fichier de secrets est en 0600 root:root parce qu'il contient des
identifiants MinIO dédiés : le lui passer par docker compose ferait échouer la
tâche, deploy n'y ayant pas accès. D'où le drapeau « env » par pile, et une
garde qui refuse de démarrer une pile dont les secrets manquent — mieux vaut
échouer là qu'au démarrage du conteneur, où un env_file absent envoie boto3
vers le vrai AWS S3 pendant deux minutes sans le dire.

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). Le rôle ne
planifie que les lanceurs réellement présents dans le dépôt : ceux du #107
seront ramassés d'eux-mêmes au déploiement suivant, et une ligne de crontab
pointant vers un fichier absent échouerait chaque minute en silence.

Les migrations en attente sont appliquées sur les deux bases, sous le rôle
applicatif et jamais sous postgres : une table posée en superutilisateur
échappe à la clause de droits par défaut et devient invisible de Grafana sans
le moindre message. Le mot de passe passe par PGPASSWORD plutôt que par le DSN,
une ligne de commande étant lisible dans ps.

Vérifié en local : yamllint propre, ansible-lint au profil production, et les
trois playbooks passent le contrôle de syntaxe.
infra: aligne les identifiants et la propriété avant de migrer
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 48s
Intégration / Tests unitaires et couverture (pull_request) Successful in 53s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 12s
Intégration / Images épinglées par version (pull_request) Successful in 2s
Intégration / Tableau de bord (pull_request) Successful in 24s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m26s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 57s
bc453172e3
Relecture critique de ma propre demande avant de la proposer. Trois défauts,
tous vérifiés sur le serveur, dont deux qui l'auraient fait échouer au premier
passage.

LES MOTS DE PASSE DU COFFRE ÉTAIENT REFUSÉS par les deux rôles applicatifs. Le
modèle postgres.env n'est lu qu'à la première initialisation du volume : passé
ce moment, le coffre et le serveur dérivent sans que rien ne le signale. Une
reconstruction depuis zéro aurait produit un serveur auquel personne ne se
connecte avec les identifiants documentés. Le rôle réaligne désormais les deux
rôles de collector_bases, et surtout pas mlflow ni grafana, qui lisent leur mot
de passe ailleurs.

LA PROPRIÉTÉ DES OBJETS N'ÉTAIT PAS AU RÔLE APPLICATIF. Sur enervision_prod les
trois tables appartenaient à postgres et le rôle n'avait pas create sur ops ;
sur enervision_preprod il ne l'avait pas sur public. Aucune migration n'y serait
passée. Le geste est celui de db/migrations/README.md, qui note à juste titre
qu'il ne peut pas vivre dans une migration jouée par le rôle applicatif : sa
place est ici, en superutilisateur, et il est rejouable.

LES DÉPENDANCES ÉTAIENT INCOMPLÈTES. migrate.py passe par enervision_api.db, qui
lit la configuration de l'API au niveau module : psycopg seul ne suffit pas, il
manquait pydantic-settings et le reste. Plutôt que de courir après les
dépendances transitives, le rôle lit les pyproject.toml des services par le
script que la chaîne d'intégration utilise déjà pour la même question. Une
dépendance ajoutée à un service arrive au déploiement suivant sans que personne
n'y pense.

Deux corrections plus petites. Le changed_when des migrations cherchait
« Applied » dans stdout ; migrate.py journalise en français et sur stderr, la
tâche serait restée éternellement « ok ». Et la variable de boucle du bloc
PL/pgSQL doit être déclarée, ce que PL/pgSQL n'infère pas dans un bloc anonyme.

Séquence jouée en entier sur le serveur : 14 migrations appliquées sur la
production, 8 sur la préproduction, code 0, et un second passage ne fait rien.
Les deux bases portent maintenant leurs onze tables et leurs quinze migrations.

yamllint, ansible-lint au profil production et les trois contrôles de syntaxe
passent.
docs: la fiche 0008 n'annonce plus comme manuel ce que le rôle pose
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 53s
Intégration / Tests unitaires et couverture (pull_request) Successful in 55s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 13s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 21s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m24s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m0s
d9829ef84d
Sa section « Conséquences » disait que la ligne de crontab et le déploiement du
lanceur étaient posés à la main et que les porter dans le rôle Ansible était le
ticket suivant. C'est ce ticket, et il est fait. Une fiche qui décrit un état
révolu se retourne contre celui qui s'y fie.
lenaic requested review from gabriel 2026-09-03 12:34:31 +00:00
olivier requested changes 2026-09-03 12:38:27 +00:00
Dismissed
olivier left a comment

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:, le changed_when sur stderr plutôt que sur stdout, 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 : deploy ne peut pas lire mlflow.env

La prémisse du drapeau env est fausse. Un env_file: déclaré dans un service n'est pas lu par le démon Docker — il est lu par le client docker compose, qui en injecte le contenu dans la configuration du conteneur avant d'appeler l'API. Vérifié :

$ chmod 000 secret.env && docker compose config
open .../secret.env: permission denied

Or infra/compose/mlflow/docker-compose.yml:69 déclare /etc/enervision/mlflow.env, que genere-identifiants.sh:243-244 pose en 0600 root:root, et « Démarrer les piles » tourne en become_user: deploy. La tâche échouera sur l'item mlflow.

Le compose le dit d'ailleurs à rebours : si docker inspect montre les variables lues par env_file, c'est bien le client qui les y a mises. Le commentaire de vars.yml:30-32 est donc à réécrire, et env: false ne 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.env n'existe pas ;
  • pour le produire, genere-identifiants.sh exige mc contre MinIO déjà démarré (lignes 38-48) ;
  • mais MinIO ne démarre pas : la garde a arrêté le playbook avant.

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.env depuis le coffre en 0640 root:deploy, comme postgres.env et minio.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_RACINE n'est jamais posé : la relève échouera chaque minute

La ligne de crontab produite est :

ENERVISION_PYTHON=/opt/enervision/venv/bin/python \
  /opt/enervision/.repo/services/collector/bin/collecte-current.sh >> …

Mais collecte-current.sh:12 fait RACINE="${ENERVISION_RACINE:-/opt/enervision}" puis cd "$RACINE/services/collector" — soit /opt/enervision/services/collector, qui n'existe pas : app_repo_dir vaut {{ app_root }}/.repo. Avec set -eu, le cd échoue et le script sort. Et ce cd n'est pas décoratif : pip n'installe que les dépendances, pas le paquet collector, donc python -m collector.current ne 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 traiter ENERVISION_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=current augmente 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 » construit psql … -c "alter role x with password '…'" : cette chaîne est un argument du client docker sur l'hôte, lisible par n'importe qui via ps le 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.sh avec \getenv, genere-identifiants.sh avec l'entrée standard — et c'est l'argument même que la demande avance pour choisir PGPASSWORD côté migrations. Les deux tâches devraient tenir la même ligne. → docker exec -i + argv: + le SQL sur stdin: du module command.

Nuance sur PGPASSWORD, pendant qu'on y est : le mot de passe n'échappe pas au ps de 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. Le no_log reste justifié ; la justification écrite ne l'est qu'à moitié.

5. La réparation de droits est incomplète : il manque create on database. run_migrations commence par create schema if not exists ops (services/api/enervision_api/db.py:23-29). PostgreSQL vérifie ACL_CREATE sur la base avant le court-circuit IF NOT EXISTSschemacmds.c le dit explicitement en commentaire. Sur enervision_prod le rôle est propriétaire de la base et l'a implicitement ; mais le constat porté par la demande (« sur enervision_preprod, il n'avait pas create sur public ») signale que cette base n'a pas été créée par ce chemin. Si elle ne lui appartient pas, la première migration échoue sur permission denied for database, sous no_log. → ajouter grant create on database {{ item.nom }} to {{ item.role }} à la tâche de réparation.

6. Rien n'attend que PostgreSQL soit disponible. docker_compose_v2 sans wait: true rend la main dès les conteneurs créés. Sur un volume neuf, initdb + CREATE EXTENSION timescaledb sur 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 en the database system is starting up, sous no_log. → until: … rc == 0 avec retries sur le premier docker exec psql, ou wait: true sur la pile postgres.

7. Idempotence non tenue — c'est un critère d'acceptation explicite. collecte-current.sh est commité en 100644. La tâche pose 0755, mais la tâche git en amont a force: true : git status --porcelain voit le changement de mode, le module conclut à des modifications locales et remet 0644. Chaque exécution rapportera « changed » sur le clone et sur le chmod. → 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_paquets est mort. Défini en vars.yml:72-76, jamais lu : la tâche pip utilise app_deps.stdout_lines. Conséquence de fond : psycopg, dont les migrations dépendent, n'arrive dans le venv que parce que le pyproject.toml de 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-85 annonce qu'il sera « ramassé de lui-même au déploiement suivant », mais il n'est pas dans collector_taches. Le commentaire promet plus que le code.
  • journal: en dur (/var/log/enervision/collector.log) alors que collector_log_dir est défini deux lignes plus haut. Ansible résout les auto-références sans difficulté.
  • Aucune rotation pour /var/log/enervision/*.log, alimentés chaque minute, sur un disque de 63 Go partagé à six groupes. Une entrée logrotate, ou l'admettre comme écart ouvert dans le runbook.
  • .forgejo/workflows/deploy.yml:117 : for chemin in postgres . n'a pas été mis à jour avec mlflow, alors que son commentaire dit reprendre app_stacks. La preuve d'après-déploiement ne montrera pas la pile que ce ticket ajoute.
  • Runbook : la commande de rattrapage (lignes 135-139) remet le mot de passe dans le DSN, donc dans ps — ce que la tâche évite délibérément. Écrire PGPASSWORD=… 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: false sur 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_tables ne couvre ni vues, ni vues matérialisées, ni séquences autonomes. Aucun cas dans db/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_class filtré sur relkind serait durable. À vérifier aussi : la propagation de alter table … owner to aux chunks TimescaleDB de public.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 chercher Applied dans 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 jeton omit, y compris rendu par une expression. Le mécanisme est bon — c'est sa justification qui ne l'est pas (point 1).
  • Appliquer les migrations sous le rôle applicatif, et la reprise de propriété qui la précède : c'est la bonne lecture d'ALTER DEFAULT PRIVILEGES FOR ROLE, et la tâche est rejouable.
  • Ne planifier que les lanceurs présents, avec state: absent pour les autres : la boucle se nettoie toute seule.

Critères d'acceptation du #113

Critère État
mlflow dans app_stacks et démarré par le playbook ✗ échouera (1)
Dépendances Python installées par le rôle
Lignes de crontab posées ✓ posées, ✗ inopérantes (3)
Lanceurs exécutables ✓, mais non idempotent (7)
Migrations appliquées, échec bloquant ✓ sous réserve de (5)
Playbook idempotent ✗ (7)
Exécution réelle + croissance de la zone bronze ✗ non jouée, et ne peut pas passer en l'état
Runbook complété ✓ (deux corrections ci-dessus)

Les points 1 à 3 corrigés, l'exécution réelle sur le serveur départagera — et c'est bien la preuve endpoint=current qui tranche, comme la demande le dit elle-même.

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:`, le `changed_when` sur `stderr` plutôt que sur `stdout`, 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 : `deploy` ne peut pas lire `mlflow.env` La prémisse du drapeau `env` est fausse. Un `env_file:` déclaré dans un service **n'est pas lu par le démon Docker** — il est lu par le *client* `docker compose`, qui en injecte le contenu dans la configuration du conteneur avant d'appeler l'API. Vérifié : ``` $ chmod 000 secret.env && docker compose config open .../secret.env: permission denied ``` Or `infra/compose/mlflow/docker-compose.yml:69` déclare `/etc/enervision/mlflow.env`, que `genere-identifiants.sh:243-244` pose en `0600 root:root`, et « Démarrer les piles » tourne en `become_user: deploy`. La tâche échouera sur l'item `mlflow`. Le compose le dit d'ailleurs à rebours : si `docker inspect` montre les variables lues par `env_file`, c'est bien le client qui les y a mises. Le commentaire de `vars.yml:30-32` est donc à réécrire, et `env: false` ne 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.env` n'existe pas ; - pour le produire, `genere-identifiants.sh` exige `mc` **contre MinIO déjà démarré** (lignes 38-48) ; - mais MinIO ne démarre pas : la garde a arrêté le playbook avant. 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.env` depuis le coffre en `0640 root:deploy`, comme `postgres.env` et `minio.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_RACINE` n'est jamais posé : la relève échouera chaque minute La ligne de crontab produite est : ``` ENERVISION_PYTHON=/opt/enervision/venv/bin/python \ /opt/enervision/.repo/services/collector/bin/collecte-current.sh >> … ``` Mais `collecte-current.sh:12` fait `RACINE="${ENERVISION_RACINE:-/opt/enervision}"` puis `cd "$RACINE/services/collector"` — soit `/opt/enervision/services/collector`, qui **n'existe pas** : `app_repo_dir` vaut `{{ app_root }}/.repo`. Avec `set -eu`, le `cd` échoue et le script sort. Et ce `cd` n'est pas décoratif : `pip` n'installe que les *dépendances*, pas le paquet `collector`, donc `python -m collector.current` ne 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 traiter `ENERVISION_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=current` augmente 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 » construit `psql … -c "alter role x with password '…'"` : cette chaîne est un argument du client `docker` sur l'hôte, lisible par n'importe qui via `ps` le 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.sh` avec `\getenv`, `genere-identifiants.sh` avec l'entrée standard — et c'est l'argument même que la demande avance pour choisir `PGPASSWORD` côté migrations. Les deux tâches devraient tenir la même ligne. → `docker exec -i` + `argv:` + le SQL sur `stdin:` du module `command`. *Nuance sur `PGPASSWORD`, pendant qu'on y est :* le mot de passe n'échappe pas au `ps` de 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. Le `no_log` reste justifié ; la justification écrite ne l'est qu'à moitié. **5. La réparation de droits est incomplète : il manque `create on database`.** `run_migrations` commence par `create schema if not exists ops` (`services/api/enervision_api/db.py:23-29`). PostgreSQL vérifie `ACL_CREATE` sur la base **avant** le court-circuit `IF NOT EXISTS` — `schemacmds.c` le dit explicitement en commentaire. Sur `enervision_prod` le rôle est propriétaire de la base et l'a implicitement ; mais le constat porté par la demande (« sur `enervision_preprod`, il n'avait pas `create` sur `public` ») signale que cette base n'a pas été créée par ce chemin. Si elle ne lui appartient pas, la première migration échoue sur `permission denied for database`, sous `no_log`. → ajouter `grant create on database {{ item.nom }} to {{ item.role }}` à la tâche de réparation. **6. Rien n'attend que PostgreSQL soit disponible.** `docker_compose_v2` sans `wait: true` rend la main dès les conteneurs créés. Sur un volume neuf, initdb + `CREATE EXTENSION timescaledb` sur 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 en `the database system is starting up`, sous `no_log`. → `until: … rc == 0` avec `retries` sur le premier `docker exec psql`, ou `wait: true` sur la pile postgres. **7. Idempotence non tenue — c'est un critère d'acceptation explicite.** `collecte-current.sh` est commité en `100644`. La tâche pose `0755`, mais la tâche `git` en amont a `force: true` : `git status --porcelain` voit le changement de mode, le module conclut à des modifications locales et remet `0644`. Chaque exécution rapportera « changed » sur le clone **et** sur le `chmod`. → 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_paquets` est mort.** Défini en `vars.yml:72-76`, jamais lu : la tâche `pip` utilise `app_deps.stdout_lines`. Conséquence de fond : `psycopg`, dont les migrations dépendent, n'arrive dans le venv que parce que le `pyproject.toml` de 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-85` annonce qu'il sera « ramassé de lui-même au déploiement suivant », mais il n'est pas dans `collector_taches`. Le commentaire promet plus que le code. - **`journal:` en dur** (`/var/log/enervision/collector.log`) alors que `collector_log_dir` est défini deux lignes plus haut. Ansible résout les auto-références sans difficulté. - **Aucune rotation** pour `/var/log/enervision/*.log`, alimentés chaque minute, sur un disque de 63 Go partagé à six groupes. Une entrée `logrotate`, ou l'admettre comme écart ouvert dans le runbook. - **`.forgejo/workflows/deploy.yml:117`** : `for chemin in postgres .` n'a pas été mis à jour avec `mlflow`, alors que son commentaire dit reprendre `app_stacks`. La preuve d'après-déploiement ne montrera pas la pile que ce ticket ajoute. - **Runbook** : la commande de rattrapage (lignes 135-139) remet le mot de passe dans le DSN, donc dans `ps` — ce que la tâche évite délibérément. Écrire `PGPASSWORD=… 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: false`** sur 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_tables`** ne couvre ni vues, ni vues matérialisées, ni séquences autonomes. Aucun cas dans `db/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_class` filtré sur `relkind` serait durable. À vérifier aussi : la propagation de `alter table … owner to` aux chunks TimescaleDB de `public.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 chercher `Applied` dans 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 jeton `omit`, y compris rendu par une expression. Le mécanisme est bon — c'est sa justification qui ne l'est pas (point 1). - Appliquer les migrations sous le rôle applicatif, et la reprise de propriété qui la précède : c'est la bonne lecture d'`ALTER DEFAULT PRIVILEGES FOR ROLE`, et la tâche est rejouable. - Ne planifier que les lanceurs présents, avec `state: absent` pour les autres : la boucle se nettoie toute seule. ## Critères d'acceptation du #113 | Critère | État | |---|---| | `mlflow` dans `app_stacks` et démarré par le playbook | ✗ échouera (1) | | Dépendances Python installées par le rôle | ✓ | | Lignes de crontab posées | ✓ posées, ✗ inopérantes (3) | | Lanceurs exécutables | ✓, mais non idempotent (7) | | Migrations appliquées, échec bloquant | ✓ sous réserve de (5) | | Playbook idempotent | ✗ (7) | | Exécution réelle + croissance de la zone bronze | ✗ non jouée, et ne peut pas passer en l'état | | Runbook complété | ✓ (deux corrections ci-dessus) | Les points 1 à 3 corrigés, l'exécution réelle sur le serveur départagera — et c'est bien la preuve `endpoint=current` qui tranche, comme la demande le dit elle-même.
lenaic force-pushed lenaic/113-deploiement-complet from d9829ef84d
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 53s
Intégration / Tests unitaires et couverture (pull_request) Successful in 55s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 13s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 21s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m24s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m0s
to 39e5494837
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 1m0s
Intégration / Tests unitaires et couverture (pull_request) Successful in 1m2s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 15s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 20s
Intégration / Aucun secret commité (pull_request) Successful in 2s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m32s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 56s
2026-09-03 12:44:39 +00:00
Compare
Author
Owner

Relecture 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_file est lu par le client, pas par le démon. Je croyais l'inverse, et tout
mon drapeau env reposait là-dessus. La pile MLflow aurait échoué. Le rôle rend
maintenant les fichiers de secrets des piles lisibles par le groupe deploy, et le
commentaire de vars.yml dit la vérité au lieu du contraire. Rendre mlflow.env depuis
le 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.sh a besoin de MinIO démarré, et la garde arrêtait le playbook
avant 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 je
n'aurais pas vu : le cd n'est pas décoratif, pip n'installe pas le paquet collector.
La même passe avait ajouté ENERVISION_PYTHON sans traiter son jumeau. Ajouté à la ligne
de crontab.

Les importants

4. Le SQL passe par stdin avec argv:, plus rien dans la ligne de commande. Tu as
raison de dire que les deux tâches devaient tenir la même ligne. Et ta nuance sur
PGPASSWORD est exacte : environment: est réalisé en préfixant la commande, l'exposition
est déplacée et non supprimée. Le runbook le dit maintenant, au lieu de laisser croire
l'inverse.

5. create on database ajouté. Le court-circuit du IF NOT EXISTS vérifié après le
droit sur la base, je ne le savais pas.

6. Attente explicite par pg_isready, until et 30 tentatives, au lieu du pari sur
les secondes achetées par les tâches intercalées.

7. Idempotence. Ton diagnostic est le bon : force: true voyait le changement de mode
et le remettait. La tâche chmod disparaît, et le dépôt livre les trois lanceurs en
100755.

Les mineurs

collector_paquets était bien mort, et ta remarque de fond porte plus loin que la
variable : psycopg n'arrivait dans le venv que par le pyproject de l'API. Il est
maintenant nommé là où il sert, dans collector_paquets_migrations, ajouté à ce que
déclarent les services.

Le commentaire sur rattrapage-readings.sh promettait en effet plus que le code — mais
c'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 par collector_log_dir. Une entrée logrotate est posée, quotidienne,
sept jours. deploy.yml couvre mlflow dans sa preuve. Le runbook ne dit plus que
no_log protège un mot de passe qui n'est pas dans le DSN, et il explique comment
constater qu'une réparation a eu lieu, puisque les deux tâches SQL rapportent ok.

pg_tables devient pg_class filtré sur relkind, avec l'alter qui convient pour une
séquence, une vue ou une vue matérialisée. Sur les chunks TimescaleDB : alter table sur
l'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-lint au profil production, les trois contrôles de syntaxe. Et sur le
serveur, les deux gestes que ta relecture a changés : le SQL par stdin passe, et
pg_isready répond.

L'exécution réelle départagera, et c'est bien la croissance de endpoint=current qui
tranche — tu as raison de la remettre au centre, c'est le seul critère qui ne se triche
pas.

Relecture 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_file` est lu par le client, pas par le démon.** Je croyais l'inverse, et tout mon drapeau `env` reposait là-dessus. La pile MLflow aurait échoué. Le rôle rend maintenant les fichiers de secrets des piles lisibles par le groupe `deploy`, et le commentaire de `vars.yml` dit la vérité au lieu du contraire. Rendre `mlflow.env` depuis le 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.sh` a besoin de MinIO démarré, et la garde arrêtait le playbook avant 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 je n'aurais pas vu : le `cd` n'est pas décoratif, `pip` n'installe pas le paquet `collector`. La même passe avait ajouté `ENERVISION_PYTHON` sans traiter son jumeau. Ajouté à la ligne de crontab. ### Les importants **4.** Le SQL passe par `stdin` avec `argv:`, plus rien dans la ligne de commande. Tu as raison de dire que les deux tâches devaient tenir la même ligne. Et ta nuance sur `PGPASSWORD` est exacte : `environment:` est réalisé en préfixant la commande, l'exposition est déplacée et non supprimée. Le runbook le dit maintenant, au lieu de laisser croire l'inverse. **5. `create on database` ajouté.** Le court-circuit du `IF NOT EXISTS` vérifié après le droit sur la base, je ne le savais pas. **6. Attente explicite** par `pg_isready`, `until` et 30 tentatives, au lieu du pari sur les secondes achetées par les tâches intercalées. **7. Idempotence.** Ton diagnostic est le bon : `force: true` voyait le changement de mode et le remettait. La tâche `chmod` disparaît, et le dépôt livre les trois lanceurs en `100755`. ### Les mineurs `collector_paquets` était bien mort, et ta remarque de fond porte plus loin que la variable : `psycopg` n'arrivait dans le venv que par le `pyproject` de l'API. Il est maintenant nommé là où il sert, dans `collector_paquets_migrations`, ajouté à ce que déclarent les services. Le commentaire sur `rattrapage-readings.sh` promettait en effet plus que le code — mais c'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 par `collector_log_dir`. Une entrée `logrotate` est posée, quotidienne, sept jours. `deploy.yml` couvre `mlflow` dans sa preuve. Le runbook ne dit plus que `no_log` protège un mot de passe qui n'est pas dans le DSN, et il explique comment constater qu'une réparation a eu lieu, puisque les deux tâches SQL rapportent `ok`. `pg_tables` devient `pg_class` filtré sur `relkind`, avec l'`alter` qui convient pour une séquence, une vue ou une vue matérialisée. Sur les chunks TimescaleDB : `alter table` sur l'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-lint` au profil production, les trois contrôles de syntaxe. Et sur le serveur, les deux gestes que ta relecture a changés : le SQL par `stdin` passe, et `pg_isready` répond. L'exécution réelle départagera, et c'est bien la croissance de `endpoint=current` qui tranche — tu as raison de la remettre au centre, c'est le seul critère qui ne se triche pas.
infra: réaligne aussi grafana et lui donne pg_monitor
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 49s
Intégration / Tests unitaires et couverture (pull_request) Successful in 55s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 14s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 18s
Intégration / Aucun secret commité (pull_request) Successful in 2s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m29s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m10s
badc710b97
Trouvé en relisant la #115 de Gabriel, qui ne peut pas fonctionner sans.

Le mot de passe du rôle grafana dans le coffre était refusé par le serveur, la
même dérive que pour les deux rôles applicatifs. Ses deux consommateurs de la
#115 le lisent — la source de données Grafana et postgres-exporter — et
auraient échoué au démarrage : tableau de bord métier vide, exportateur muet.
Vérifié, avec un contre-essai montrant qu'un mauvais mot de passe est bien
rejeté, donc le refus était réel et non un artefact.

mlflow reste volontairement en dehors : il tourne, et il lit son mot de passe
dans /etc/enervision/mlflow.env posé à la main lors du #30. Le réaligner sur le
coffre le couperait de sa base.

Et grafana devient membre de pg_monitor. postgres-exporter lit
pg_stat_replication, pg_stat_wal et le texte des requêtes de pg_stat_activity :
sans ce droit il démarre, rend des métriques partielles et journalise des
erreurs. Mesuré avant et après sur le serveur : les 23 requêtes de
pg_stat_activity passent de masquées à lisibles. Le rôle reste en lecture seule
sur les données métier.
gabriel requested changes 2026-09-03 12:56:14 +00:00
Dismissed
gabriel left a comment

Relu après le correctif 39e5494 : le rôle app, 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 assert par un debug, mais « Démarrer les piles » boucle toujours sur app_stacks sans filtre (roles/app/tasks/main.yml:186).

Sur une machine neuve, sans mlflow.env :

  1. « Signaler les piles sautées » affiche son message ;
  2. « Démarrer les piles » itère quand même mlflow ;
  3. docker compose lit services.mlflow.env_file: [/etc/enervision/mlflow.env] depuis le fichier de composition (indépendamment de --env-file / du drapeau env), le fichier est absent → la tâche échoue, sans failed_when ;
  4. le play s'arrête. postgres et minio sont 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.

- name: Démarrer les piles
  community.docker.docker_compose_v2:
    project_src: "{{ app_repo_dir }}/infra/compose/{{ item.item.path }}"
    env_files: >-
      {{ [app_secrets_dir ~ '/' ~ item.item.name ~ '.env']
         if item.item.env | default(true) else omit }}
    # ...
  loop: "{{ app_secrets_piles.results }}"
  when: item.stat.exists
  loop_control:
    label: "{{ item.item.name }}"

À vérifier au passage : mlflow a env: false mais sa composition exige quand même mlflow.env (son env_file: interne). La condition de saut pour mlflow reste donc « mlflow.env présent », pas « env: true ».

Importants

1. when: item.name != 'minio' or true (main.yml:148) — tautologie (X or true est toujours vrai), manifestement un reliquat d'édition. À supprimer.

2. failed_when: false sur le chmod des fichiers de secrets (main.yml:149) — la tâche « Rendre lisibles par le groupe » avale toute erreur, sur des fichiers sensibles. Si le chmod échoue en silence, mlflow.env reste 0600 root:root et 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 le stat avant, puis when: item.stat.exists sans failed_when.

3. « Attendre que PostgreSQL réponde » fait doublon. « Démarrer les piles » porte déjà wait: true + wait_timeout (présent sur develop), et la pile postgres a un healthcheck pg_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âche until.

4. Première exécution des migrations : l'état du registre est une hypothèse. run_migrations se fie à ops.schema_migrations. Les migrations 0007+ sont idempotentes (create table if not exists) et la reprise de propriété passe avant — le risque est donc contenu. Mais 0001_auth.sql fait create table ops.users sans if not exists : si une migration ancienne a été appliquée à la main sans passer par migrate.py, elle n'est pas dans le registre et la tâche échoue sur relation already exists, sous no_log. À confirmer sur le serveur réel avant merge : select version from ops.schema_migrations sur enervision_prod et enervision_preprod reflè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é, avec psycopg dupliqué. Deux variables au nom quasi identique, une seule lue → à supprimer.
  • Le commentaire du code sur 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é en sh -c 'PGPASSWORD=… …', visible en ps sur la cible.
  • Le déploiement desserre mlflow.env de 0600 root:root à 0640 root:deploy à chaque passage, en silence. Cohérent avec postgres.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.
  • debug n'est pas « bruyant ». Dans un log de CI il défile. Pour une pile non démarrée en déploiement continu, envisager un fail conditionné au contexte CD, ou au moins un marqueur ::warning::.
  • Échec de migration sous no_log : un rescue: 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.
  • Propriété des chunks TimescaleDB de public.mesure : alter table … owner to ne 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_when sur stderr, SQL par stdin, grant create on database, pg_class/relkind à la place de pg_tables, ENERVISION_RACINE sur la ligne de crontab, la rotation des journaux, rattrapage-readings.sh retiré de collector_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 après le correctif `39e5494` : le rôle `app`, `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 `assert` par un `debug`, mais **« Démarrer les piles » boucle toujours sur `app_stacks` sans filtre** (`roles/app/tasks/main.yml:186`). Sur une machine neuve, sans `mlflow.env` : 1. « Signaler les piles sautées » affiche son message ; 2. « Démarrer les piles » itère quand même `mlflow` ; 3. `docker compose` lit `services.mlflow.env_file: [/etc/enervision/mlflow.env]` **depuis le fichier de composition** (indépendamment de `--env-file` / du drapeau `env`), le fichier est absent → la tâche échoue, sans `failed_when` ; 4. le play s'arrête. `postgres` et `minio` sont 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. ```yaml - name: Démarrer les piles community.docker.docker_compose_v2: project_src: "{{ app_repo_dir }}/infra/compose/{{ item.item.path }}" env_files: >- {{ [app_secrets_dir ~ '/' ~ item.item.name ~ '.env'] if item.item.env | default(true) else omit }} # ... loop: "{{ app_secrets_piles.results }}" when: item.stat.exists loop_control: label: "{{ item.item.name }}" ``` À vérifier au passage : `mlflow` a `env: false` mais sa composition **exige quand même** `mlflow.env` (son `env_file:` interne). La condition de saut pour `mlflow` reste donc « `mlflow.env` présent », pas « `env: true` ». ## Importants **1. `when: item.name != 'minio' or true`** (`main.yml:148`) — tautologie (`X or true` est toujours vrai), manifestement un reliquat d'édition. À supprimer. **2. `failed_when: false` sur le `chmod` des fichiers de secrets** (`main.yml:149`) — la tâche « Rendre lisibles par le groupe » avale **toute** erreur, sur des fichiers sensibles. Si le `chmod` échoue en silence, `mlflow.env` reste `0600 root:root` et 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 le `stat` **avant**, puis `when: item.stat.exists` sans `failed_when`. **3. « Attendre que PostgreSQL réponde » fait doublon.** « Démarrer les piles » porte déjà `wait: true` + `wait_timeout` (présent sur `develop`), et la pile `postgres` a un healthcheck `pg_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âche `until`. **4. Première exécution des migrations : l'état du registre est une hypothèse.** `run_migrations` se fie à `ops.schema_migrations`. Les migrations `0007+` sont idempotentes (`create table if not exists`) et la reprise de propriété passe avant — le risque est donc contenu. Mais `0001_auth.sql` fait `create table ops.users` **sans `if not exists`** : si une migration ancienne a été appliquée à la main sans passer par `migrate.py`, elle n'est pas dans le registre et la tâche échoue sur `relation already exists`, sous `no_log`. À **confirmer sur le serveur réel avant merge** : `select version from ops.schema_migrations` sur `enervision_prod` et `enervision_preprod` reflè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é, avec `psycopg` dupliqué. Deux variables au nom quasi identique, une seule lue → à supprimer. - **Le commentaire du code sur `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é en `sh -c 'PGPASSWORD=… …'`, visible en `ps` sur la cible. - **Le déploiement desserre `mlflow.env` de `0600 root:root` à `0640 root:deploy` à chaque passage**, en silence. Cohérent avec `postgres.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`. - **`debug` n'est pas « bruyant ».** Dans un log de CI il défile. Pour une pile non démarrée en déploiement continu, envisager un `fail` conditionné au contexte CD, ou au moins un marqueur `::warning::`. - **Échec de migration sous `no_log`** : un `rescue:` 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. - **Propriété des chunks TimescaleDB** de `public.mesure` : `alter table … owner to` ne 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_when` sur `stderr`, SQL par `stdin`, `grant create on database`, `pg_class`/`relkind` à la place de `pg_tables`, `ENERVISION_RACINE` sur la ligne de crontab, la rotation des journaux, `rattrapage-readings.sh` retiré de `collector_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.
olivier requested changes 2026-09-03 12:57:29 +00:00
Dismissed
olivier left a comment

Relu à badc710, après la rebase sur develop (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_timeout sur les piles vaut mieux que la seule attente pg_isready, et sur rattrapage-readings.sh tu 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égression

C'est ma remarque sur pg_tables qui l'a introduite, et je ne l'avais pas vue venir. Rejouée telle quelle sur un PostgreSQL 16 neuf, en reproduisant 0001 et 0012 :

ERROR:  cannot change owner of sequence "alerte_alerte_id_seq"
DETAIL:  Sequence "alerte_alerte_id_seq" is linked to table "alerte".
CONTEXT: SQL statement "alter sequence public.alerte_alerte_id_seq owner to appro"
code de sortie psql : 3

ALTER SEQUENCE … OWNER TO est 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 posent bigint generated always as identity : 0001, 0002, 0012, 0014. Les deux bases sont donc touchées, avec ON_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_tables fonctionnait, 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 TO les emporte de toute façon, donc rien n'est perdu.

and not (c.relkind = 'S' and exists (
      select 1 from pg_depend d
       where d.classid = 'pg_class'::regclass and d.objid = c.oid
         and d.refclassid = 'pg_class'::regclass and d.refobjsubid > 0))

Avec ça la boucle passe, et les séquences appartiennent bien au rôle applicatif à l'arrivée — donc le attendu : 0 du runbook tient toujours.

2. La garde est neutralisée, mais la boucle des piles n'est pas filtrée

main.yml:186 boucle toujours sur app_stacks entier. Or un env_file absent fait sortir docker compose en 1 :

env file .../mlflow.env not found: stat ...: no such file or directory
code de sortie : 1

Donc sur une machine neuve : le debug affiche « 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 :

  • le message décrit ce qui n'arrive pas : la pile est tentée, et elle emporte le déploiement avec elle ;
  • le runbook (« un premier déploiement qui pose PostgreSQL et MinIO en sautant MLflow ») présente la première passe comme un déploiement à avertissement. C'est une exécution en échec, rouge dans la chaîne, et elle le restera à chaque passage tant que mlflow.env manque.

Il manque un when sur app_secrets_piles.results, ou un loop filtré.

Sur badc710 — le réalignement de grafana

Le 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.j2 rend PG_GRAFANA_PASSWORD et DATA_SOURCE_PASS depuis vault_pg_grafana_password, et datasources.yml l'interpole. Sans ce réalignement, la source de données et postgres-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 :

Limité aux rôles applicatifs de collector_bases. Surtout pas mlflow ni grafana : ils lisent leur mot de passe dans leur propre fichier de secrets, et le réaligner sur le coffre les couperait de leur base.

La boucle vaut désormais collector_bases + collector_roles_lecture, et grafana y est. docs/runbooks/deploiement.md:92-94 porte 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.yml et group_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_monitor mérite une phrase de plus. « Le rôle reste en lecture seule sur les données métier » est exact pour les tables, mais pg_monitor donne aussi pg_stat_activity.query — le texte des requêtes des autres sessions, qui porte des valeurs littérales : une adresse dans un where, les valeurs d'un insert 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 true est une tautologie — reliquat d'édition. ansible-lint ne l'attrape pas.
  • collector_paquets est toujours mort. collector_paquets_migrations a été ajouté à côté sans que l'ancien parte : deux listes, psycopg déclaré dans les deux, une seule lue. C'est le doublon que la remarque visait à supprimer.
  • Dérive de doc sur mlflow.env. Le rôle force 0640 root:deploy, mais docs/runbooks/mlflow.md:15, infra/compose/mlflow/docker-compose.yml:5 et genere-identifiants.sh:244 disent toujours 0600 root. L'élargissement est le bon choix, mais il change la posture — les identifiants MinIO dédiés du #30 deviennent lisibles par le groupe deploy — et ça doit être écrit là où le contraire est encore affirmé.
  • main.yml:424 : « sur les deux tâches SQL » — elles sont trois depuis badc710.
  • collector_roles_lecture porte le préfixe du collecteur sans avoir de rapport avec lui. Détail.

Sur les preuves

yamllint, ansible-lint au 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 de endpoint=current derrière.

Les deux bloquants tiennent en quelques lignes chacun, et le SQL de remplacement ci-dessus est déjà testé.

Relu à `badc710`, après la rebase sur `develop` (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_timeout` sur les piles vaut mieux que la seule attente `pg_isready`, et sur `rattrapage-readings.sh` tu 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égression C'est ma remarque sur `pg_tables` qui l'a introduite, et je ne l'avais pas vue venir. Rejouée telle quelle sur un PostgreSQL 16 neuf, en reproduisant `0001` et `0012` : ``` ERROR: cannot change owner of sequence "alerte_alerte_id_seq" DETAIL: Sequence "alerte_alerte_id_seq" is linked to table "alerte". CONTEXT: SQL statement "alter sequence public.alerte_alerte_id_seq owner to appro" code de sortie psql : 3 ``` `ALTER SEQUENCE … OWNER TO` est 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 posent `bigint generated always as identity` : `0001`, `0002`, `0012`, `0014`. Les deux bases sont donc touchées, avec `ON_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_tables` fonctionnait, 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 TO` les emporte de toute façon, donc rien n'est perdu. ```sql and not (c.relkind = 'S' and exists ( select 1 from pg_depend d where d.classid = 'pg_class'::regclass and d.objid = c.oid and d.refclassid = 'pg_class'::regclass and d.refobjsubid > 0)) ``` Avec ça la boucle passe, et les séquences appartiennent bien au rôle applicatif à l'arrivée — donc le `attendu : 0` du runbook tient toujours. ## 2. La garde est neutralisée, mais la boucle des piles n'est pas filtrée `main.yml:186` boucle toujours sur `app_stacks` entier. Or un `env_file` absent fait sortir `docker compose` en 1 : ``` env file .../mlflow.env not found: stat ...: no such file or directory code de sortie : 1 ``` Donc sur une machine neuve : le `debug` affiche « 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 : - le message décrit ce qui n'arrive pas : la pile *est* tentée, et elle emporte le déploiement avec elle ; - le runbook (« un premier déploiement qui pose PostgreSQL et MinIO en sautant MLflow ») présente la première passe comme un déploiement à avertissement. C'est une exécution en échec, rouge dans la chaîne, et elle le restera à chaque passage tant que `mlflow.env` manque. Il manque un `when` sur `app_secrets_piles.results`, ou un `loop` filtré. ## Sur `badc710` — le réalignement de grafana Le 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.j2` rend `PG_GRAFANA_PASSWORD` et `DATA_SOURCE_PASS` depuis `vault_pg_grafana_password`, et `datasources.yml` l'interpole. Sans ce réalignement, la source de données et `postgres-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 : > Limité aux rôles applicatifs de `collector_bases`. Surtout pas `mlflow` ni `grafana` : ils lisent leur mot de passe dans leur propre fichier de secrets, et le réaligner sur le coffre les couperait de leur base. La boucle vaut désormais `collector_bases + collector_roles_lecture`, et `grafana` y est. `docs/runbooks/deploiement.md:92-94` porte 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.yml` et `group_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_monitor` mérite une phrase de plus.** « Le rôle reste en lecture seule sur les données métier » est exact pour les tables, mais `pg_monitor` donne aussi `pg_stat_activity.query` — le *texte* des requêtes des autres sessions, qui porte des valeurs littérales : une adresse dans un `where`, les valeurs d'un `insert 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 true` est une tautologie — reliquat d'édition. `ansible-lint` ne l'attrape pas. - **`collector_paquets` est toujours mort.** `collector_paquets_migrations` a été ajouté à côté sans que l'ancien parte : deux listes, `psycopg` déclaré dans les deux, une seule lue. C'est le doublon que la remarque visait à supprimer. - **Dérive de doc sur `mlflow.env`.** Le rôle force `0640 root:deploy`, mais `docs/runbooks/mlflow.md:15`, `infra/compose/mlflow/docker-compose.yml:5` et `genere-identifiants.sh:244` disent toujours `0600 root`. L'élargissement est le bon choix, mais il change la posture — les identifiants MinIO dédiés du #30 deviennent lisibles par le groupe `deploy` — et ça doit être écrit là où le contraire est encore affirmé. - **`main.yml:424`** : « sur les deux tâches SQL » — elles sont trois depuis `badc710`. - `collector_roles_lecture` porte le préfixe du collecteur sans avoir de rapport avec lui. Détail. ## Sur les preuves `yamllint`, `ansible-lint` au 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 de `endpoint=current` derrière. Les deux bloquants tiennent en quelques lignes chacun, et le SQL de remplacement ci-dessus est déjà testé.
infra: retours de Gabriel et d'Olivier sur la #114
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 51s
Intégration / Tests unitaires et couverture (pull_request) Successful in 53s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 14s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 23s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m24s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 2m12s
414bacce68
Trois défauts, dont un que j'avais cru corriger et qui ne l'était pas, et une
régression que j'avais introduite.

LE SAUT DE MLFLOW N'ÉTAIT PAS CÂBLÉ. J'avais remplacé la garde par un
avertissement, mais la boucle de démarrage itérait toujours app_stacks entier :
mon filtrage n'avait rien trouvé à remplacer et je ne l'avais pas vérifié. La
pile était donc tentée malgré l'avertissement, docker compose sortait en 1 sur
son env_file absent — qu'il lit depuis la composition, indépendamment du
drapeau env — et tout ce qui suit ne tournait pas. On retombait sur le scénario
d'Olivier, décalé de quelques tâches. La boucle porte maintenant sur la
présence du secret, et la condition est bien celle-là et non le drapeau env :
MLflow a env: false et exige pourtant son fichier.

LA BOUCLE PG_CLASS ÉCHOUAIT SUR LES SÉQUENCES D'IDENTITÉ, régression née de ma
propre réponse à la remarque d'Olivier sur pg_tables. « alter sequence … owner
to » est refusé sur une séquence liée à une colonne, quel que soit son
propriétaire : le test porte sur le lien, pas sur la propriété. Quatre
migrations posent « generated always as identity », donc la réparation échouait
sur les deux bases, sous ON_ERROR_STOP, juste avant les migrations. Les
séquences liées sont écartées ; « alter table … owner to » les emporte de toute
façon. Vérifié sur la préproduction : 4 séquences liées, réparation passée,
0 objet hors du rôle à l'arrivée.

Et deux scories de mon correctif précédent : une tautologie « X or true »
laissée d'une édition, et un failed_when: false qui avalait toute erreur sur
des fichiers de secrets — un chmod échoué en silence aurait laissé mlflow.env
en 0600 root:root et fait échouer la pile bien plus loin, avec le message boto3
vers le vrai AWS S3 que ce ticket veut éviter. Seule l'absence du fichier est
désormais tolérée.
Author
Owner

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 compose sortait en 1
sur son env_file: absent, et tout ce qui suit ne tournait pas. On retombait exactement
sur 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.results avec when: item.stat.exists.
Et la condition est bien la présence du secret, pas le drapeau env — Gabriel a raison
de le souligner, MLflow a env: false et 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_tables qui l'a introduite, et ton diagnostic est exact : alter sequence … owner to porte sur le lien, pas sur la propriété. Vérifié sur la préproduction avant de
pousser :

séquences liées à une colonne : 4
réparation avec l'exclusion   : code 0
objets hors du rôle après     : 0

Les quatre que tu annonçais, et le attendu : 0 du runbook tient toujours puisque
alter table … owner to les emporte. J'ai repris ton pg_depend tel quel.

Gabriel, tes deux scories étaient bien des scories de mon correctif. La tautologie
X or true est un reliquat d'édition, retirée. Et tu as raison sur le failed_when: false : avaler toute erreur sur des fichiers de secrets aurait laissé mlflow.env en
0600 root:root et fait échouer la pile bien plus loin, avec le message boto3 vers le
vrai 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 grafana dans le coffre était refusé par le serveur, même dérive
que 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. mlflow reste volontairement
dehors, il tourne et lit son mot de passe dans mlflow.env.

Et grafana devient membre de pg_monitor. Mesuré avant et après : les requêtes lisibles
dans pg_stat_activity passent de 0 sur 23 à 23 sur 23.

Sur ce que vous validez tous les deux, je garde tel quel : changed_when sur stderr,
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.sh c'est le commentaire qui
mentait. 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=current qui
tranche : à 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.

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 compose` sortait en 1 sur son `env_file:` absent, et tout ce qui suit ne tournait pas. On retombait exactement sur 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.results` avec `when: item.stat.exists`. Et la condition est bien la présence du secret, pas le drapeau `env` — Gabriel a raison de le souligner, MLflow a `env: false` et 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_tables` qui l'a introduite, et ton diagnostic est exact : `alter sequence … owner to` porte sur le lien, pas sur la propriété. Vérifié sur la préproduction avant de pousser : ``` séquences liées à une colonne : 4 réparation avec l'exclusion : code 0 objets hors du rôle après : 0 ``` Les quatre que tu annonçais, et le `attendu : 0` du runbook tient toujours puisque `alter table … owner to` les emporte. J'ai repris ton `pg_depend` tel quel. **Gabriel, tes deux scories étaient bien des scories de mon correctif.** La tautologie `X or true` est un reliquat d'édition, retirée. Et tu as raison sur le `failed_when: false` : avaler toute erreur sur des fichiers de secrets aurait laissé `mlflow.env` en `0600 root:root` et fait échouer la pile bien plus loin, avec le message boto3 vers le vrai 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 `grafana` dans le coffre était refusé par le serveur, même dérive que 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. `mlflow` reste volontairement dehors, il tourne et lit son mot de passe dans `mlflow.env`. Et `grafana` devient membre de `pg_monitor`. Mesuré avant et après : les requêtes lisibles dans `pg_stat_activity` passent de 0 sur 23 à 23 sur 23. **Sur ce que vous validez tous les deux**, je garde tel quel : `changed_when` sur `stderr`, 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.sh` c'est le commentaire qui mentait. 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=current` qui tranche : à 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.
olivier requested changes 2026-09-03 13:17:49 +00:00
Dismissed
olivier left a comment

Relu à 414bacc. Mes deux bloquants sont bien corrigés : l'exclusion pg_depend est reprise telle quelle, et la boucle de démarrage porte maintenant sur app_secrets_piles.results avec when: item.stat.exists. Rejoué sous ansible-core 2.15 : le filtrage fonctionne, mlflow est sauté et le playbook continue. La réécriture en item.item.* et l'expression env_files sur deux lignes tiennent aussi.

Mais le correctif du failed_when réintroduit le blocage, quelques tâches plus tôt.

main.yml:156-158 teste le message d'erreur :

failed_when:
  - app_secrets_mode is failed
  - "'does not exist' not in (app_secrets_mode.msg | default(''))"

Le module file ne dit pas « does not exist ». Exécuté sur un fichier absent :

failed: (item=mlflow) => "failed_when_result": true,
  "msg": "file (/etc/enervision/mlflow.env) is absent, cannot continue",
  "state": "absent"

La condition est donc vraie et la tâche échoue. Sur une machine neuve, le playbook meurt sur ce chmod avant le stat, 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.

failed_when:
  - app_secrets_mode is failed
  - app_secrets_mode.state | default('') != 'absent'

Avec ça, postgres passe, mlflow est 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_paquets est toujours mort (vars.yml:83) : collector_paquets_migrations a été ajouté à côté sans que l'ancien parte, et psycopg est déclaré dans les deux.
  • main.yml:357 et docs/runbooks/deploiement.md:92 interdisent toujours par écrit — « surtout pas mlflow ni grafana » — le réalignement de grafana que le code fait maintenant. La raison a changé et c'est elle qu'il faut écrire, aux deux endroits.
  • docs/runbooks/mlflow.md:15 et infra/compose/mlflow/docker-compose.yml:5 annoncent encore mlflow.env en 0600 root alors que le rôle le pose en 0640 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 groupe deploy — et ça doit être écrit là où le contraire est encore affirmé.
  • main.yml:442 parle de « deux tâches SQL » ; elles sont trois depuis badc710.

Sur pg_monitor

Le besoin est réel, j'ai vérifié la #115 : supervision.env.j2 rend PG_GRAFANA_PASSWORD et DATA_SOURCE_PASS depuis le coffre, sans le réalignement la source de données et postgres-exporter échouent tous deux au démarrage. Une phrase à ajouter au manuel quand même : pg_monitor donne aussi le texte des requêtes des autres sessions dans pg_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.

Relu à `414bacc`. **Mes deux bloquants sont bien corrigés** : l'exclusion `pg_depend` est reprise telle quelle, et la boucle de démarrage porte maintenant sur `app_secrets_piles.results` avec `when: item.stat.exists`. Rejoué sous ansible-core 2.15 : le filtrage fonctionne, `mlflow` est sauté et le playbook continue. La réécriture en `item.item.*` et l'expression `env_files` sur deux lignes tiennent aussi. **Mais le correctif du `failed_when` réintroduit le blocage, quelques tâches plus tôt.** `main.yml:156-158` teste le message d'erreur : ```yaml failed_when: - app_secrets_mode is failed - "'does not exist' not in (app_secrets_mode.msg | default(''))" ``` Le module `file` ne dit pas « does not exist ». Exécuté sur un fichier absent : ``` failed: (item=mlflow) => "failed_when_result": true, "msg": "file (/etc/enervision/mlflow.env) is absent, cannot continue", "state": "absent" ``` La condition est donc vraie et la tâche échoue. Sur une machine neuve, le playbook meurt sur ce `chmod` **avant** le `stat`, 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. ```yaml failed_when: - app_secrets_mode is failed - app_secrets_mode.state | default('') != 'absent' ``` Avec ça, `postgres` passe, `mlflow` est 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_paquets` est toujours mort (`vars.yml:83`) : `collector_paquets_migrations` a été ajouté à côté sans que l'ancien parte, et `psycopg` est déclaré dans les deux. - `main.yml:357` et `docs/runbooks/deploiement.md:92` interdisent toujours par écrit — « surtout pas `mlflow` ni `grafana` » — le réalignement de `grafana` que le code fait maintenant. La raison a changé et c'est elle qu'il faut écrire, aux deux endroits. - `docs/runbooks/mlflow.md:15` et `infra/compose/mlflow/docker-compose.yml:5` annoncent encore `mlflow.env` en `0600 root` alors que le rôle le pose en `0640 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 groupe `deploy` — et ça doit être écrit là où le contraire est encore affirmé. - `main.yml:442` parle de « deux tâches SQL » ; elles sont trois depuis `badc710`. ## Sur `pg_monitor` Le besoin est réel, j'ai vérifié la #115 : `supervision.env.j2` rend `PG_GRAFANA_PASSWORD` et `DATA_SOURCE_PASS` depuis le coffre, sans le réalignement la source de données et `postgres-exporter` échouent tous deux au démarrage. Une phrase à ajouter au manuel quand même : `pg_monitor` donne aussi le *texte* des requêtes des autres sessions dans `pg_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.
Merge branch 'develop' into lenaic/113-deploiement-complet
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 58s
Intégration / Tests unitaires et couverture (pull_request) Successful in 59s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 15s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 20s
Intégration / Aucun secret commité (pull_request) Successful in 2s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m27s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m23s
4257b7c2fe
Retour d'Olivier. Mon correctif précédent réintroduisait le blocage quelques
tâches plus tôt : je testais le message du module `file`, qui dit « is absent,
cannot continue » et non « does not exist ». La condition était donc toujours
vraie, le playbook mourait sur le chmod, avant le stat, avant l'avertissement et
avant la boucle filtrée. Tout le dispositif « sauter et continuer » n'avait
jamais l'occasion de tourner. Le test porte maintenant sur l'état, qui est déjà
dans le résultat, correctif repris de lui.

Et trois endroits où l'écrit contredisait le code.

Le commentaire du réalignement et le runbook interdisaient tous deux « surtout
pas mlflow ni grafana » alors que le code réaligne grafana depuis la #115. La
raison a changé et c'est elle qui est écrite : le réalignement suit le fichier
de secrets, pas le rôle. Celui de grafana vient du coffre, celui de mlflow est
posé à la main, donc mlflow reste dehors. Vérifié avant d'ajouter grafana :
aucun Grafana n'est installé sur le serveur, ni conteneur ni service, le
réalignement ne coupe rien.

La composition MLflow et son manuel annonçaient encore mlflow.env en 0600 root
alors que le rôle le pose en 0640 root:deploy. L'élargissement est le bon choix
mais il change la posture, les identifiants MinIO dédiés du #30 devenant
lisibles par le groupe deploy : c'est écrit là où le contraire était affirmé.

Et pg_monitor ne donne pas que des compteurs : il donne aussi le texte des
requêtes des autres sessions, qui porte des valeurs littérales. « Lecture seule
sur les données métier » reste vrai pour les tables, mais l'échange se décide
plutôt qu'il ne se subit.

collector_paquets, mort depuis deux passes, est enfin supprimé.
collector_roles_lecture devient pg_roles_lecture, il n'avait rien à voir avec le
collecteur. Et le commentaire parle de trois tâches SQL, puisqu'elles sont trois.
tests: un banc d'essai qui joue la logique du rôle app
Some checks failed
Intégration / Qualité du code Python (pull_request) Successful in 47s
Intégration / Tests unitaires et couverture (pull_request) Successful in 57s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 13s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 19s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m21s
Intégration / Aucun secret commité (pull_request) Successful in 2s
Infra Ansible / Playbooks Ansible valides (pull_request) Failing after 59s
366d9ee108
Trois défauts sont partis en relecture sur la #114, et aucun n'était atteignable
par yamllint ni par ansible-lint : une condition tautologique, une boucle non
filtrée, et un failed_when qui testait le message d'un module au lieu de son
état. Un linter lit la forme ; ces trois-là ne se voient qu'en exécutant.

Ce playbook exécute. Il rejoue les expressions du rôle sur des données
fabriquées, dans un répertoire temporaire, sans serveur ni Docker :

  - un fichier de secrets absent doit être toléré par le failed_when,
  - la boucle de démarrage ne doit retenir que les piles qui ont le leur,
  - env_files doit rendre une liste ou omettre le paramètre,
  - la ligne de crontab doit porter ENERVISION_RACINE autant qu'ENERVISION_PYTHON,
  - aucune condition du rôle ne doit se terminer par « or true ».

Les cinq ont été vérifiés dans les deux sens : chacun a été mis en échec en
réintroduisant le défaut qu'il vise, puis remis au vert. Ce contrôle a d'ailleurs
trouvé une faute dans le banc lui-même — un double antislash en YAML donnait un
antislash littéral, et la détection de tautologie ne cherchait pas ce que je
croyais. Un test qui passe ne prouve rien tant qu'il n'a pas échoué.

Branché à « Infra Ansible », en amont d'ansible-lint.
tests: le banc ne dépend plus d'un groupe qui n'existe pas dans la chaîne
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 55s
Intégration / Tests unitaires et couverture (pull_request) Successful in 59s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 13s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 18s
Intégration / Aucun secret commité (pull_request) Successful in 2s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m24s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m31s
c20fc38f8b
Mon propre banc faisait échouer « Infra Ansible » : il posait un groupe lu dans
la variable USER, absente du conteneur de la chaîne, et le module rendait
« chgrp failed: failed to look up group ». La tâche échouait donc pour une
raison étrangère à ce qu'elle vérifie — le comportement du failed_when sur un
fichier absent.

Le groupe est retiré. Rejoué dans les trois environnements : avec USER, sans
USER, et sans HOME. Dix tâches au vert dans chaque cas.
olivier dismissed gabriel's review 2026-09-03 13:41:39 +00:00
Reason:

I got it

olivier removed review request for justine 2026-09-03 13:41:47 +00:00
olivier requested changes 2026-09-03 13:48:28 +00:00
Dismissed
olivier left a comment

Relu à c20fc38. f2dd1c2 ne me laisse plus rien à redire sur le code. Le failed_when teste l'état, les trois contradictions écrit/code sont levées avec la raison réécrite plutôt que rafistolée, collector_paquets est 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 de requirements-ci.txt : dix tâches au vert, ta mesure est exacte. J'ai ensuite réintroduit les trois défauts dans main.yml — le failed_when fautif, ENERVISION_RACINE retiré de la ligne de crontab, la boucle de démarrage dé-filtrée — et rejoué :

=== les trois défauts sont-ils dans le rôle ? ===  oui
=== le banc les voit-il ? ===
localhost : ok=10  changed=3  unreachable=0  failed=0

Aucun des trois. Quatre contrôles sur cinq rejouent une copie de l'expression, écrite dans le banc : env_files et la ligne de crontab sont réécrits dans vars:, le filtre des piles est reconstruit sur des données locales, et le failed_when est recopié sur une tâche file du banc. Seule la détection de tautologie lit vraiment le rôle, par lookup('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.yml et éprouver ce qui y est écrit, pas une copie.

Deux détails au passage :

  • le banc est rouge sur ansible-core ≤ 2.18 : sans_env is not sequence est faux tant qu'omit est 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 == omit serait insensible à la version ;
  • infra/compose/mlflow/genere-identifiants.sh:217,243-244 écrit toujours /etc/enervision en 0700 root:root et mlflow.env en 0600 root:root, ce qui contredit la doc que f2dd1c2 vient 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=current est à 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, le failed_when qui 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.yml porte un workflow_dispatch dont 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 changed sur 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.

Relu à `c20fc38`. **`f2dd1c2` ne me laisse plus rien à redire sur le code.** Le `failed_when` teste l'état, les trois contradictions écrit/code sont levées avec la raison réécrite plutôt que rafistolée, `collector_paquets` est 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 de `requirements-ci.txt` : dix tâches au vert, ta mesure est exacte. J'ai ensuite réintroduit les trois défauts dans `main.yml` — le `failed_when` fautif, `ENERVISION_RACINE` retiré de la ligne de crontab, la boucle de démarrage dé-filtrée — et rejoué : ``` === les trois défauts sont-ils dans le rôle ? === oui === le banc les voit-il ? === localhost : ok=10 changed=3 unreachable=0 failed=0 ``` Aucun des trois. Quatre contrôles sur cinq rejouent une **copie** de l'expression, écrite dans le banc : `env_files` et la ligne de crontab sont réécrits dans `vars:`, le filtre des piles est reconstruit sur des données locales, et le `failed_when` est recopié sur une tâche `file` du banc. Seule la détection de tautologie lit vraiment le rôle, par `lookup('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.yml` et éprouver ce qui y est écrit, pas une copie. Deux détails au passage : - le banc est rouge sur `ansible-core ≤ 2.18` : `sans_env is not sequence` est faux tant qu'`omit` est 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 == omit` serait insensible à la version ; - `infra/compose/mlflow/genere-identifiants.sh:217,243-244` écrit toujours `/etc/enervision` en `0700 root:root` et `mlflow.env` en `0600 root:root`, ce qui contredit la doc que `f2dd1c2` vient 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=current` est à 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, le `failed_when` qui 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.yml` porte un `workflow_dispatch` dont 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 `changed` sur 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.
tests: le banc lit enfin le rôle au lieu d'en recopier les expressions
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 49s
Intégration / Tests unitaires et couverture (pull_request) Successful in 58s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 11s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 18s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m25s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 2m7s
2b13e9bfac
Olivier a démontré que mon banc restait vert sur un rôle cassé : quatre de ses
cinq contrôles rejouaient une COPIE de l'expression, réécrite dans le banc.
Seule la détection de tautologie lisait vraiment main.yml. Un banc vert sur un
rôle cassé est pire qu'une absence de banc, parce que le suivant lui fait
confiance — et mon message de commit affirmait le contraire de ce que le code
faisait.

Tout est désormais extrait de roles/app/tasks/main.yml à l'exécution.

Le point délicat est qu'Ansible RETEMPLATE la valeur d'une variable : une
expression du rôle stockée telle quelle est évaluée au lieu d'être lue, et
l'assertion porte alors sur son résultat. Deux contrôles sont passés au vert
sur un rôle cassé pour cette raison, pendant que je les écrivais. Les accolades
sont donc neutralisées dès la lecture, dans la même expression.

Éprouvé dans les deux sens, et c'est le seul point qui compte : vert sur le
rôle sain, ROUGE sur les cinq défauts réintroduits un par un — le failed_when
qui teste le message, ENERVISION_RACINE retiré du cron, la boucle dé-filtrée,
le filtre pg_depend retiré, et la tautologie. Le script d'injection est lui-même
passé par deux corrections : il frappait la mauvaise occurrence d'une expression
présente deux fois, et son « vert » ne prouvait rien.

genere-identifiants.sh pose enfin /etc/enervision en 0710 root:deploy et
mlflow.env en 0640 root:deploy, au lieu de contredire la documentation que le
commit précédent venait d'aligner. C'est ce script qu'un exploitant lance à la
main, avant tout déploiement.

Reste ce que ce banc ne remplace pas, et qu'Olivier a raison d'exiger : une
exécution réelle sur le serveur, deux fois, et le décompte bronze.
Author
Owner

Tu avais raison de refuser sans exécution. Elle est faite, deux fois, par
workflow_dispatch sur cette branche et sans fusionner.

Le critère qui tranche

16:15:48  relevé 1 :  7 objets sous endpoint=current
16:18:58  relevé 2 : 28 objets
croissance : 21 objets en 3 minutes

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 :

collector.current site=SITE007 cle=source=mock_api/endpoint=current/site_id=SITE007/
                  dt=2026-09-03/hour=14/20260903T141500Z.json http=200
collector.current passe terminée : 7/7 objets écrits, 7 avec payload

Et la ligne de crontab telle qu'elle est posée, avec la variable dont l'absence faisait
sortir la relève chaque minute :

* * * * * ENERVISION_RACINE=/opt/enervision/.repo
          ENERVISION_PYTHON=/opt/enervision/venv/bin/python
          /opt/enervision/.repo/services/collector/bin/collecte-current.sh

État des deux bases après le déploiement

enervision_prod enervision_preprod
Migrations appliquées 15 15
Tables 11 11
Objets hors du rôle applicatif 0 0

Le attendu : 0 du runbook tient : ton exclusion pg_depend fonctionne, et
alter table … owner to emporte 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 :

  • la crontab : 2 lignes après deux exécutions, pas 4. Le module cron pose par nom ;
  • l'environnement Python : intact, aucune réinstallation ;
  • les migrations : « Base à jour », rien de réappliqué ;
  • la propriété et les mots de passe : sans changement.

Ce qui ne l'est pas, et ce n'est pas de mon fait :

  • postgres et mlflow sont recréés à chaque passage. ev-postgres affichait
    « 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 conteneur
    recréé à 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: always a sans doute une raison que je ne connais pas, et c'est le rôle de
Gabriel.

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_file lu côté client.

Le banc d'essai

Refait après ta démonstration. Il lit maintenant main.yml au lieu d'en recopier les
expressions, 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.sh pose /etc/enervision en
0710 root:deploy et mlflow.env en 0640, au lieu de contredire la documentation.

Tu avais raison de refuser sans exécution. Elle est faite, deux fois, par `workflow_dispatch` sur cette branche et sans fusionner. ### Le critère qui tranche ``` 16:15:48 relevé 1 : 7 objets sous endpoint=current 16:18:58 relevé 2 : 28 objets croissance : 21 objets en 3 minutes ``` 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 : ``` collector.current site=SITE007 cle=source=mock_api/endpoint=current/site_id=SITE007/ dt=2026-09-03/hour=14/20260903T141500Z.json http=200 collector.current passe terminée : 7/7 objets écrits, 7 avec payload ``` Et la ligne de crontab telle qu'elle est posée, avec la variable dont l'absence faisait sortir la relève chaque minute : ``` * * * * * ENERVISION_RACINE=/opt/enervision/.repo ENERVISION_PYTHON=/opt/enervision/venv/bin/python /opt/enervision/.repo/services/collector/bin/collecte-current.sh ``` ### État des deux bases après le déploiement | | `enervision_prod` | `enervision_preprod` | |---|---|---| | Migrations appliquées | 15 | 15 | | Tables | 11 | 11 | | Objets hors du rôle applicatif | **0** | **0** | Le `attendu : 0` du runbook tient : ton exclusion `pg_depend` fonctionne, et `alter table … owner to` emporte 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 : - **la crontab** : 2 lignes après deux exécutions, pas 4. Le module `cron` pose par nom ; - **l'environnement Python** : intact, aucune réinstallation ; - **les migrations** : « Base à jour », rien de réappliqué ; - **la propriété et les mots de passe** : sans changement. Ce qui ne l'est pas, et ce n'est pas de mon fait : - **`postgres` et `mlflow` sont recréés à chaque passage.** `ev-postgres` affichait « 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 conteneur recréé à 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: always` a sans doute une raison que je ne connais pas, et c'est le rôle de Gabriel. ### 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_file` lu côté client. ### Le banc d'essai Refait après ta démonstration. Il lit maintenant `main.yml` au lieu d'en recopier les expressions, 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.sh` pose `/etc/enervision` en `0710 root:deploy` et `mlflow.env` en `0640`, au lieu de contredire la documentation.
Merge branch 'develop' into lenaic/113-deploiement-complet
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 53s
Intégration / Tests unitaires et couverture (pull_request) Successful in 55s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 14s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Tableau de bord (pull_request) Successful in 21s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m30s
Infra Ansible / Playbooks Ansible valides (pull_request) Successful in 1m8s
73df1acb6b
olivier approved these changes 2026-09-03 14:30:32 +00:00
lenaic merged commit f4f4d7ee0e into develop 2026-09-03 14:34:07 +00:00
lenaic deleted branch lenaic/113-deploiement-complet 2026-09-03 14:34:07 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
g2/enervision!114
No description provided.