docs: décrit la CI unifiée, l'e2e, la charge et la supervision
- ADR 0014 : un pipeline CI unique, « CI ok » seul check à exiger, déploiement du commit testé. - ADR 0015 : e2e et charge contre la stack Compose déployée, hypothèses et seuils de k6. - ADR 0016 : supervision en profil Compose, active en prod, rôle en lecture seule. - Nouvelle vue 60-observabilite.md ; 00-vue-ensemble, 10-infra, 20-backend et 50-cicd mis à jour (monitoring passé à Fait, nouveau graphe de CI, gates, ports). - README racine et guide de tests du frontend : où sont l'e2e, la charge et la supervision.
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# 0014 - Un pipeline CI unique appelle les workflows de composant et conditionne le déploiement
|
||||
|
||||
- Statut : accepté
|
||||
- Date : 2026-09-23
|
||||
- Complète : [0009](0009-deux-environnements-compose-sur-la-vm-eni.md), qui reste en vigueur
|
||||
|
||||
## Contexte
|
||||
|
||||
Au 22/09, huit workflows se déclenchaient chacun de leur côté, et l'audit y a relevé :
|
||||
|
||||
- **Double exécution.** Chaque workflow partait sur `push` (toutes branches) **et** sur
|
||||
`pull_request`. Un commit poussé sur une branche de PR jouait donc toute la CI deux fois,
|
||||
à la même minute (constaté dans l'historique des runs de `test/integration-api-db-ml`).
|
||||
- **Sonar refaisait tout.** `sonarqube.yml` reconstruisait le frontend et retestait frontend,
|
||||
backend et ML pour produire ses rapports de couverture, en double exact de `frontend.yml`,
|
||||
`backend.yml` et `ml.yml`. Son test backend tournait sans `uv sync`. Il n'avait ni
|
||||
`permissions` ni `concurrency`.
|
||||
- **Déploiement non conditionné.** `deploy.yml` partait à chaque push sur `dev` ou `main`,
|
||||
que la CI du commit soit verte ou non, et déployait la pointe de branche du moment plutôt
|
||||
que le commit poussé.
|
||||
- **Erreurs silencieuses et hygiène.**
|
||||
- `npm test --watch=false --code-coverage` : npm garde ces options pour lui, `ng test` ne
|
||||
les reçoit jamais, et la CI ne tenait que par les réglages d'`angular.json`.
|
||||
- `uv sync --frozen` ne vérifie pas que `uv.lock` suit `pyproject.toml`.
|
||||
- Plusieurs actions tierces étaient épinglées par tag, contrairement à la règle Sonar
|
||||
`githubactions:S7637`.
|
||||
- Aucun job n'avait de `timeout-minutes` (360 minutes par défaut).
|
||||
|
||||
## Décision
|
||||
|
||||
**`ci.yml` est le seul workflow déclenché par `pull_request` et par les push sur `dev` et
|
||||
`main`.** Les workflows de composant (`backend`, `frontend`, `ml`, `airflow`, `infra`, `e2e`)
|
||||
passent en `workflow_call` et n'ont plus de déclencheur propre.
|
||||
|
||||
1. **`changes`.** Un job initial calcule, par `dorny/paths-filter` épinglé sur un SHA, les
|
||||
composants touchés par la PR, et chaque composant n'est appelé que si son filtre vaut vrai.
|
||||
Sur un push vers `dev` ou `main`, tous les filtres valent vrai : l'analyse Sonar reste
|
||||
complète sur les branches longues, et paths-filter ne compare pas à la base de fusion avec
|
||||
`main`, qui a 80 commits de retard.
|
||||
2. **`sonar`.** Il ne reconstruit ni ne reteste plus rien : il télécharge, dans le même run, les
|
||||
couvertures versées par les jobs `verification` des composants.
|
||||
3. **`CI ok`.** Le job agrège le résultat de tous les autres. Il tourne toujours (`if:
|
||||
always()`) et échoue dès qu'un job est en `failure` ou `cancelled`. **C'est le seul check à
|
||||
exiger dans les règles de branche** : un composant sauté par son filtre ne publie aucun check
|
||||
interne, qui resterait « en attente » s'il était exigé.
|
||||
4. **`deploy`.** Il appelle `deploy.yml`, sur les seuls push, et seulement si `CI ok` a réussi.
|
||||
`deploy.yml` aligne le dossier de l'environnement sur `GITHUB_SHA`, le commit testé.
|
||||
|
||||
`deploy.yml` n'a toujours **aucun déclencheur `pull_request`** : il n'accepte que
|
||||
`workflow_call` et `workflow_dispatch`, dans l'esprit de l'ADR 0009.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
| Écartée | Raison |
|
||||
|---|---|
|
||||
| Garder huit workflows et restreindre seulement `push` à `dev` et `main` | Supprime la double exécution, pas le doublon Sonar : il faudrait toujours rejouer les tests pour que Sonar ait ses couvertures, les artefacts ne passant pas d'un workflow à l'autre. Et rien n'empêche un déploiement rouge. |
|
||||
| Déclencher le déploiement par `workflow_run` | `workflow_run` joue toujours le fichier de la branche par défaut, `main`, en retard de 80 commits : la recette ne se serait plus déployée avant la prochaine remontée vers `main`, sans erreur visible. |
|
||||
| `alls-green` ou une action tierce d'agrégation | Dix lignes de shell sur `toJSON(needs.*.result)` font le même travail, sans dépendance de plus à épingler. |
|
||||
| Cache de couches Docker (`bake-action`, `type=gha`) pour l'e2e | Quatre pièges (noms d'image, cibles Compose, buildx, `load`) pour deux à quatre minutes gagnées. Reporté après le rendu. |
|
||||
|
||||
## Conséquences
|
||||
|
||||
- Une PR ne joue que ce qu'elle touche. Une PR de documentation ne joue que `changes` et
|
||||
`CI ok`.
|
||||
- Les checks s'appellent désormais « Backend / Lint, typage et tests », etc. Au 23/09, ni `dev`
|
||||
ni `main` n'ont de règle de protection : à la première, exiger **« CI ok »** et rien d'autre.
|
||||
- Modifier `ci.yml` rejoue toute la CI sur la PR (filtre `ci`).
|
||||
- Le job `deploy` reste en file tant que le runner `eni-g3` n'est pas enregistré sur la VM,
|
||||
comme avant. Le groupe de concurrence par SHA des push l'empêche de bloquer les runs suivants.
|
||||
- La sécurité du runner auto-hébergé ne repose pas sur l'absence de `pull_request` dans
|
||||
`deploy.yml`. Une PR de fork peut ajouter son propre workflow. Ce qui protège le runner :
|
||||
- l'approbation obligatoire des workflows de tous les contributeurs externes ;
|
||||
- les règles de branche des environnements `rec` (`dev`) et `prod` (`main` et un relecteur).
|
||||
|
||||
Ces deux réglages restent à poser par l'administratrice du dépôt.
|
||||
@@ -0,0 +1,70 @@
|
||||
# 0015 - Les tests de bout en bout et de charge visent la stack Compose déployée
|
||||
|
||||
- Statut : accepté
|
||||
- Date : 2026-09-23
|
||||
|
||||
## Contexte
|
||||
|
||||
Les issues #46 (Playwright) et #47 (k6) demandent des preuves de robustesse pour EC03 et EC04.
|
||||
Rien ne vérifiait un parcours utilisateur complet : les tests du frontend simulent l'API, ceux
|
||||
du backend n'ouvrent pas de navigateur. Rien ne mesurait non plus l'API sous charge, et le
|
||||
dépôt ne chiffre aucun temps de réponse ni aucun volume d'utilisateurs.
|
||||
|
||||
Trois contraintes du système pèsent sur la manière de tester :
|
||||
|
||||
- **La session tient dans un cookie de refresh HttpOnly qui tourne à chaque usage.** Rejouer un
|
||||
cookie déjà servi révoque toute la famille de session (ADR 0002).
|
||||
- **Le cookie n'est `__Secure-` et `Secure` que hors `local`, derrière le proxy TLS.** Tester
|
||||
contre `ng serve` ne dit rien de ce que voit un navigateur en prod (ADR 0007).
|
||||
- **nginx limite chaque adresse IP** à 20 req/s sur l'API, avec une rafale de 40, et à 30
|
||||
connexions par minute, avec une rafale de 20 (ADR 0007). Tout le trafic d'un tir parti d'une
|
||||
seule machine partage la même adresse.
|
||||
|
||||
## Décision
|
||||
|
||||
**Playwright joue contre la stack de prod** (`docker-compose.yml` et
|
||||
`docker-compose.prod.yml`), sur `https://localhost` avec un certificat auto-signé.
|
||||
- **En CI**, le workflow `e2e.yml` démarre `db`, `mailpit`, `backend`, `frontend` et `proxy`,
|
||||
sème `db/seeds/demo.sql` et crée les comptes par `scripts/comptes-test.sh`.
|
||||
- **Sur le poste**, la même suite vise `make dev` (`http://localhost:4200`).
|
||||
- **Écriture des tests**, imposée par la rotation du refresh et par la zone `auth` :
|
||||
- un seul worker ;
|
||||
- une session par fichier, sans `storageState` partagé ;
|
||||
- chaque parcours qui consomme un compte le crée lui-même.
|
||||
|
||||
**k6 tourne en service Compose (profil `load`) sur le réseau du projet et vise `backend:8000`**,
|
||||
pour mesurer l'API et non la limite de nginx. Un seul scénario, `limitation-debit.js`, passe par
|
||||
`https://proxy`, pour vérifier que la limite tient : des 429, jamais de 5xx.
|
||||
|
||||
**Hypothèses et seuils**, faute d'exigence chiffrée :
|
||||
|
||||
| Hypothèse ou seuil | Valeur |
|
||||
|---|---|
|
||||
| Utilisateurs simultanés | 50 : 40 sur le tableau de bord, qui interroge `/stats/summary` toutes les 10 s et `/alerts` toutes les 60 s ; 10 qui explorent les sites |
|
||||
| Lectures, p95 | < 500 ms |
|
||||
| Lectures, p99 | < 1 s |
|
||||
| Échecs HTTP | < 1 % |
|
||||
| Vérifications réussies | > 99 % |
|
||||
|
||||
**En CI de PR** : Playwright, le tir `smoke` (une minute) et `limitation-debit`. La charge
|
||||
nominale et le stress se lancent à la main (`make load-test`, `make load-stress`), en recette,
|
||||
parce que rec et prod partagent la VM (ADR 0009).
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
| Écartée | Raison |
|
||||
|---|---|
|
||||
| Playwright contre `ng serve` en CI | Pas de TLS, pas de cookie `__Secure-`, pas de CSP ni de limitation : le parcours testé ne serait pas celui des utilisateurs. |
|
||||
| `storageState` partagé entre fichiers | Chaque fichier rejouerait le même cookie de refresh ; le second usage révoque la famille, et la suite échoue de façon intermittente selon l'ordre. |
|
||||
| k6 depuis le runner, à travers le proxy | Au-delà de 20 req/s, on mesure nginx. Relever la limite pour le tir, ce serait tester une configuration qui n'est pas celle de la prod. |
|
||||
| Tir de charge complet à chaque PR | Huit minutes de plus par PR, sur un runner partagé dont les performances varient d'un run à l'autre : un seuil franchi n'y voudrait rien dire. |
|
||||
| Un workflow k6 en `workflow_dispatch` contre la recette | La recette partage la VM avec la prod ; un tir déclenché d'un clic ralentirait la prod sans que personne soit prévenu. La cible Makefile, lancée sur la VM, garde un humain dans la boucle. |
|
||||
|
||||
## Conséquences
|
||||
|
||||
- La CI construit enfin les images backend et frontend avant le déploiement, par le job E2E.
|
||||
- `db/seeds/demo.sql` et `scripts/comptes-test.sh` deviennent le jeu commun de la CI, repris par
|
||||
le DAST. Les deux sont réservés aux bases jetables.
|
||||
- Les seuils de k6 sont des hypothèses de l'équipe : à réviser dès qu'un besoin chiffré existe.
|
||||
- L'API expose des seaux de latence fins autour de 500 ms, pour que Grafana lise le même seuil
|
||||
que k6 (ADR 0016).
|
||||
@@ -0,0 +1,61 @@
|
||||
# 0016 - La supervision vit dans un profil Compose, active en prod
|
||||
|
||||
- Statut : accepté
|
||||
- Date : 2026-09-23
|
||||
|
||||
## Contexte
|
||||
|
||||
L'API expose `/metrics` au format Prometheus depuis le début, et `monitoring/` ne contenait que
|
||||
des `.gitkeep` : aucun collecteur, aucun tableau de bord, aucune alerte (issue #26). La VM ENI
|
||||
porte la recette et la prod, deux piles complètes, sur 8 Go de mémoire (ADR 0009).
|
||||
|
||||
## Décision
|
||||
|
||||
**Prometheus, Alertmanager, Grafana et trois exporteurs** sont des services de
|
||||
`docker-compose.yml` sous le profil `monitoring` : postgres-exporter, node-exporter et cAdvisor.
|
||||
|
||||
- **En prod**, `COMPOSE_PROFILES=monitoring` dans le `.env` : `make stack-up`, donc chaque
|
||||
déploiement, les démarre avec le reste.
|
||||
- **En recette et sur le poste**, ils se lancent à la demande (`make monitoring-up`, en
|
||||
`--no-deps`). La recette ne paie rien tant qu'on ne les lance pas.
|
||||
- **Mémoire.** Chaque service a un `mem_limit`, pour environ 700 Mo au total.
|
||||
- **Accès.** Les interfaces n'écoutent que sur `127.0.0.1` et se consultent par tunnel SSH,
|
||||
comme Airflow. Rien ne passe par le proxy : Grafana derrière nginx exigerait sa propre
|
||||
authentification forte, et rendrait `/metrics` joignable à un routage près (ADR 0007).
|
||||
- **Sécurité.**
|
||||
- **Jeton.** Prometheus présente sur `/metrics` le jeton `APP_METRICS_TOKEN`, passé en
|
||||
secret Compose. Il est exigé dès que la supervision tourne.
|
||||
- **Lecture de la base.** Grafana et l'exportateur lisent la base par un rôle `supervision`
|
||||
en lecture seule, limité aux tables métier (`db/roles/supervision.sql`). Ils n'ont
|
||||
jamais accès à `app_user`, aux jetons ni à l'audit.
|
||||
- **Alertes.**
|
||||
- Neuf règles couvrent l'API, la base, l'hôte et les cibles, chacune avec un cas de test
|
||||
joué par `promtool test rules` en CI.
|
||||
- Alertmanager les envoie par courriel à Mailpit, le seul SMTP de la stack.
|
||||
- **Dérive du modèle.** Elle s'affiche dans Grafana par une lecture SQL de `drift_report`.
|
||||
L'ADR 0013 a écarté une jauge Prometheus calculée par un traitement par lot, pas la lecture
|
||||
de sa table.
|
||||
|
||||
## Alternatives écartées
|
||||
|
||||
| Écartée | Raison |
|
||||
|---|---|
|
||||
| Supervision démarrée dans les deux environnements | Double la mémoire consommée sur une VM déjà serrée, pour des tableaux de recette que personne ne regarde. |
|
||||
| Une pile de supervision partagée, troisième projet Compose | Elle devrait rejoindre les réseaux des deux projets, par des réseaux externes à déclarer sur la VM : plus de pièces, et un couplage entre environnements que l'ADR 0009 évite. |
|
||||
| Publier Grafana derrière le proxy | Une interface d'administration de plus exposée au réseau de l'école, et un pas de plus vers une publication accidentelle de `/metrics`. |
|
||||
| Grafana avec le compte applicatif de la base | Le compte applicatif écrit partout, y compris dans `app_user`. Une requête libre dans Grafana y aurait accès. |
|
||||
| Une jauge de fraîcheur des relevés calculée par l'API au moment du scrape | Une requête SQL dans un collecteur synchrone, à chaque scrape. La même information se lit directement dans TimescaleDB depuis Grafana. |
|
||||
|
||||
## Conséquences
|
||||
|
||||
- **Secrets.** `.env.example` gagne `COMPOSE_PROFILES`, `APP_METRICS_TOKEN`,
|
||||
`GRAFANA_ADMIN_PASSWORD`, `SUPERVISION_DB_PASSWORD` et les ports.
|
||||
`scripts/provision-host.sh` génère ces secrets pour un nouvel environnement. Le `.env` d'un
|
||||
environnement déjà provisionné n'est jamais réécrit : il faut les y ajouter à la main.
|
||||
`make stack-up` refuse de démarrer si le profil est actif et qu'un secret manque.
|
||||
- **Instrumentation.** L'API ne compte plus les sondes de santé dans ses métriques, et chaque
|
||||
application a son propre registre Prometheus.
|
||||
- **cAdvisor tourne en `privileged`**, avec des montages en lecture seule et sans port publié.
|
||||
C'est le prix de la mémoire par conteneur, l'indicateur qui compte le plus sur une VM partagée.
|
||||
- **Données non couvertes.** L'ingestion et les DAG Airflow n'ont pas encore de métriques
|
||||
(StatsD ou OpenTelemetry). Le tableau « Données » les supplée en lisant `reading`.
|
||||
Reference in New Issue
Block a user