infra: pose le registre de modèles MLflow sur PostgreSQL et MinIO #100

Merged
lenaic merged 1 commit from olivier/30-mlflow-registre into develop 2026-09-03 08:06:14 +00:00
Member

Ce que ça change

Un registre de modèles MLflow, en conteneur, qui garde son suivi dans la base
mlflow de PostgreSQL et écrit ses artefacts dans le bucket mlflow de MinIO.
Il est en service sur ml-stagiaire-02 depuis le 2 septembre : cette demande
fait entrer dans le dépôt ce qui tourne déjà.

Closes #30

Preuve

Les quatre critères du ticket, relevés sur le serveur à l'ouverture de cette
demande.

$ sudo docker compose -f /opt/enervision/mlflow/docker-compose.yml ps
NAME        STATUS
ev-mlflow   Up 18 hours (healthy)

$ sudo ss -ltnp | grep :5000
LISTEN 0      2048       127.0.0.1:5000       0.0.0.0:*    users:(("python3.12",pid=1534376,fd=3),("python3.12",pid=1534375,fd=3),("python3.12",pid=1534372,fd=3))

$ psql -d mlflow -tAc "<tables et proprietaire>"
59 tables dans public, possedees par mlflow

$ sudo docker exec -i ev-mlflow python - < exemple-execution.py
Contrôle de fumée : OK
  expérience   : controle-de-fumee (id 1)
  exécution    : b6a0c3d7340e4f9badd3ae9ccdc30f38
  artefacts    : mlflow-artifacts:/1/b6a0c3d7340e4f9badd3ae9ccdc30f38/artifacts
  artefact relu : temoin.txt, 60 octets, identique

À retrouver dans l'interface, puis à prouver côté stockage :
  mc ls --recursive local/mlflow/1/

$ mc ls --recursive local/mlflow/  (identifiants racine)
[2026-09-02 14:29:44 UTC]    62B STANDARD 1/5d86af005cd24163b3a22fea72a6d6ef/artifacts/temoin.txt
[2026-09-02 14:25:19 UTC]    62B STANDARD 1/78c9b63010724f24b97f3cb5dec57823/artifacts/temoin.txt
[2026-09-03 07:57:44 UTC]    62B STANDARD 1/b6a0c3d7340e4f9badd3ae9ccdc30f38/artifacts/temoin.txt

$ cloisonnement de l utilisateur dedie
identifiant : mlflow-7ffe27e5
  bronze : AccessDenied (attendu)
  silver : AccessDenied (attendu)
  gold : AccessDenied (attendu)

$ pytest tests/integration/test_registre_mlflow.py
..............                                                           [100%]
14 passed in 0.67s

Depuis un poste tiers de la salle — ENF-14 :

$ depuis un poste tiers de la salle (mon poste)
$ curl -sS --max-time 5 http://10.105.200.41:5000/health  -> curl: (28) Connection timed out after 5006 milliseconds
$ nc -z 10.105.200.41 22    -> ouvert
$ nc -z 10.105.200.41 5000  -> refusé
$ nc -z 10.105.200.41 5433  -> refusé
$ nc -z 10.105.200.41 9000  -> refusé
$ nc -z 10.105.200.41 9001  -> refusé

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/mlflow.md créé — installation, contrôles, tunnel,
    promotion d'un modèle, rotation des identifiants, panne, retour arrière
  • docs/adr/0004 — les artefacts passent par le serveur
  • Le fichier de secrets /etc/enervision/mlflow.env est produit par
    genere-identifiants.sh, pas rempli à la main : il n'y a donc pas de
    .env.example, et le README de la pile dit pourquoi

Où regarder en priorité

1. Le mandataire d'artefacts, et ce qu'il donne aux #36 et #37. Le serveur
relaie les artefacts (--serve-artifacts). Un client d'entraînement ou
d'inférence n'a besoin que de MLFLOW_TRACKING_URI : aucun identifiant MinIO
ne quitte le serveur
, et un seul tunnel SSH sert le suivi et les artefacts.
C'est l'ADR 0004, et c'est le contrat écrit au §4 du manuel.

2. L'utilisateur MinIO dédié, et le fait qu'il s'éprouve lui-même.
genere-identifiants.sh crée un utilisateur limité au seul bucket mlflow,
puis vérifie qu'il ne peut pas lire bronze, silver ni gold avant
d'écrire le fichier de secrets
— et retire l'utilisateur qu'il vient de créer
s'il constate le contraire. Le test d'intégration rejoue ce contrôle. Ce n'est
pas init-buckets.sh qui est modifié : rien du #28 n'est touché.

3. Deux secrets, deux placements différents, et c'est délibéré. Le mot de
passe PostgreSQL n'est pas dans l'URI de suivi mais dans PGPASSWORD, que libpq
lit dans l'environnement : dans la ligne de commande, il serait lisible par tout
compte de la machine à travers /proc/<pid>/cmdline. Il reste visible dans
docker inspect, qui exige l'accès à Docker — les deux ne sont pas comparables.
À l'inverse, MLFLOW_S3_ENDPOINT_URL n'est pas dans le fichier de secrets
mais dans la composition versionnée : mesuré le 2 septembre, absent, boto3 se
rabat sur le vrai AWS S3 et le client attend deux minutes sans qu'aucun
message ne dise que la requête part vers Internet.

4. Aucun volume, et c'est le point. Tout l'état est dans les deux magasins,
tous deux sauvegardés — le vidage nocturne prend la base mlflow sans qu'on ait
rien à déclarer, puisqu'il lit le catalogue. Le conteneur est jetable. C'est ce
que le magasin fichier par défaut de MLflow ne permet pas, et que le « Risque »
du ticket demandait d'éviter.

5. network_mode: host n'est pas un choix de style. Les deux magasins
n'écoutent que sur la boucle locale de l'hôte : en réseau bridge,
127.0.0.1 désignerait le conteneur et le serveur tomberait à la première
écriture. À noter pour le journal : le réseau bridge fonctionne désormais
sur ce LXC (MinIO tourne ainsi), contrairement au constat du 31 août — la raison
de rester en réseau hôte n'est plus celle-là.

Conflit connu

docs/adr/README.md : la PR #97 de Lénaïc insère sa ligne au même endroit que
la mienne. J'ai pris 0004 et laissé 0003 à sa fiche, qui était dans la
file avant moi — il ne reste qu'une ligne de tableau à démêler, dans l'ordre où
les deux seront fusionnées.

Ce qui n'est pas dans cette demande

pyfunc.load_model est la seule commande du manuel qui n'a pas été jouée : elle
exige un vrai artefact de modèle, et le contrôle de fumée n'écrit qu'un fichier
texte. Le manuel le signale à cet endroit précis. Les étapes 1 à 3 de la
promotion et le retour arrière, eux, ont été éprouvés sur un modèle jetable
supprimé ensuite.

## Ce que ça change Un registre de modèles MLflow, en conteneur, qui garde son suivi dans la base `mlflow` de PostgreSQL et écrit ses artefacts dans le bucket `mlflow` de MinIO. Il est **en service sur `ml-stagiaire-02` depuis le 2 septembre** : cette demande fait entrer dans le dépôt ce qui tourne déjà. Closes #30 ## Preuve Les quatre critères du ticket, relevés sur le serveur à l'ouverture de cette demande. ``` $ sudo docker compose -f /opt/enervision/mlflow/docker-compose.yml ps NAME STATUS ev-mlflow Up 18 hours (healthy) $ sudo ss -ltnp | grep :5000 LISTEN 0 2048 127.0.0.1:5000 0.0.0.0:* users:(("python3.12",pid=1534376,fd=3),("python3.12",pid=1534375,fd=3),("python3.12",pid=1534372,fd=3)) $ psql -d mlflow -tAc "<tables et proprietaire>" 59 tables dans public, possedees par mlflow $ sudo docker exec -i ev-mlflow python - < exemple-execution.py Contrôle de fumée : OK expérience : controle-de-fumee (id 1) exécution : b6a0c3d7340e4f9badd3ae9ccdc30f38 artefacts : mlflow-artifacts:/1/b6a0c3d7340e4f9badd3ae9ccdc30f38/artifacts artefact relu : temoin.txt, 60 octets, identique À retrouver dans l'interface, puis à prouver côté stockage : mc ls --recursive local/mlflow/1/ $ mc ls --recursive local/mlflow/ (identifiants racine) [2026-09-02 14:29:44 UTC] 62B STANDARD 1/5d86af005cd24163b3a22fea72a6d6ef/artifacts/temoin.txt [2026-09-02 14:25:19 UTC] 62B STANDARD 1/78c9b63010724f24b97f3cb5dec57823/artifacts/temoin.txt [2026-09-03 07:57:44 UTC] 62B STANDARD 1/b6a0c3d7340e4f9badd3ae9ccdc30f38/artifacts/temoin.txt $ cloisonnement de l utilisateur dedie identifiant : mlflow-7ffe27e5 bronze : AccessDenied (attendu) silver : AccessDenied (attendu) gold : AccessDenied (attendu) $ pytest tests/integration/test_registre_mlflow.py .............. [100%] 14 passed in 0.67s ``` Depuis un poste tiers de la salle — ENF-14 : ``` $ depuis un poste tiers de la salle (mon poste) $ curl -sS --max-time 5 http://10.105.200.41:5000/health -> curl: (28) Connection timed out after 5006 milliseconds $ nc -z 10.105.200.41 22 -> ouvert $ nc -z 10.105.200.41 5000 -> refusé $ nc -z 10.105.200.41 5433 -> refusé $ nc -z 10.105.200.41 9000 -> refusé $ nc -z 10.105.200.41 9001 -> refusé ``` ## 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/mlflow.md` créé — installation, contrôles, tunnel, **promotion d'un modèle**, rotation des identifiants, panne, retour arrière - [x] `docs/adr/0004` — les artefacts passent par le serveur - [x] Le fichier de secrets `/etc/enervision/mlflow.env` est **produit** par `genere-identifiants.sh`, pas rempli à la main : il n'y a donc pas de `.env.example`, et le README de la pile dit pourquoi ## Où regarder en priorité **1. Le mandataire d'artefacts, et ce qu'il donne aux #36 et #37.** Le serveur relaie les artefacts (`--serve-artifacts`). Un client d'entraînement ou d'inférence n'a besoin que de `MLFLOW_TRACKING_URI` : **aucun identifiant MinIO ne quitte le serveur**, et un seul tunnel SSH sert le suivi et les artefacts. C'est l'ADR 0004, et c'est le contrat écrit au §4 du manuel. **2. L'utilisateur MinIO dédié, et le fait qu'il s'éprouve lui-même.** `genere-identifiants.sh` crée un utilisateur limité au seul bucket `mlflow`, puis **vérifie qu'il ne peut pas lire `bronze`, `silver` ni `gold` avant d'écrire le fichier de secrets** — et retire l'utilisateur qu'il vient de créer s'il constate le contraire. Le test d'intégration rejoue ce contrôle. Ce n'est pas `init-buckets.sh` qui est modifié : rien du #28 n'est touché. **3. Deux secrets, deux placements différents, et c'est délibéré.** Le mot de passe PostgreSQL n'est pas dans l'URI de suivi mais dans `PGPASSWORD`, que libpq lit dans l'environnement : dans la ligne de commande, il serait lisible par tout compte de la machine à travers `/proc/<pid>/cmdline`. Il reste visible dans `docker inspect`, qui exige l'accès à Docker — les deux ne sont pas comparables. À l'inverse, `MLFLOW_S3_ENDPOINT_URL` **n'est pas** dans le fichier de secrets mais dans la composition versionnée : mesuré le 2 septembre, absent, boto3 se rabat sur le **vrai AWS S3** et le client attend deux minutes sans qu'aucun message ne dise que la requête part vers Internet. **4. Aucun volume, et c'est le point.** Tout l'état est dans les deux magasins, tous deux sauvegardés — le vidage nocturne prend la base `mlflow` sans qu'on ait rien à déclarer, puisqu'il lit le catalogue. Le conteneur est jetable. C'est ce que le magasin fichier par défaut de MLflow ne permet pas, et que le « Risque » du ticket demandait d'éviter. **5. `network_mode: host` n'est pas un choix de style.** Les deux magasins n'écoutent que sur la boucle locale de l'**hôte** : en réseau bridge, `127.0.0.1` désignerait le conteneur et le serveur tomberait à la première écriture. À noter pour le journal : **le réseau bridge fonctionne** désormais sur ce LXC (MinIO tourne ainsi), contrairement au constat du 31 août — la raison de rester en réseau hôte n'est plus celle-là. ## Conflit connu `docs/adr/README.md` : la PR #97 de Lénaïc insère sa ligne au même endroit que la mienne. J'ai pris **0004** et laissé **0003** à sa fiche, qui était dans la file avant moi — il ne reste qu'une ligne de tableau à démêler, dans l'ordre où les deux seront fusionnées. ## Ce qui n'est pas dans cette demande `pyfunc.load_model` est la seule commande du manuel qui n'a pas été jouée : elle exige un vrai artefact de modèle, et le contrôle de fumée n'écrit qu'un fichier texte. Le manuel le signale à cet endroit précis. Les étapes 1 à 3 de la promotion et le retour arrière, eux, ont été éprouvés sur un modèle jetable supprimé ensuite.
infra: pose le registre de modèles MLflow sur PostgreSQL et MinIO
All checks were successful
Intégration / Qualité du code Python (pull_request) Successful in 45s
Intégration / Tests unitaires et couverture (pull_request) Successful in 54s
Intégration / Dépendances du tableau de bord (pull_request) Successful in 7s
Intégration / Images épinglées par version (pull_request) Successful in 3s
Intégration / Aucun secret commité (pull_request) Successful in 3s
Intégration / Dépendances Python et inventaire applicatif (pull_request) Successful in 1m14s
6e3c7c10d1
Le serveur écoute sur 127.0.0.1:5000 en réseau hôte, garde son suivi dans la
base `mlflow` de PostgreSQL et écrit ses artefacts dans le bucket `mlflow` de
MinIO. Aucun volume : tout l'état est dans les deux magasins, tous deux
sauvegardés, et le conteneur est jetable. C'est précisément ce que le magasin
fichier par défaut de MLflow ne permet pas, et que le ticket demandait d'éviter.

Les artefacts passent par le serveur (ADR 0003) : un client d'entraînement ou
d'inférence n'a besoin que de MLFLOW_TRACKING_URI, et les identifiants MinIO ne
quittent pas la machine. Ce sont ceux d'un utilisateur MinIO dédié, limité au
seul bucket mlflow par une politique que le générateur éprouve avant d'écrire —
et dont il retire l'utilisateur s'il constate le contraire.

Le mot de passe PostgreSQL n'est pas dans l'URI de suivi mais dans PGPASSWORD,
que libpq lit dans l'environnement : dans la ligne de commande, il serait
lisible par tout compte de la machine à travers /proc. Le point de terminaison
S3, lui, est dans la composition versionnée et non dans le fichier de secrets :
absent, boto3 se rabat sur le vrai AWS S3 sans rien en dire.

En service sur ml-stagiaire-02 depuis le 2 septembre, les quatre critères
éprouvés, test d'intégration à 14 cas sur 14.

Closes #30

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lenaic approved these changes 2026-09-03 08:06:10 +00:00
lenaic merged commit 2612841074 into develop 2026-09-03 08:06:14 +00:00
lenaic deleted branch olivier/30-mlflow-registre 2026-09-03 08:06:14 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 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!100
No description provided.