diff --git a/.env.example b/.env.example index 56198db..3218698 100644 --- a/.env.example +++ b/.env.example @@ -82,16 +82,15 @@ GARAGE_SSE_KEY=change_me PUBLIC_HOST=enervision.local ACME_EMAIL= -# Deux environnements sur la même machine (ADR 0009) : un dossier, un `.env` et un projet Compose -# chacun. Le nom de projet préfixe volumes, réseau et conteneurs et l'emporte sur `name:`. +# Trois environnements sur la même machine (ADR 0009, 0017) : un dossier, un `.env` et un projet +# Compose chacun. Le nom de projet préfixe volumes, réseau et conteneurs et l'emporte sur `name:`. # Vide sur un poste de développement : le projet reste `enervision`. COMPOSE_PROJECT_NAME= # Origine publique, avec le port si le proxy HTTPS n'écoute pas 443. Vide : https://PUBLIC_HOST. -# Recette : PUBLIC_HOST=rec.enervision.local et PUBLIC_ORIGIN=https://rec.enervision.local:8443. +# Sur la VM, provision-host.sh pose https://, sans port (frontal SNI). PUBLIC_ORIGIN= -# Ports publiés par le proxy. Vides : 80 et 443. Recette : PROXY_HTTPS_PORT=8443 et -# PROXY_HTTP_PORT=127.0.0.1:8081, la redirection vers 443 n'ayant pas à être joignable de -# l'extérieur. Décaler aussi POSTGRES_PORT, MAILPIT_UI_PORT et AIRFLOW_PORT (5434, 8026, 8082). +# Ports publiés par le proxy. Vides : 80 et 443. Sur la VM, provision-host.sh les pose sur 127.0.0.1, +# derrière le frontal SNI, et décale aussi base, Mailpit et Airflow par environnement. PROXY_HTTP_PORT= PROXY_HTTPS_PORT= # Écouteur PROXY protocol du proxy, que seul le frontal de la VM joint (infra/front, ADR 0018). diff --git a/README.md b/README.md index 42f2eb5..448c665 100644 --- a/README.md +++ b/README.md @@ -16,26 +16,27 @@ series temporelles energetiques, deployee sur une machine on-premise. Ce que la documentation apporte à chacun : [docs/architecture/00-vue-ensemble.md](docs/architecture/00-vue-ensemble.md). -## Stack cible +## Stack | Domaine | Technologie | Emplacement | Etat | |------------|-------------------------------------|---------------------|---------------| | Backend | FastAPI, Python 3.14 | `apps/backend` | En place | | Frontend | Angular 22, Node 26 | `apps/frontend` | En place | | Base | PostgreSQL 17 + TimescaleDB | `db` | En place | -| ETL | Apache Airflow | `etl/airflow` | Cinq DAGs | -| Infra | Terraform (k3s single-node) | `infra/terraform` | Initialise | -| Reverse proxy | Nginx, TLS | `infra/proxy` | En place | +| ETL | Apache Airflow | `etl/airflow` | Sept DAGs | +| Infra | Terraform (VM ENI ; module k3s) | `infra/terraform` | VM appliquée, k3s écrit non appliqué | +| Reverse proxy | Nginx, TLS, frontal SNI | `infra/proxy`, `infra/front` | En place, certificats Let's Encrypt | | CI/CD | GitHub Actions | `.github/workflows` | En place | | Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | En place, profil Compose | | Stockage objet | Garage (S3), un par environnement | `infra/garage` | En place, archives de `reading` | | Tests e2e et de charge | Playwright, k6 | `tests` | En place | | ML | LightGBM, MLflow | `ml` | En place | -Le backend, la base et l'infrastructure (Terraform/k3s) sont initialises a ce stade. Le frontend -sert un tableau de bord sur `/dashboard`, dont les données proviennent de fixtures : les endpoints -correspondants restent à écrire côté API. Les autres dossiers portent l'arborescence et un README -de cadrage, leur contenu fait l'objet d'un ticket dedie. +Toutes ces briques tournent sur la machine du groupe, en trois environnements (production, +recette, dev). Le frontend sert le tableau de bord, les vues sites, recommandations et +supervision des capteurs, toutes branchées sur l'API réelle : les fixtures sont coupées +(`useMockFixtures: false`). Le module Terraform k3s reste une cible, écrite et validée, jamais +appliquée. L'etat detaille de chaque brique et les vues d'architecture sont dans [docs/architecture](docs/architecture/README.md). @@ -53,11 +54,12 @@ L'etat detaille de chaque brique et les vues d'architecture sont dans │ ├── roles/ Roles PostgreSQL hors schema (supervision) │ └── seeds/ Jeu de demonstration des tests ├── etl/airflow/ -│ ├── dags/ DAGs d'orchestration (pipeline ML, alertes, imports, dérive) +│ ├── dags/ DAGs d'orchestration (pipeline ML, alertes, imports, dérive, rétention) │ ├── plugins/ Operateurs et hooks maison │ ├── include/ Requetes SQL et ressources des DAGs │ └── tests/ Tests d'integrite des DAGs ├── infra/ +│ ├── front/ Frontal SNI de la machine : ports 80 et 443, aiguillage par nom │ ├── garage/ Stockage objet S3 : configuration sans secret │ ├── proxy/ Reverse proxy Nginx : terminaison TLS et routage │ └── terraform/ @@ -72,13 +74,13 @@ L'etat detaille de chaque brique et les vues d'architecture sont dans │ ├── e2e/ Parcours Playwright contre la stack │ ├── garage/ Tests de fumée S3 joués par la CI contre Garage │ └── load/ Scenarios de charge k6 -├── docs/ ADR et vues d'architecture +├── docs/ ADR, vues d'architecture, runbook de pilotage, livrables de rendu └── scripts/ Outillage local ``` ## Demarrage -Prerequis : uv, Docker, Node 24 LTS (npm fourni). Le poste doit disposer de Python 3.14, que +Prerequis : uv, Docker, Node 26 (version de la CI et de l'image frontend, npm fourni). Le poste doit disposer de Python 3.14, que `uv` installe seul. ```bash @@ -148,16 +150,19 @@ L'overlay emploie `!override` et `!reset`, donc **Docker Compose 2.24.4 ou plus ```bash make tls-selfsigned PUBLIC_HOST=enervision.local # certificat de démonstration -make stack-up PUBLIC_HOST=enervision.local # nginx en 80/443, rien d'autre n'est publié +make stack-up PUBLIC_HOST=enervision.local # nginx en 80/443, le reste sur 127.0.0.1 ``` -Le navigateur avertit d'un émetteur inconnu : Let's Encrypt reste hors d'atteinte tant qu'aucun -nom de domaine public ne résout vers la machine. Routage, mode ACME et renouvellement dans +Le navigateur avertit d'un émetteur inconnu : sur le poste, le certificat est auto-signé. Sur la +machine, les certificats viennent de Let's Encrypt par défi DNS-01 +([ADR 0018](docs/adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md)). Routage, mode ACME et renouvellement dans [`infra/proxy/README.md`](infra/proxy/README.md) ; la décision et ses motifs dans [l'ADR 0007](docs/adr/0007-terminaison-tls-et-reverse-proxy-nginx.md). -Sur la VM ENI, deux environnements cohabitent, recette sur `dev` et production sur `main`, -chacun dans son dossier et son projet Compose : `scripts/provision-host.sh` les prépare, le +Sur la VM ENI, trois environnements cohabitent, production sur `main`, recette sur `dev`, et +`dev` pour toute autre branche lancée à la main +([ADR 0017](docs/adr/0017-environnement-dev-a-la-demande.md)), chacun dans son dossier et son +projet Compose, derrière un frontal SNI commun : `scripts/provision-host.sh` les prépare, le workflow `deploy.yml` les redéploie par un runner auto-hébergé, une fois la CI du commit poussé verte ([ADR 0014](docs/adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md)). Ports, noms d'hôte et garde-fous dans [`docs/architecture/10-infra.md`](docs/architecture/10-infra.md) et diff --git a/apps/frontend/TESTING.md b/apps/frontend/TESTING.md index 7db5d36..018c662 100644 --- a/apps/frontend/TESTING.md +++ b/apps/frontend/TESTING.md @@ -24,14 +24,14 @@ it('devrait faire X quand Y', () => { ## Ce qui doit être testé en priorité - Services (`core/services/`) : logique métier, gestion des erreurs - Guards et interceptors (`core/guards/`, `core/interceptors/`) : chaque branche de décision -- Composants avec logique (formulaires, conditions d'affichage) — pas nécessaire pour +- Composants avec logique (formulaires, conditions d'affichage), mais pas nécessaire pour un composant 100% template, sans logique `core/services/`, `core/guards/` et `core/interceptors/` n'existent pas encore : c'est l'arborescence cible, décrite dans [docs/architecture/30-frontend.md](../../docs/architecture/30-frontend.md). -## Gabarit — tester un service avec appel HTTP +## Gabarit · tester un service avec appel HTTP ```typescript import { TestBed } from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; @@ -61,7 +61,7 @@ describe('MonService', () => { }); ``` -## Gabarit — tester un composant standalone +## Gabarit · tester un composant standalone ```typescript import { TestBed } from '@angular/core/testing'; import { MonComposant } from './mon-composant'; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e9c2608..b50aa71 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -5,8 +5,8 @@ # moyen de dépublier 8000 et 3000 : sans lui, l'API resterait joignable en clair à côté du proxy. # Piège : pas de `:?` sur `PUBLIC_HOST`. Compose interpole tout le fichier, y compris pour # `stop` et `logs` : la garde vit dans `make stack-up`, qui la compare au certificat servi. -# Pourquoi : ports du proxy et origine publique en variables, pour que deux environnements -# cohabitent sur la même machine, chacun dans son projet Compose (ADR 0009). +# Pourquoi : ports du proxy et origine publique en variables, pour que trois environnements +# cohabitent sur la même machine, chacun dans son projet Compose (ADR 0009, 0017). name: enervision diff --git a/docs/README.md b/docs/README.md index 07b06cf..74ead45 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,11 @@ - `adr` : décisions d'architecture, une par fichier, numérotées et immuables. - `architecture` : les vues du système. Point d'entrée : [architecture/README.md](architecture/README.md). + Le pilotage des traitements automatisés a son runbook : + [architecture/70-pilotage.md](architecture/70-pilotage.md). +- `livrables` : rapports de rendu, le rapport collectif EC02 et le rapport de sécurisation EC04 + avec ses preuves. +- `dailies` : points d'avancement versionnés. ## Décisions en vigueur @@ -15,7 +20,7 @@ | [0006](adr/0006-moteur-de-regles-dans-le-backend.md) | Le moteur de règles de recommandation vit dans le backend, pas dans `ml/` | | [0007](adr/0007-terminaison-tls-et-reverse-proxy-nginx.md) | Terminaison TLS par un reverse proxy Nginx, en Docker Compose | | [0008](adr/0008-airflow-execute-le-code-du-backend.md) | Airflow exécute le code du backend en sous-processus, dans son propre environnement | -| [0009](adr/0009-deux-environnements-compose-sur-la-vm-eni.md) | Deux environnements sur la VM ENI, un projet Compose chacun, déployés par un runner auto-hébergé | +| [0009](adr/0009-deux-environnements-compose-sur-la-vm-eni.md) | Un projet Compose par environnement sur la VM ENI, déployé par un runner auto-hébergé (deux environnements à l'origine, trois depuis l'ADR 0017) | | [0010](adr/0010-terraform-provisionne-github-actions-deploie.md) | Terraform provisionne la machine, GitHub Actions déploie l'application | | [0011](adr/0011-enervision-procedure-deploiement.md) | Procédure de déploiement, telle qu'exécutée le 22/09/2026 | | [0012](adr/0012-enervision-deploiement-rec-prod-vm-eni.md) | État de la recette et de la production sur la VM ENI | @@ -23,3 +28,7 @@ | [0014](adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md) | Un pipeline CI unique appelle les workflows de composant et conditionne le déploiement | | [0015](adr/0015-tests-e2e-et-de-charge-contre-la-stack-compose.md) | Les tests de bout en bout et de charge visent la stack Compose déployée | | [0016](adr/0016-supervision-en-profil-compose.md) | La supervision vit dans un profil Compose, active en prod | +| [0017](adr/0017-environnement-dev-a-la-demande.md) | Un troisième environnement, `dev`, déployé à la demande depuis n'importe quelle branche | +| [0018](adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md) | Noms publics, certificats Let's Encrypt par DNS-01 et frontal SNI sans port | +| [0019](adr/0019-stockage-objet-garage-et-cycle-de-vie-des-mesures.md) | Stockage objet Garage par environnement, et cycle de vie des mesures : export puis suppression | +| [0020](adr/0020-chiffrement-au-repos-coffre-luks-et-sse-c.md) | Chiffrement au repos : coffre LUKS des volumes Docker et SSE-C des archives | diff --git a/docs/adr/0005-modele-prediction-lightgbm.md b/docs/adr/0005-modele-prediction-lightgbm.md index cb39524..4919176 100644 --- a/docs/adr/0005-modele-prediction-lightgbm.md +++ b/docs/adr/0005-modele-prediction-lightgbm.md @@ -13,7 +13,7 @@ contraintes non négociables cadrent le choix, discutées dans l'issue #89 : 1. **EC06** (grille de notation individuelle) exige un modèle **entraîné, versionné avec MLflow**, exposé via un endpoint fonctionnel, avec **surveillance du drift** en production. 2. **Aucun GPU dédié** : l'infra tourne on-premise sur une VM à 4 CPU / 8 Gio RAM (ou - `Standard_B2s`/`B2ms` côté Azure, 2 vCPU max) — Azure Machine Learning est de toute façon + `Standard_B2s`/`B2ms` côté Azure, 2 vCPU max) ; Azure Machine Learning est de toute façon bloqué par la politique Azure du projet. 3. **Délai serré** : le jalon J3 arrive à échéance le lendemain de la décision, J4 concentre déjà 26 issues sur 4 jours. Un modèle long à mettre en œuvre retarde la chaîne complète (service de @@ -32,7 +32,7 @@ déjà dérivées. | Régresseurs exogènes | Oui, mais doivent être connus dans le futur au moment de la prédiction | Oui, via lags/moyennes glissantes sur le passé | Oui, natif | Difficile en multivarié | Aucun support | Contexte de prompt seulement, non appris | | Coût de calcul (VM sans GPU) | Faible | Faible | Élevé (deep learning) | Faible | Faible | Élevé à prohibitif | | Versionnable MLflow | Oui, nativement | Oui, nativement | Pas de support direct | Oui, générique | Pas de support direct | Rien à versionner (pas un modèle entraîné) | -| Granularité | Un modèle par site (ou par site × métrique) | Un seul modèle global sur tous les sites | Un par site | Un par site | Un par site | — | +| Granularité | Un modèle par site (ou par site × métrique) | Un seul modèle global sur tous les sites | Un par site | Un par site | Un par site | - | | Effort avant l'échéance | Faible | Moyen (feature engineering) | Élevé | Moyen à élevé | Faible en soi | Élevé, ou factice | ## Décision @@ -40,7 +40,7 @@ déjà dérivées. **LightGBM, un seul modèle global** couvrant tous les sites, plutôt qu'un modèle par site (Prophet) ou par famille de site. Cible : `consumption_kwh`, avec `period_minutes` comme feature d'entrée plutôt que comme étape d'agrégation post-prédiction. Suivi et versioning via **MLflow** -(tracking + registre de modèles), sur le magasin local par défaut dans un premier temps — +(tracking + registre de modèles), sur le magasin local par défaut dans un premier temps ; l'hébergement sur l'infra k3s reste une question ouverte, non bloquante pour démarrer. Raisons retenues, au-delà du tableau ci-dessus : @@ -53,7 +53,7 @@ Raisons retenues, au-delà du tableau ci-dessus : `humidity_percent` et `solar_irradiance_wm2` sont des mesures passées, pas des prévisions, et aucune source de prévision météo n'existe dans le projet. LightGBM s'en sort avec des features de lag/moyenne glissante calculées sur l'historique déjà présent dans `reading`, cf. - `ml/enervision_ml/features.py` — un choix qui vaut aussi bien à l'entraînement qu'au futur + `ml/enervision_ml/features.py`, un choix qui vaut aussi bien à l'entraînement qu'au futur scoring. - **Apprentissage direct sur `consumption_kwh`** avec `period_minutes` en feature, sans étape d'agrégation intermédiaire que la sortie continue de Prophet aurait demandée. @@ -92,9 +92,9 @@ ValentinDeFaria), actée en réunion d'équipe du 2026-09-17 et validée par l'e - **SARIMA** : ne gère pas nativement plusieurs régresseurs exogènes ; réglage (p,d,q,P,D,Q) plus long que le délai disponible. - **NeuralProphet** : fait tout ce que fait Prophet et apprend en plus des motifs autorégressifs, - mais coûte plus cher en calcul (pas de GPU disponible) et n'a pas d'outil MLflow direct — piste + mais coûte plus cher en calcul (pas de GPU disponible) et n'a pas d'outil MLflow direct : piste d'évolution possible, non engageante à ce stade. -- **Holt-Winters** : écarté d'entrée, pas seulement différé — aucun support de régresseurs +- **Holt-Winters** : écarté d'entrée, pas seulement différé : aucun support de régresseurs exogènes, alors que la météo et l'irradiance sont nécessaires ici. - **CatBoost** : même famille que LightGBM, gère nativement les colonnes catégorielles (comme `site_type`) sans encodage manuel. Non rejeté, différé : candidat à comparer si LightGBM diff --git a/docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md b/docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md index 6b32e89..cf6ee61 100644 --- a/docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md +++ b/docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md @@ -2,6 +2,8 @@ - Statut : accepté - Date : 2026-09-21 +- Complété par : [ADR 0017](0017-environnement-dev-a-la-demande.md), troisième environnement `dev` +- Note du 24/09 : l'approbation annoncée avant la production n'a jamais été activée. L'environnement GitHub `prod` n'accepte que `main`, sans relecteur requis. ## Contexte diff --git a/docs/adr/0011-enervision-procedure-deploiement.md b/docs/adr/0011-enervision-procedure-deploiement.md index fb74241..b18bbc1 100644 --- a/docs/adr/0011-enervision-procedure-deploiement.md +++ b/docs/adr/0011-enervision-procedure-deploiement.md @@ -1,7 +1,7 @@ # EnerVision · procédure de déploiement (22/09/2026) Terraform provisionne la machine, GitHub Actions déploie (ADR 0010). Deux environnements Compose -sur la VM ENI `10.101.200.37` : `rec` sur la branche `dev`, `prod` sur `main` (ADR 0009). +sur la VM ENI `` : `rec` sur la branche `dev`, `prod` sur `main` (ADR 0009). | | recette | production | |---|---|---| @@ -13,7 +13,7 @@ sur la VM ENI `10.101.200.37` : `rec` sur la branche `dev`, `prod` sur `main` (A ## 0. Avant toute commande -1. **Clé SSH déposée** sur la VM : `ssh-copy-id -i ~/.ssh/id_ed25519.pub root@10.101.200.37`. +1. **Clé SSH déposée** sur la VM : `ssh-copy-id -i ~/.ssh/id_ed25519.pub root@`. Terraform ne gère **pas** l'authentification par mot de passe (elle finirait dans le state). 2. **L'utilisateur propriétaire existe déjà** sur la VM (ex. `enervision`) : il possède `/srv/enervision` et fait tourner le runner. Terraform échoue tôt s'il manque, il ne le crée pas. @@ -42,7 +42,7 @@ runner_version = "2.330.0" # épingler depuis github.com/actions/runner/rel runner_token = "..." # jeton d'1 h, à retirer du fichier après l'apply ``` -Défauts utiles : `ssh_host = "10.101.200.37"`, `ssh_user = "root"`, +Défauts utiles : `ssh_host = ""`, `ssh_user = "root"`, `ssh_private_key_path = "~/.ssh/id_ed25519"`, `racine = "/srv/enervision"`, `runner_labels = "eni-g3"` (ciblé par `deploy.yml`), `runner_dossier = "/opt/actions-runner"`. @@ -134,7 +134,7 @@ curl -k https://localhost/api/v1/health/ready # production, sur la VM Depuis un poste, ajouter à `/etc/hosts` : ``` -10.101.200.37 enervision.local rec.enervision.local + enervision.local rec.enervision.local ``` Les deux noms sont obligatoires : le cookie `__Secure-ev_refresh` est posé par hôte et non par diff --git a/docs/adr/0012-enervision-deploiement-rec-prod-vm-eni.md b/docs/adr/0012-enervision-deploiement-rec-prod-vm-eni.md index 8d01d39..a1d7569 100644 --- a/docs/adr/0012-enervision-deploiement-rec-prod-vm-eni.md +++ b/docs/adr/0012-enervision-deploiement-rec-prod-vm-eni.md @@ -1,7 +1,7 @@ # EnerVision · Recette et production sur la VM ENI, aujourd'hui État au lundi 21 septembre 2026, 15h. Cible : deux environnements qui tournent sur la VM -`eadl-2025-nantes-g3` (`10.101.200.37`) avant vendredi 25/09 9h, déployés automatiquement depuis +`eadl-2025-nantes-g3` (``) avant vendredi 25/09 9h, déployés automatiquement depuis GitHub. Ce document donne la solution retenue, ce qu'elle change dans le dépôt, et le déroulé de l'après-midi avec qui fait quoi. @@ -37,7 +37,7 @@ prod à chaque connexion sur la recette. - **Un projet Compose isole tout.** Volumes, réseau, noms de conteneurs sont préfixés par le nom du projet. Casser la recette ne touche pas la prod, ce qui est la raison d'être d'une recette. - **Le runner sur la VM est la seule façon d'atteindre une IP privée d'école depuis GitHub.** Les - runners hébergés par GitHub ne voient pas `10.101.200.37`. Le runner se connecte en sortie + runners hébergés par GitHub ne voient pas ``. Le runner se connecte en sortie vers GitHub, aucun port entrant n'est nécessaire. C'était le choix 16 du dossier EC01 : il redevient tenu. - **La promotion existe déjà dans la stratégie de branches** : `dev` puis `main` par PR. Le @@ -71,9 +71,9 @@ Ce qui ne change pas : `docker-compose.yml`, la configuration Nginx, `infra/terr | # | Qui | Quoi | Durée | |---|---|---|---| | 1 | **ineszang** (seule admin du dépôt) | Environnement `prod` : branche autorisée `main`, un relecteur requis. Environnement `rec` : branche `dev`. Settings > Actions : « Require approval for all outside collaborators ». Générer le jeton d'enregistrement du runner (Settings > Actions > Runners > New self-hosted runner, Linux x64) et le transmettre à Johan | 10 min | -| 2 | **Johan** | Déposer sa clé sur la VM : `ssh-copy-id -i ~/.ssh/id_ed25519.pub root@10.101.200.37`, mot de passe du compte administrateur local des postes de l'école | 2 min | +| 2 | **Johan** | Déposer sa clé sur la VM : `ssh-copy-id -i ~/.ssh/id_ed25519.pub root@`, mot de passe du compte administrateur local des postes de l'école | 2 min | | 3 | Johan + Claude | **Fait à 15h** : branche locale `feat/deploy-rec-prod` avec tous les changements du §3, image frontend reconstruite avec succès, fusion Compose vérifiée pour les deux environnements. Reste : commit, push, PR vers `dev` | fait | -| 4 | Claude, par SSH | `scripts/provision-host.sh` sur la VM. Écrire les deux `.env` (secrets générés sur la VM, jamais dans git). Certificats : `PUBLIC_HOST=rec.enervision.local PUBLIC_IP=10.101.200.37 make tls-selfsigned` dans `rec`, idem avec `enervision.local` dans `prod`. Puis `make stack-up` dans chaque dossier | 20 min plus la construction des images | +| 4 | Claude, par SSH | `scripts/provision-host.sh` sur la VM. Écrire les deux `.env` (secrets générés sur la VM, jamais dans git). Certificats : `PUBLIC_HOST=rec.enervision.local PUBLIC_IP= make tls-selfsigned` dans `rec`, idem avec `enervision.local` dans `prod`. Puis `make stack-up` dans chaque dossier | 20 min plus la construction des images | | 5 | Johan, sur la VM | Installer le runner sous un utilisateur non-root membre du groupe `docker`, label `eni-g3`, en service systemd (`./config.sh --unattended --labels eni-g3`, `sudo ./svc.sh install && sudo ./svc.sh start`) | 10 min | | 6 | Équipe | Merger la PR dans `dev` : la recette se redéploie seule. Ouvrir la PR `dev` vers `main` : la prod se déploie après approbation dans l'onglet Environments | 15 min | | 7 | Tous | Vérifier depuis un poste de l'équipe, `/etc/hosts` renseigné : connexion, tableau de bord, Airflow par tunnel SSH | 15 min | diff --git a/docs/adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md b/docs/adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md index 97681cc..e918566 100644 --- a/docs/adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md +++ b/docs/adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md @@ -2,6 +2,7 @@ - Statut : accepté - Date : 2026-09-23 +- Note du 24/09 : au gel, les règles de branche ne sont pas posées : `prod` n'accepte que `main` mais sans relecteur, `rec` et `dev` n'ont aucune règle, aucune branche n'est protégée. L'approbation des workflows externes n'est pas lisible avec les droits d'un membre. - Complète : [0009](0009-deux-environnements-compose-sur-la-vm-eni.md), qui reste en vigueur ## Contexte diff --git a/docs/adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md b/docs/adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md index 27b6928..2c01053 100644 --- a/docs/adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md +++ b/docs/adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md @@ -12,7 +12,7 @@ Chaque poste devait éditer son `/etc/hosts` et accepter trois avertissements du rien de présentable à un jury, et rien d'utilisable par quelqu'un qui n'a pas la main sur son poste. -Contraintes : la VM n'a qu'une IP privée, `10.101.200.37`, que ni Internet ni Let's Encrypt ne +Contraintes : la VM n'a qu'une IP privée, ``, que ni Internet ni Let's Encrypt ne joignent, et le réseau de l'école ne doit pas être touché. Vérifications faites le 23/09 : les résolveurs de l'école rendent bien une adresse privée pour un nom public, la VM sort en HTTPS vers Let's Encrypt et vers l'API de dynv6, mais le filtrage de l'école bloque duckdns.org, site diff --git a/docs/architecture/00-vue-ensemble.md b/docs/architecture/00-vue-ensemble.md index 056fd9a..1687113 100644 --- a/docs/architecture/00-vue-ensemble.md +++ b/docs/architecture/00-vue-ensemble.md @@ -19,22 +19,22 @@ parce qu'ils disent ce que le projet doit prouver, et donc à quoi sert chaque d ## Contexte -Statut : `Cible`. Les acteurs et les sources de mesures ne sont pas arrêtés, c'est l'objet du -jalon J2. +Statut : `Fait`. Les sources de mesures ont été arrêtées au jalon J2 : le dataset historique +fourni par l'école (CSV et métadonnées JSON, 2023 et 2024) et l'API Mock, interrogée chaque heure. ```mermaid flowchart LR exploitant["Exploitant
consulte les courbes"] admin["Administrateur
exploite la plateforme"] - sources["Sources de mesures
à définir en J2"] + sources["Sources de mesures
dataset CSV et API Mock"] subgraph systeme["EnerVision"] plateforme["Collecte, stockage,
analyse et restitution
de séries temporelles"] end - sources -.-> plateforme - exploitant -.-> plateforme - admin -.-> plateforme + sources --> plateforme + exploitant --> plateforme + admin --> plateforme ``` ## Conteneurs @@ -46,7 +46,8 @@ flowchart TB navigateur["Navigateur"] subgraph machine["Machine on-premise"] - proxy["Reverse proxy Nginx
:80 et :443"] + frontal["Frontal SNI
:80 et :443"] + proxy["Reverse proxy Nginx
un par environnement"] front["Frontend Angular 22
apps/frontend"] api["API FastAPI
apps/backend"] db[("PostgreSQL 17
TimescaleDB")] @@ -55,10 +56,11 @@ flowchart TB grafana["Grafana
profil monitoring"] end - navigateur --> proxy + navigateur --> frontal + frontal --> proxy proxy --> front proxy --> api - front -.-> api + front --> api api --> db airflow --> db prom --> api @@ -66,15 +68,18 @@ flowchart TB grafana --> prom ``` -Le lien `front -.-> api` reste en pointillé : le frontend appelle bien une API, mais un -intercepteur répond à sa place tant que les endpoints n'existent pas. Voir -[30-frontend.md](30-frontend.md). +Le lien `front --> api` est en trait plein : les fixtures sont coupées (`useMockFixtures: false` +dans les deux fichiers d'environnement Angular), et chaque service HTTP du frontend interroge +l'API réelle. Voir [30-frontend.md](30-frontend.md). Sur la machine, un frontal SNI reçoit les +ports 80 et 443 et aiguille chaque nom vers le proxy de son environnement +([ADR 0018](../adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md)). -Le lien `airflow --> db` est maintenant en trait plein : six DAGs tournent, deux pour +Le lien `airflow --> db` est maintenant en trait plein : sept DAGs tournent, deux pour l'entraînement et le scoring du modèle ML (issue #115), un pour la détection d'alertes et la génération des recommandations (issue #116), un pour la surveillance de dérive (issue #45), -`historical_import` pour le dataset historique (issue #119) et `mock_api_import` pour l'ingestion -horaire de l'API Mock (issue #15). La réconciliation entre les deux sources de lectures (issue +`historical_import` pour le dataset historique (issue #119), `mock_api_import` pour l'ingestion +horaire de l'API Mock (issue #15) et `retention`, qui exporte les chunks anciens de `reading` +vers Garage avant de les supprimer (issue #36). La réconciliation entre les deux sources de lectures (issue #15) est tranchée : le trou entre la fin de l'historique (31/12/2024) et le début de l'ingestion API Mock est accepté comme définitivement perdu, aucune mesure réelle n'existant pour cette période. `mock_api_import` refuse toute fenêtre qui recouvrirait des lectures déjà importées du @@ -93,14 +98,14 @@ ailleurs ([ADR 0016](../adr/0016-supervision-en-profil-compose.md), | Domaine | Technologie | Emplacement | Statut | Ce qui existe réellement | |---|---|---|---|---| | Backend | FastAPI, Python 3.14 | `apps/backend` | `En cours` | Factory, configuration, journalisation, 2 sondes de santé, `/metrics`, contrat OpenAPI versionné, routes `sites`, `alerts`, `recommendations`, `stats/summary`, `readings`, `sensors/status` et `predictions` en lecture (endpoints → services → repositories → models) | -| Frontend | Angular 22, Node 24 | `apps/frontend` | `En cours` | Tableau de bord sur route `/dashboard`, authentification complète (garde de route, intercepteur de jeton), cinq services HTTP, graphiques Chart.js. `stats`/`alerts` sur fixtures, `predictions` branché sur l'API réelle | +| Frontend | Angular 22, Node 26 | `apps/frontend` | `En cours` | Tableau de bord sur route `/dashboard`, authentification complète (garde de route, intercepteur de jeton), huit services HTTP, graphiques Chart.js, vues sites, recommandations et supervision des capteurs. Tous branchés sur l'API réelle (`useMockFixtures: false`) ; l'intercepteur de fixtures ne sert plus qu'au développement hors ligne | | Base | PostgreSQL 17 + TimescaleDB | `db` | `Fait` | Bootstrap de l'extension, base de test, chaîne Alembic. Schéma applicatif créé (`site`, `dataset`, `reading` en hypertable, `prediction`, `alert`, `recommendation`) | | ML | LightGBM, MLflow | `ml` | `En cours` | Pipeline d'entraînement et de scoring (`enervision_ml.train`/`.score`, features par lags/moyennes glissantes partagées entre les deux, baseline de persistance saisonnière, suivi MLflow local), exposé en lecture via `GET /predictions`, orchestré par Airflow (`ml_train`/`ml_score`). Voir [ADR 0005](../adr/0005-modele-prediction-lightgbm.md) et [ML-START.md](../ML-START.md). Surveillance de dérive livrée côté backend (`app.monitoring.drift`, table `drift_report`, `GET /monitoring/drift`, DAG `derive`), voir [ADR 0013](../adr/0013-surveillance-de-derive-dans-le-backend.md) | -| Infra | Docker Compose, Nginx, Terraform, k3s single-node | `infra`, `docker-compose.prod.yml` | `En cours` | Reverse proxy et overlay de déploiement écrits et validés, jamais lancés sur le serveur ([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md)). Provisionnement de la VM par Terraform, qui installe Docker, prépare les deux environnements et enregistre le runner, jamais appliqué ([ADR 0010](../adr/0010-terraform-provisionne-github-actions-deploie.md)). Module d'installation k3s jamais appliqué, aucune ressource Kubernetes déclarée | +| Infra | Docker Compose, Nginx, Terraform, k3s single-node | `infra`, `docker-compose.prod.yml` | `En cours` | Reverse proxy et overlay de déploiement en service sur la machine, un proxy par environnement derrière un frontal SNI ([ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md), [ADR 0018](../adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md)). Provisionnement de la VM par Terraform, appliqué : Docker installé, trois environnements préparés, runner enregistré ; le coffre LUKS n'est pas appliqué, la machine étant un conteneur LXC ([ADR 0010](../adr/0010-terraform-provisionne-github-actions-deploie.md), [ADR 0020](../adr/0020-chiffrement-au-repos-coffre-luks-et-sse-c.md)). Module d'installation k3s jamais appliqué, aucune ressource Kubernetes déclarée | | Stockage objet | Garage, S3 | `infra/garage`, `docker-compose.yml` | `Fait` | Un Garage par environnement, `--single-node --default-bucket`, secrets par l'environnement, ports sur `127.0.0.1`, fumée S3 et SSE-C en CI. Reçoit les archives CSV gzip du DAG `retention`, chiffrées SSE-C, avant `drop_chunks` ([ADR 0019](../adr/0019-stockage-objet-garage-et-cycle-de-vie-des-mesures.md), [ADR 0020](../adr/0020-chiffrement-au-repos-coffre-luks-et-sse-c.md)) | | Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Fait` | Profil Compose `monitoring`, actif en prod : Prometheus et trois exporteurs (PostgreSQL, hôte, conteneurs), neuf règles d'alerte testées par `promtool`, Alertmanager vers Mailpit, trois tableaux de bord Grafana provisionnés. Voir [60-observabilite.md](60-observabilite.md) | -| ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver et scheduler avec LocalExecutor via Docker Compose, sur une base PostgreSQL dédiée. Six DAGs en sous-processus `uv run` : `ml_train`, `ml_score`, `alertes`, `historical_import`, `mock_api_import` et `derive` (quotidien, surveillance de dérive). L'import historique reste manuel et l'import API Mock s'exécute chaque heure. Réconciliation entre les deux sources (issue #15) : trou temporel accepté, recouvrement refusé à l'ingestion et dédupliqué en défense côté ML, voir [40-data.md](40-data.md). | -| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | Un orchestrateur `ci.yml` qui n'appelle que les composants modifiés ([ADR 0014](../adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md)) : lint, typage, tests avec seuil de couverture bloquant, tests d'intégration sur TimescaleDB réel, audit de dépendances, SAST Bandit, quality gate SonarCloud, intégrité des DAGs Airflow, Terraform, Compose et supervision, parcours Playwright et tirs k6 contre la stack de prod ([ADR 0015](../adr/0015-tests-e2e-et-de-charge-contre-la-stack-compose.md)). Déploiement vers la VM ENI par `deploy.yml`, appelé une fois « CI ok » vert, `dev` en recette et `main` en production après approbation ([ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)), mais jamais exécuté : le runner n'est pas enregistré sur la machine. Détail dans [50-cicd.md](50-cicd.md) | +| ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver et scheduler avec LocalExecutor via Docker Compose, sur une base PostgreSQL dédiée. Sept DAGs en sous-processus `uv run` : `ml_train`, `ml_score`, `alertes`, `historical_import`, `mock_api_import`, `derive` (quotidien, surveillance de dérive) et `retention` (quotidien, export vers Garage puis suppression). L'import historique reste manuel et l'import API Mock s'exécute chaque heure. Réconciliation entre les deux sources (issue #15) : trou temporel accepté, recouvrement refusé à l'ingestion et dédupliqué en défense côté ML, voir [40-data.md](40-data.md). | +| CI/CD | GitHub Actions | `.github/workflows` | `Fait` | Un orchestrateur `ci.yml` qui n'appelle que les composants modifiés ([ADR 0014](../adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md)) : lint, typage, tests avec seuil de couverture bloquant, tests d'intégration sur TimescaleDB réel, audit de dépendances, SAST Bandit, quality gate SonarCloud, intégrité des DAGs Airflow, Terraform, Compose et supervision, parcours Playwright et tirs k6 contre la stack de prod ([ADR 0015](../adr/0015-tests-e2e-et-de-charge-contre-la-stack-compose.md)). Déploiement vers la VM ENI par `deploy.yml`, appelé une fois « CI ok » vert, `dev` en recette et `main` en production ([ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)), et toute autre branche à la demande dans `dev` ([ADR 0017](../adr/0017-environnement-dev-a-la-demande.md)). Sept déploiements de production réussis, le dernier sur le commit gelé ; l'environnement `prod` n'exige aucun relecteur (relevé du 24/09). Détail dans [50-cicd.md](50-cicd.md) | ## Flux bout en bout @@ -122,7 +127,6 @@ sequenceDiagram S->>A: mesures horodatées A->>T: insertion dans l'hypertable - T->>T: rafraîchissement de l'agrégat continu U->>API: GET /api/v1/... API->>T: agrégation sur la fenêtre demandée T-->>API: lignes @@ -146,8 +150,9 @@ consolidée. vraie VM, mais la machine ENI est un conteneur LXC sans device-mapper : le chiffrement de son disque relève de l'hôte Proxmox, demandé à l'école. Ce qui est couvert et ce qui ne l'est pas : [ADR 0020](../adr/0020-chiffrement-au-repos-coffre-luks-et-sse-c.md). -- **Interdire par défaut.** Toute route exige un jeton, sauf quatre exceptions listées dans un - fichier de test qui interroge réellement chaque route sans identifiant. +- **Interdire par défaut.** Toute route exige un jeton, sauf huit routes publiques listées + nommément dans `tests/api/acces.py`, et un test interroge réellement chaque route sans + identifiant. - **Révocation immédiate.** Le compte est relu en base à chaque requête : une désactivation ou un changement de rôle prend effet à la requête suivante, pas au bout de 15 minutes. - **Limitation de débit à fenêtre glissante** sur trois clés, évaluée avant le hachage. Pas de @@ -167,7 +172,9 @@ consolidée. jeton, exigé dès que la supervision tourne, sonde de disponibilité qui ne publie plus la version de TimescaleDB. - **CI backend bloquante** : format, lint, typage strict et tests avec seuil de couverture. - **Conteneur backend non-root**, déclaré dans `apps/backend/Dockerfile`. -- **Terminaison TLS au frontal** : un reverse proxy Nginx est le seul service publié, il redirige +- **Terminaison TLS au proxy de chaque environnement**, derrière un frontal SNI qui est le seul + composant publié sur la machine et aiguille sans déchiffrer. Certificats Let's Encrypt par + DNS-01 ([ADR 0018](../adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md)). Le proxy redirige 80 vers 443, sert le SPA et l'API sous la même origine, pose **HSTS** et **CSP** que l'application refuse délibérément de poser, et ajoute une **limitation de débit au frontal** distincte de celle de l'application. Voir @@ -185,9 +192,9 @@ consolidée. arrêteraient une application compromise. Même raison de report. - **Portée par site** dans l'autorisation : les rôles sont globaux, un opérateur du site A peut agir sur le site B. C'est la limite connue du modèle. -- **Certificat reconnu** : aucun nom de domaine public ne résout vers la machine, donc le défi - HTTP-01 de Let's Encrypt ne peut pas aboutir. Le certificat servi est auto-signé, le chemin ACME - est livré et documenté mais pas exercé. +- **Approbation humaine avant la production** : l'environnement GitHub `prod` n'accepte que + `main` mais n'exige aucun relecteur, et aucune branche n'est protégée. Réglage réservé à + l'administratrice du dépôt. - **Analyse des images de conteneur** dans la CI. Celle des dépendances, elle, est en place (`pip-audit`, `npm audit`, Dependabot sur 5 écosystèmes), de même que le SAST Bandit. Voir [50-cicd.md](50-cicd.md). @@ -206,7 +213,15 @@ Elles vivent dans `../adr/`, pas ici. | [0006](../adr/0006-moteur-de-regles-dans-le-backend.md) | Le moteur de règles de recommandation vit dans le backend, pas dans `ml/` | | [0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md) | Terminaison TLS par un reverse proxy Nginx, en Docker Compose | | [0008](../adr/0008-airflow-execute-le-code-du-backend.md) | Airflow exécute le code du backend en sous-processus, dans son propre environnement | -| [0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md) | Deux environnements sur la VM ENI, un projet Compose chacun, déployés par un runner auto-hébergé | +| [0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md) | Un projet Compose par environnement sur la VM ENI, déployé par un runner auto-hébergé (deux environnements à l'origine, trois depuis l'ADR 0017) | | [0010](../adr/0010-terraform-provisionne-github-actions-deploie.md) | Terraform provisionne la machine, GitHub Actions déploie l'application | +| [0011](../adr/0011-enervision-procedure-deploiement.md) | Procédure de déploiement, telle qu'exécutée le 22/09/2026 | +| [0012](../adr/0012-enervision-deploiement-rec-prod-vm-eni.md) | État de la recette et de la production sur la VM ENI | +| [0013](../adr/0013-surveillance-de-derive-dans-le-backend.md) | La surveillance de dérive vit dans le backend et écrit sa propre table | +| [0014](../adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md) | Un pipeline CI unique appelle les workflows de composant et conditionne le déploiement | +| [0015](../adr/0015-tests-e2e-et-de-charge-contre-la-stack-compose.md) | Les tests de bout en bout et de charge visent la stack Compose déployée | +| [0016](../adr/0016-supervision-en-profil-compose.md) | La supervision vit dans un profil Compose, active en prod | +| [0017](../adr/0017-environnement-dev-a-la-demande.md) | Un troisième environnement, `dev`, déployé à la demande depuis n'importe quelle branche | +| [0018](../adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md) | Noms publics, certificats Let's Encrypt par DNS-01 et frontal SNI sans port | | [0019](../adr/0019-stockage-objet-garage-et-cycle-de-vie-des-mesures.md) | Stockage objet Garage par environnement ; les chunks anciens de `reading` sont exportés en CSV gzip puis supprimés | | [0020](../adr/0020-chiffrement-au-repos-coffre-luks-et-sse-c.md) | Chiffrement au repos : coffre LUKS des volumes Docker de la VM, SSE-C des archives | diff --git a/docs/architecture/10-infra.md b/docs/architecture/10-infra.md index 2cd38f9..3b8d1c1 100644 --- a/docs/architecture/10-infra.md +++ b/docs/architecture/10-infra.md @@ -7,8 +7,8 @@ dans quel contexte, quelles décisions sont arrêtées, et ce qui manque encore |---|---|---| | Docker Compose | Développer et recetter sur le poste | `Fait` | | Docker Compose plus reverse proxy | Déployer sur la machine on-premise | `Fait` | -| Deux projets Compose sur la VM ENI, recette et production | Déploiement continu depuis GitHub | `En cours` | -| Provisionnement Terraform de la VM | Préparer la machine et enregistrer le runner | `En cours` | +| Trois projets Compose sur la VM ENI, dev, recette et production | Déploiement continu depuis GitHub | `Fait` | +| Provisionnement Terraform de la VM | Préparer la machine et enregistrer le runner | `Fait` | | k3s single-node | Cible à terme | `En cours` | | MLflow (`ml/`) | Tracker les expériences et le registre de modèles en local | `Fait`, non relié aux autres topologies | @@ -179,7 +179,7 @@ du `docker-compose.yml` principal (réseau, volumes et démarrage séparés). | `mlflow` | Construite depuis `ml/` | Expose l'UI et l'API MLflow sur `127.0.0.1:5000`. Artefacts sur volume `mlflow-artifacts`, tracking store sur `mlflow-db` | Portée actuelle : environnement de tracking et de registre de modèles pour le développement -local uniquement. Ce compose n'est relié ni à `docker-compose.prod.yml`, ni aux deux +local uniquement. Ce compose n'est relié ni à `docker-compose.prod.yml`, ni aux trois environnements Compose de la VM ENI, ni à la cible k3s. Le magasin utilisé par Airflow pour `ml_train`/`ml_score` (SQLite, volume `airflow_ml_state`) en est distinct : les deux MLflow ne se voient pas tant que `MLFLOW_TRACKING_URI` n'est pas posé côté Airflow. @@ -199,8 +199,8 @@ d'entrainement Airflow et locaux n'a encore ete identifie. ## Machine cible, exécution Docker Statut : `Fait`. Défini par l'overlay `docker-compose.prod.yml`, appliqué par-dessus le -`docker-compose.yml`. Écrit et validé sur le poste, **jamais encore lancé sur le serveur de -l'école**. Décision et motifs dans l'[ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md). +`docker-compose.yml`. En service sur la machine du groupe, une stack par environnement (sept +déploiements de production entre le 23/09 et le 24/09). Décision et motifs dans l'[ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md). ```mermaid flowchart LR @@ -225,8 +225,9 @@ flowchart LR sup -->|"alertes par courriel"| mail ``` -Le proxy est **le seul service à publier des ports** sur le réseau. Backend et frontend ne sont -plus publiés du tout, la base et l'interface Mailpit sont ramenées sur `127.0.0.1`, donc joignables +Sur le poste, le proxy est **le seul service à publier des ports** sur le réseau ; sur la +machine, même lui n'écoute que sur `127.0.0.1`, derrière le frontal SNI (section suivante). +Backend et frontend ne sont plus publiés du tout, la base et l'interface Mailpit sont ramenées sur `127.0.0.1`, donc joignables par tunnel SSH et pas autrement. Le détail du routage, les deux modes d'obtention du certificat et la commande de validation hors exécution sont dans [`infra/proxy/README.md`](../../infra/proxy/README.md). @@ -239,7 +240,7 @@ Deux conséquences se propagent jusqu'à l'application, et elles ne se devinent ### Trois environnements sur la même machine -Statut : `En cours`. Décision et motifs dans +Statut : `Fait`. Décision et motifs dans l'[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md), étendue à un troisième environnement par l'[ADR 0017](../adr/0017-environnement-dev-a-la-demande.md) ; noms, certificats et frontal sans port dans l'[ADR 0018](../adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md). @@ -274,7 +275,9 @@ la VM aligne le dossier sur la branche poussée et lance `make stack-up`. ### Provisionnement de la machine -Statut : `En cours`. Décision et frontière dans +Statut : `Fait`. Appliqué : le state local porte Docker, les trois environnements et le runner ; +le coffre LUKS n'est pas appliqué, la machine étant un conteneur LXC +([ADR 0020](../adr/0020-chiffrement-au-repos-coffre-luks-et-sse-c.md)). Décision et frontière dans l'[ADR 0010](../adr/0010-terraform-provisionne-github-actions-deploie.md) : **Terraform provisionne la machine, GitHub Actions déploie l'application**. La racine `infra/terraform/environments/vm-eni/` fait trois choses, et rien d'autre. @@ -317,10 +320,10 @@ la migration. Runbook, joué en root sur la VM, coupure des trois environnements minutes : ```bash -scp scripts/coffre-luks.sh root@10.101.200.37:/tmp/ -ssh root@10.101.200.37 'COFFRE_TAILLE=30G COFFRE_MIGRER=1 bash /tmp/coffre-luks.sh' -ssh root@10.101.200.37 'findmnt /var/lib/docker/volumes && lsblk /dev/mapper/enervision-coffre && docker ps' -ssh root@10.101.200.37 'curl -k https://localhost:10443/api/v1/health/ready' +scp scripts/coffre-luks.sh root@:/tmp/ +ssh root@ 'COFFRE_TAILLE=30G COFFRE_MIGRER=1 bash /tmp/coffre-luks.sh' +ssh root@ 'findmnt /var/lib/docker/volumes && lsblk /dev/mapper/enervision-coffre && docker ps' +ssh root@ 'curl -k https://localhost:10443/api/v1/health/ready' ``` Ensuite, dans cet ordre : sauvegarder `/root/enervision-coffre.key` hors de la VM (sans elle, les @@ -388,7 +391,7 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de | Terraform provisionne, GitHub Actions déploie | Deux chemins pour le même acte de livraison, c'est ce que la revue de #141 relève sur la VM | [ADR 0010](../adr/0010-terraform-provisionne-github-actions-deploie.md) | | Connexion SSH par clé, jamais par mot de passe | Une variable de mot de passe finit en clair dans le state, ou dans les `triggers` qui y sont persistés | `environments/vm-eni/variables.tf`, `modules/k3s/main.tf` | | Terminaison TLS par un reverse proxy Nginx en Compose | L'ingress k3s supposait un registre et des manifestes qui n'existent pas, à quatre jours du rendu | `docker-compose.prod.yml`, [ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md) | -| Certificat auto-signé par défaut, chemin ACME câblé | Aucun domaine public ne résout vers la machine : le défi HTTP-01 ne peut pas aboutir | `scripts/tls-selfsigned.sh`, `infra/proxy/acme-deploy-hook.sh` | +| Certificats Let's Encrypt par défi DNS-01 sur la machine, auto-signé sur le poste | La machine n'a qu'une adresse privée : le défi HTTP-01 ne peut pas aboutir, le défi DNS-01 ne demande qu'un enregistrement TXT dans la zone publique | `make tls-dns01`, `scripts/provision-host.sh`, `scripts/tls-selfsigned.sh`, [ADR 0018](../adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md) | | Un projet Compose par environnement, sur la même machine | Une seule VM, et l'isolation par nom de projet ne demande ni cluster ni registre | `.env` de chaque dossier, [ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md) | | Runner GitHub Actions auto-hébergé sur la VM | Les runners hébergés par GitHub ne joignent pas une adresse privée d'école | `.github/workflows/deploy.yml` | | Secrets dans le `.env` de chaque environnement, sur la machine | Ni dans git, ni dans GitHub : le runner n'a rien à recevoir | `scripts/provision-host.sh` | @@ -422,8 +425,6 @@ question à trancher, avant toute ressource Kubernetes. - **Quel ingress** remplace Traefik le jour de la bascule k3s. Qui termine le TLS est tranché par l'[ADR 0007](../adr/0007-terminaison-tls-et-reverse-proxy-nginx.md), mais la réponse vaut pour la topologie Compose, pas pour Kubernetes. -- **Quel nom de domaine public**, sans lequel Let's Encrypt reste hors d'atteinte et le certificat - reste auto-signé. - **Quel registre d'images**, et comment il est alimenté sans CI. - **Quel stockage persistant** côté Kubernetes pour PostgreSQL, et si la base tourne dans le cluster ou à côté. diff --git a/docs/architecture/20-backend.md b/docs/architecture/20-backend.md index fa0dd4d..1d15dff 100644 --- a/docs/architecture/20-backend.md +++ b/docs/architecture/20-backend.md @@ -488,7 +488,7 @@ Quatre fichiers méritent d'être connus avant de toucher à l'authentification - **Rôles PostgreSQL cantonnés** pour l'ETL et le travail d'apprentissage, plus le `REVOKE` sur `audit_log`. Dette assumée, décrite dans les ADR 0003 et 0004. - **Pagination et fenêtrage** : posés sur `GET /readings` (fenêtre plafonnée à 90 jours, - `limit`/`offset` plafonné à 2000), mais toujours en `limit`/`offset` simple — pas de curseur ni + `limit`/`offset` plafonné à 2000), mais toujours en `limit`/`offset` simple : pas de curseur ni de plan de secours si un `offset` élevé sur une fenêtre dense devient lent en pratique. `statement_timeout` reste absent au niveau de la connexion, donc rien n'empêche une requête individuelle de tourner longtemps si les plafonds au-dessus d'elle s'avéraient insuffisants. diff --git a/docs/architecture/30-frontend.md b/docs/architecture/30-frontend.md index 4969b0b..5705c8c 100644 --- a/docs/architecture/30-frontend.md +++ b/docs/architecture/30-frontend.md @@ -4,8 +4,9 @@ Application Angular 22, 100 % standalone, testée avec Vitest. Source dans `apps ## État actuel -Statut : `En cours`. L'application sert le tableau de bord, la liste et le détail des sites, la -supervision des capteurs (admin) et le flux des alertes actives, tous branchés sur l'API réelle. +Statut : `En cours`. L'application sert le tableau de bord, la liste et le détail des sites, les +recommandations, la supervision des capteurs (admin) et le flux des alertes actives, tous +branchés sur l'API réelle. Ce qui est en place : @@ -13,10 +14,11 @@ Ce qui est en place : - `app.config.ts` fournit `provideBrowserGlobalErrorListeners()`, `provideRouter(routes)` et `provideHttpClient(withInterceptors([authInterceptor, mockApiInterceptor]))`. - Des routes en composants différés (`/dashboard`, `/sites`, `/sites/:siteId`, - `/monitoring/sensors` réservée au rôle `admin`) et une redirection depuis la racine. + `/recommendations`, `/monitoring/sensors` réservée au rôle `admin`, et les pages + d'authentification) et une redirection depuis la racine. - `core/services` porte un service HTTP par domaine (`StatsService`, `AlertsService` avec ses filtres `site_id` et `severity`, `PredictionsService`, `SitesService`, `ReadingsService`, - `SensorsService`, `AuthService`), `core/interceptors` l'intercepteur de fixtures et l'intercepteur + `RecommendationsService`, `SensorsService`, `AuthService`), `core/interceptors` l'intercepteur de fixtures et l'intercepteur d'authentification (jeton porteur, rafraîchissement sur 401), `core/guards` la garde `authGuard`. - `features/` porte une page par domaine. `shared/components` porte la jauge de consommation et les graphiques Chart.js, le widget `app-alert-feed` (flux d'alertes filtrable par site et diff --git a/docs/architecture/31-contrat-authentification.md b/docs/architecture/31-contrat-authentification.md index 4ad3dcf..6e34807 100644 --- a/docs/architecture/31-contrat-authentification.md +++ b/docs/architecture/31-contrat-authentification.md @@ -136,8 +136,10 @@ origine, en HTTPS**. C'est cela, et rien d'autre, qui rend le cookie `__Secure-e utilisable : servi en HTTP simple ou depuis une autre origine, il n'est jamais posé et l'authentification ne survit pas à un rechargement de page. -Ce qui reste à surveiller : le certificat est auto-signé tant qu'aucun domaine public ne résout -vers la machine. Un navigateur qui refuse l'exception refusera aussi le cookie. +Sur la machine, le certificat vient de Let's Encrypt par DNS-01 +([ADR 0018](../adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md)) : le navigateur n'a +aucune exception à accepter. Sur le poste, il reste auto-signé, et un navigateur qui refuse +l'exception refusera aussi le cookie. Et au moins une fois avant la soutenance, lancer le front **sans le proxy**, en cross-origin réel : c'est le seul moyen d'exercer le préflight CORS et `SameSite`, que la même origine masque. diff --git a/docs/architecture/40-data.md b/docs/architecture/40-data.md index 81f5387..d20cdc8 100644 --- a/docs/architecture/40-data.md +++ b/docs/architecture/40-data.md @@ -74,10 +74,10 @@ Les mécanismes d'ingestion sont maintenant implémentés pour les deux sources - le dataset historique CSV/JSON avec `historical_import.py` ; - l'API Mock avec `mock_api_import.py`. -Les traitements sont actuellement exécutables directement depuis le backend. +Les traitements restent exécutables directement depuis le backend, et Airflow les orchestre : +`historical_import` se lance à la demande, `mock_api_import` chaque heure à la minute 45. -L'orchestration avec Apache Airflow reste une cible, tout comme les agrégats continus et la -compression. La rétention est faite : le DAG `retention` exporte chaque chunk de `reading` plus +Les agrégats continus et la compression restent des cibles. La rétention est faite : le DAG `retention` exporte chaque chunk de `reading` plus vieux que `READING_RETENTION_DAYS` vers Garage, puis le supprime. ```mermaid @@ -88,8 +88,8 @@ flowchart LR hist --> hy[("Hypertable reading")] api --> hy - airflow["Airflow"] -.-> hist - airflow -.-> api + airflow["Airflow"] --> hist + airflow --> api hy -.-> agg[("Agrégat continu")] hy -.-> comp["Compression"] @@ -542,7 +542,7 @@ Cinq garde-fous, tous dans `mock_api_import.py` : | Garde-fou | Mise en œuvre | |---|---| | Timeout | `APP_MOCK_API_TIMEOUT_SECONDS`, dix secondes par défaut | -| Taille de tableau plafonnée | `MAX_SITES` sites, et au plus `--limit` mesures par site | +| Taille de tableau plafonnée | `MAX_SITES` sites, et au plus `limit` mesures par site, dérivé de la fenêtre par `limit_for_window()` | | Bornes physiques | `PHYSICAL_BOUNDS`, une plage par grandeur | | Frontière d'anti-corruption | `build_site_row()` et `build_reading_row()`, qui ne recopient que les champs attendus | | Refus de recouvrir l'historique | `refuse_if_overlaps_historical_dataset()`, voir ci-dessous | @@ -622,9 +622,10 @@ imputed_values = NULL imputation_method = NULL ``` -### Validation de l'import API Mock +### Validation initiale de l'import API Mock (18/09) -Un scénario de validation a été exécuté pour les 7 sites sur la période : +Un premier scénario de validation a été exécuté le 18/09, avant la clôture de la réconciliation +(#15), pour les 7 sites sur la période : ```text 15/06/2024 12:00 UTC @@ -646,6 +647,10 @@ Résultat : 420 lectures récupérées ``` +Ce scénario ne se rejoue plus tel quel depuis le 23/09 : la fenêtre recouvre le dataset +historique, donc `refuse_if_overlaps_historical_dataset()` la refuse, et `limit` n'est plus +fourni par l'appelant (une lecture par heure, voir plus haut). + Les données ont été chargées dans PostgreSQL/TimescaleDB puis contrôlées directement en base. Les contrôles ont confirmé : @@ -673,9 +678,9 @@ Les tests automatisés couvrent également : - la conservation des données sources ; - l'idempotence en base. -## Évolution prévue +## Orchestration par Airflow -La prochaine étape consiste à orchestrer les deux mécanismes d'ingestion avec Apache Airflow. +Les deux mécanismes d'ingestion sont orchestrés par Apache Airflow (`etl/airflow/dags`) : ```text CSV / JSON ----------------+ @@ -696,18 +701,11 @@ historical_import.py mock_api_import.py PostgreSQL / TimescaleDB ``` -Airflow servira à : +`historical_import` n'a pas de planification (lancement à la demande) ; `mock_api_import` tourne +chaque heure à la minute 45. Airflow planifie, ordonne, suit l'état et remonte les erreurs ; il ne +remplace pas la logique ETL : les scripts Python restent responsables de l'extraction, de la +validation, de la transformation et du chargement, appelés tels quels par des `BashOperator`. -- planifier les traitements ; -- définir leur ordre d'exécution ; -- suivre leur état ; -- gérer et remonter les erreurs ; -- faciliter les exécutions récurrentes. - -Airflow ne remplacera pas la logique ETL déjà implémentée. - -Les scripts Python resteront responsables de l'extraction, de la validation, de la transformation -et du chargement des données. - -Le pipeline servira ensuite de base à la préparation des données nécessaires au modèle -de Machine Learning. +Le même Airflow porte la suite de la chaîne : entraînement et scoring du modèle (`ml_train`, +`ml_score`), alertes et recommandations (`alertes`), dérive (`derive`) et rétention +(`retention`), soit sept DAGs. diff --git a/docs/architecture/50-cicd.md b/docs/architecture/50-cicd.md index 3b692c9..81ba240 100644 --- a/docs/architecture/50-cicd.md +++ b/docs/architecture/50-cicd.md @@ -6,14 +6,15 @@ vérifié, ce qui bloque, et ce qui ne l'est pas. | Étage | Sert à | Statut | |---|---|---| | Intégration continue | Interdire le merge d'un code qui casse la qualité, les tests ou la sécurité | `Fait` | -| Livraison continue | Déployer chaque branche d'intégration sur son environnement de la VM ENI | `En cours` | +| Livraison continue | Déployer chaque branche d'intégration sur son environnement de la VM ENI | `Fait` | Le **D** de CI/CD est écrit depuis le 21/09 : `deploy.yml` déploie `dev` en recette et `main` en production sur la VM de l'école, par un runner auto-hébergé (issue #21, [ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)). Depuis le 23/09, il ne part plus qu'une fois la CI du commit verte ([ADR 0014](../adr/0014-pipeline-ci-unique-et-deploiement-conditionne.md)). -Il n'a encore rien déployé : le runner n'est pas enregistré sur la machine. Statut à -basculer sur `Fait` au premier déploiement vert. Sa limite, nommée ici plutôt que découverte en +Le runner est enregistré sur la machine (`null_resource.runner_github` dans le state +Terraform) et l'environnement `prod` compte sept déploiements entre le 23/09 11h37 et le 24/09 +11h12, le dernier sur le commit gelé. Sa limite, nommée ici plutôt que découverte en soutenance : les images sont construites sur la machine à chaque déploiement, aucun artefact n'est publié puis promu d'un environnement à l'autre. @@ -103,7 +104,7 @@ n'est ouvert. Il n'a pas de déclencheur propre en dehors de `workflow_dispatch` | Événement | Environnement GitHub | Dossier sur la VM | Garde | |---|---|---|---| | `push` sur `dev`, « CI ok » vert | `rec` | `/srv/enervision/rec` | aucune de plus : la recette suit `dev` | -| `push` sur `main`, « CI ok » vert | `prod` | `/srv/enervision/prod` | approbation d'un relecteur dans l'environnement `prod`, branche `main` seule autorisée | +| `push` sur `main`, « CI ok » vert | `prod` | `/srv/enervision/prod` | branche `main` seule autorisée ; **aucun relecteur requis** (relevé par l'API le 24/09), l'approbation prévue n'est pas activée | | `workflow_dispatch` sur toute autre branche | `dev` | `/srv/enervision/dev` | droit d'écriture sur le dépôt, seul à pouvoir lancer un workflow ([ADR 0017](../adr/0017-environnement-dev-a-la-demande.md)) | Le job aligne le clone sur **le commit testé** (`fetch`, `checkout`, `reset --hard $GITHUB_SHA`), @@ -134,20 +135,23 @@ de `pull_request` dans `deploy.yml` ne suffit donc pas. Ce qui protège vraiment - le dépôt exige l'approbation des workflows de tous les contributeurs externes (Settings, Actions, « Require approval for all external contributors ») ; -- les environnements `rec` et `prod` n'acceptent que leur branche (`dev`, `main` avec un - relecteur), ce qui bloque un job qui les déclare avant qu'il atteigne le runner ; +- l'environnement `prod` n'accepte que `main`, ce qui bloque un job qui le déclare depuis une + autre branche avant qu'il atteigne le runner. `rec` et `dev` n'ont, eux, aucune règle + (relevé par l'API le 24/09) ; - le runner tourne sous un utilisateur dédié membre du groupe `docker`, jamais root. -Les deux réglages de dépôt restent à activer par l'administratrice. Tous les autres workflows -restent sur `ubuntu-latest`. +Restent à activer par l'administratrice : l'approbation des workflows de contributeurs +externes (non vérifiable sans droit d'administration), un relecteur requis sur `prod`, et la +restriction de `rec` à `dev`. Tous les autres workflows restent sur `ubuntu-latest`. -Cet utilisateur dédié doit posséder `/srv/enervision` : sinon git refuse les deux clones pour +Cet utilisateur dédié doit posséder `/srv/enervision` : sinon git refuse les clones pour propriété douteuse et le `.env` en `600` lui échappe. `PROPRIETAIRE=` passé à `scripts/provision-host.sh` fixe ce propriétaire. La machine se prépare avec `scripts/provision-host.sh`, qui vérifie Docker et Compose 2.24.4 ou -plus, clone les deux branches, génère les secrets de chaque `.env` et les certificats -auto-signés, et ne démarre rien. Le détail des trois environnements, ports et noms d'hôte, est +plus, prépare les trois clones (`prod` sur `main`, `rec` et `dev` sur `dev`), génère les secrets +de chaque `.env`, obtient les certificats Let's Encrypt par DNS-01 (un auto-signé ne reste en +place qu'en cas d'échec) et planifie leur renouvellement, et ne démarre rien. Le détail des trois environnements, ports et noms d'hôte, est dans [10-infra.md](10-infra.md). ## Ce qui bloque un merge @@ -171,6 +175,8 @@ dans [10-infra.md](10-infra.md). | Formatage et validité Terraform | infra | `fmt -check -recursive`, puis `init` et `validate` par racine | Bloque | | Verrous uv à jour | backend, ml, airflow | `uv sync --locked` : un `uv.lock` qui ne suit plus `pyproject.toml` échoue | Bloque | | Fichiers Compose | infra | `docker compose config` sur la stack de dev et la stack déployée, tous profils | Bloque | +| Frontal SNI | infra | `docker compose config` et `nginx -t` de `infra/front` | Bloque | +| Stockage objet | infra | fumée S3 sur Garage démarré par Compose : aller-retour, suppression, lecture refusée sans clé SSE-C (`tests/garage`) | Bloque | | Supervision | infra | `promtool check config`, `promtool test rules` (un cas par alerte), `amtool check-config`, JSON des tableaux | Bloque | | Workflows | infra | `actionlint`, shellcheck compris sur les blocs `run:` | Bloque | | Parcours de bout en bout | e2e | 18 parcours Playwright contre la stack de prod (proxy TLS) | Bloque | @@ -187,8 +193,8 @@ Deux seuils portent une décision qu'il faut savoir défendre : sans bloquer. Sans cette seconde passe, un constat LOW disparaîtrait du journal sans trace. Le revers à connaître : cette seconde étape porte `continue-on-error`, donc le job reste **vert** même quand elle relève quelque chose ; un LOW ne se voit qu'en ouvrant le journal. Au - 21/09/2026, les deux modules sont à **zéro constat, tous niveaux confondus**, sur 5 904 lignes - analysées. + 24/09/2026, les deux modules sont à **zéro constat, tous niveaux confondus**, sur 6 858 lignes + analysées (6 136 pour le backend, 722 pour le ML). - **La version de Bandit est épinglée** (`uvx bandit==1.9.4`) dans les deux jobs. Sans épingle, une nouvelle version passerait la CI au rouge sans qu'une seule ligne du dépôt ait changé, et le rejeu à l'identique promis plus bas n'existerait pas. @@ -209,7 +215,7 @@ partie de la suite, et son taux n'aurait aucun sens face au seuil de 85 %. ### Pourquoi le job d'intégration ML installe aussi le backend -Le schéma de la base n'a qu'une source, les six révisions Alembic de `apps/backend/alembic` : le +Le schéma de la base n'a qu'une source, les sept révisions Alembic de `apps/backend/alembic` : le backend est propriétaire du schéma, `ml/` n'en est que consommateur. Reconstruire ce schéma à la main dans le job ML donnerait un job vert sur une base qui n'est pas la nôtre, exactement l'erreur qu'évite déjà le choix de l'image `timescaledb-ha` plutôt qu'un `postgres` nu. Le job installe @@ -281,7 +287,7 @@ les tests ne se merge pas. | Préfixes de branche | `feat/`, `fix/`, `chore/`, `docs/`, `test/` | | Messages de commit | Conventional Commits | | Branche d'intégration | `dev` ; `main` est la branche par défaut du dépôt public | -| Revue | Toute PR passe par une revue écrite avant merge | +| Revue | Relecture écrite par un autre membre avant merge : 55 PR de fonctionnalité sur 62 au gel (89 %). Convention d'équipe, non imposée par une protection de branche | | ADR | Toute décision structurante porte son ADR dans la même PR | | Vues d'architecture | Toute PR qui change un composant met à jour sa vue **dans la même PR** | @@ -354,7 +360,7 @@ du runner qui l'a écrit, sans remappage automatique. depuis le début. Corrigé par `sudo chown 1000:1000` du fichier avant de le passer à `644`. - Ce `chown` déplace la propriété du fichier hors de l'utilisateur du runner : un `chmod` qui suit sans `sudo` échoue alors (« Operation not permitted »), et le `-e` implicite des étapes - bash de GitHub Actions arrête toute l'étape avant même `docker run` — un scan « réussi » en une + bash de GitHub Actions arrête toute l'étape avant même `docker run` : un scan « réussi » en une fraction de seconde, sans le moindre journal ni rapport produit. Les deux commandes doivent passer par `sudo`. @@ -427,6 +433,9 @@ voir `tests/load/README.md`. | Tir de charge nominal automatisé | #47 | Seul le smoke tourne en CI ; la charge à 50 utilisateurs se lance à la main en recette (`make load-test`), rec et prod partageant la VM | | Cache de couches Docker en CI | aucune | Le job E2E reconstruit les images backend et frontend à chaque run, deux à quatre minutes de plus | | Scan d'image de conteneur | aucune | Les images sont construites par le job E2E, pas analysées | +| Scan de secrets et d'IaC en CI | aucune | gitleaks, Trivy et Checkov ne tournent qu'à la main, pour le rapport de sécurisation : un secret commité ne serait vu qu'au passage suivant | +| `pip-audit` sur les verrous ML et Airflow, `npm audit` sur `tests/e2e` | aucune | Seuls les verrous du backend et du frontend sont audités en CI : la CVE-2026-41016 d'`apache-airflow-providers-smtp`, relevée le 23/09, y reste invisible | +| Approbation humaine avant la production | aucune | L'environnement `prod` n'exige aucun relecteur : un push sur `main` au CI vert part en production sans autre garde | ## Reproduire la CI en local @@ -437,7 +446,7 @@ Les tests d'intégration demandent une base **migrée**, et `db/init` ne crée ` vide : ```bash -make db-up migrate-test # la base de test reçoit les six révisions Alembic +make db-up migrate-test # la base de test reçoit les sept révisions Alembic make test-integration # backend, marqueur `integration` make ml-test-integration # pipeline ML, marqueur `integration` make test-chaine # vrais binaires ML puis relecture par l'API, marqueur `chaine` diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 162c784..07c211f 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -17,13 +17,16 @@ contredisent, c'est l'ADR qui fait foi et la vue qui est en retard. | [40-data.md](40-data.md) | Frontières `db/` et `alembic/`, cycle de vie d'une mesure, modèle | | [50-cicd.md](50-cicd.md) | Orchestrateur `ci.yml`, gates bloquantes, e2e et charge, SonarCloud, Dependabot, ce qui manque | | [60-observabilite.md](60-observabilite.md) | Métriques, Prometheus, alertes, tableaux de bord Grafana, ce qui manque | +| [70-pilotage.md](70-pilotage.md) | Runbook de pilotage des traitements automatisés : entraîner, lire la dérive, rejouer un DAG, rétention | +| [owasp-traceabilite.md](owasp-traceabilite.md) | Traçabilité OWASP Top 10 et API Top 10 : couvert, partiel, ouvert | La CI/CD a son document : un orchestrateur et ses workflows de composant, c'est assez de matière pour qu'une section de plus dans une autre vue devienne illisible. L'observabilité a le sien depuis l'issue #26, qui lui a donné de la matière : collecte, alertes et tableaux de bord. -L'orchestration Airflow, elle, en a depuis les issues #115 et #116 : trois DAGs, leur image et -leurs contraintes sont décrits dans [10-infra.md](10-infra.md). +L'orchestration Airflow, elle, en a depuis les issues #115 et #116 : les sept DAGs, leur image et +leurs contraintes sont décrits dans [10-infra.md](10-infra.md), leur pilotage au quotidien dans +[70-pilotage.md](70-pilotage.md). La sécurité applicative, elle, a désormais de la matière : la vue consolidée reste dans [00-vue-ensemble.md](00-vue-ensemble.md), le détail dans [20-backend.md](20-backend.md), la @@ -38,6 +41,11 @@ GitHub rend Mermaid nativement dans les fichiers `.md`. Un diagramme est donc du relit en revue, il se diffe, et il ne se périme pas dans un binaire que plus personne ne sait rouvrir six mois plus tard. Aucune image exportée, aucun `.drawio`, aucun `.png`. +Une exception existe, et elle est connue : le schéma de données de +[40-data.md](40-data.md) est une image, `images/EnerVision-schema-donnees.png`, versionnée le 15/09, +quelques heures après l'adoption de la règle, sans que la revue le relève. Elle se relit à côté de la description des tables qui la suit, +qui fait foi ; la migrer en Mermaid reste à faire. + ### Chaque section porte son statut Une large part de la stack n'est pas écrite. Une vue qui mélange l'existant et la cible sans le diff --git a/docs/architecture/owasp-traceabilite.md b/docs/architecture/owasp-traceabilite.md index 15618ad..e0a3a18 100644 --- a/docs/architecture/owasp-traceabilite.md +++ b/docs/architecture/owasp-traceabilite.md @@ -43,15 +43,18 @@ lecture seule ; plusieurs lignes resteront à compléter une fois les endpoints | Refus de rétrograder ou désactiver le dernier administrateur actif | `app/services/user.py` | A04 Insecure Design | | Amorçage du premier administrateur hors dépôt, mot de passe jamais dans `argv` ni dans Git | `app/cli.py` | A02, A05 | | Réponse de l'API Mock bornée avant écriture : timeout, plafond de sites et de mesures, bornes physiques par grandeur, recopie des seuls champs attendus | `app/etl/mock_api_import.py` | API10 Unsafe Consumption of APIs | -| CI bloquante : format, lint avec règles Bandit, typage strict, tests avec seuil de couverture | `.github/workflows/backend.yml` | A06 Vulnerable and Outdated Components | -| Terminaison TLS au frontal, redirection 80 vers 443, HSTS et CSP posés par le proxy, limitation de débit au frontal | `infra/proxy/conf.d/enervision.conf`, ADR 0007 | API8 Security Misconfiguration, A05 | +| CI bloquante : format, lint avec règles Bandit, typage strict, tests avec seuil de couverture, SAST Bandit à partir de MEDIUM, `pip-audit` sur le verrou du backend, `npm audit --audit-level=high` sur le frontend | `.github/workflows/backend.yml`, `ml.yml`, `frontend.yml` | A06 Vulnerable and Outdated Components | +| Terminaison TLS au proxy de chaque stack, derrière un frontal SNI qui aiguille sans déchiffrer ; certificats Let's Encrypt par DNS-01 ; redirection 80 vers 443, HSTS et CSP posés par le proxy, limitation de débit sur l'adresse réelle du client (PROXY protocol) | `infra/proxy/conf.d/enervision.conf`, `infra/front/nginx.conf`, ADR 0007, ADR 0018 | API8 Security Misconfiguration, A05 | -Note sur A06 : le jeu de règles `S` de ruff, déjà actif dans `pyproject.toml`, est le portage des -règles Bandit. Ajouter Bandit à la CI serait redondant, contrairement à ce qu'annonce l'EC01. +Note sur A06 : le jeu de règles `S` de ruff, actif dans `pyproject.toml`, est le portage des +règles Bandit. Bandit lui-même a tout de même rejoint la CI le 21/09 (PR #121), bloquant à partir +de MEDIUM sur le backend et le ML, comme l'annonçait l'EC01 : les deux se recouvrent, redondance +assumée pour disposer d'un rapport SAST dédié et d'une version épinglée. -Note sur API8 : le transport est couvert, le certificat ne l'est qu'à moitié. Tant qu'aucun nom de -domaine public ne résout vers la machine, le défi HTTP-01 de Let's Encrypt ne peut pas aboutir et -le certificat servi reste auto-signé. Le chemin ACME est livré et documenté, pas exercé. +Note sur API8 : le transport et le certificat sont couverts. La machine n'a qu'une adresse privée, +le défi HTTP-01 ne peut pas aboutir : les certificats Let's Encrypt sont obtenus par défi DNS-01, +sur un domaine public dont la zone publie les enregistrements de validation (ADR 0018). L'auto-signé +ne sert plus qu'au poste de développement et aux tests e2e. ## Non couvert, et pourquoi @@ -60,9 +63,9 @@ le certificat servi reste auto-signé. Le chemin ACME est livré et documenté, | **API1 Broken Object Level Authorization** | **ouvert** | Les rôles sont globaux, il n'y a pas de portée par site : `GET /sites/{site_id}` et `GET /recommendations/{recommendation_id}` répondent à tout compte `lecteur` pour n'importe quel site ou recommandation, sans vérifier une affectation compte-site qui n'existe pas encore. Un opérateur du site A pourra agir sur le site B dès que les endpoints d'écriture métier existeront. Correctif prévu : table d'affectation compte-site, contrôle d'appartenance dans la même dépendance que le contrôle de rôle. | | **API4, lectures de séries temporelles** | **partiel** | `GET /readings` plafonne la fenêtre temporelle (90 jours) et la pagination (`limit` ≤ 2000), voir plus haut. Reste ouvert : pagination en `limit`/`offset` simple plutôt qu'en curseur (un `offset` élevé sur une fenêtre dense reste coûteux), et aucun `statement_timeout` au niveau de la connexion pour borner une requête individuelle si les plafonds au-dessus s'avéraient insuffisants. | | **API10 Unsafe Consumption of APIs** | **partiel, et spécifique à ce projet** | L'API Mock de l'école n'a aucune authentification, tourne en HTTP clair sur le réseau de l'école, et expose un endpoint mutatif à quiconque. Sa réponse est traitée comme une entrée hostile par `app/etl/mock_api_import.py`, son seul consommateur à ce jour : les quatre garde-fous attendus sont en place, voir la ligne correspondante plus haut. Reste ouvert : le plafond de taille s'applique après désérialisation de la réponse, borner le corps HTTP lui-même demanderait une lecture en flux ; et `APP_MOCK_API_BASE_URL` n'impose pas `https`, donc les identifiants Basic partiraient en clair sur une URL en `http`. La conséquence la plus sérieuse n'est pas la fausse alerte, c'est l'empoisonnement du jeu d'entraînement du modèle de prédiction. | -| **A08 Software and Data Integrity Failures** | **partiel** | La CI vérifie le code mais n'analyse ni les dépendances ni les images. `.terraform.lock.hcl` reste ignoré par git, ce qui contredit une chaîne d'approvisionnement maîtrisée. | +| **A08 Software and Data Integrity Failures** | **partiel** | La CI audite les dépendances du backend (`pip-audit` sur le verrou figé) et du frontend (`npm audit`), et les `.terraform.lock.hcl` sont versionnés. Restent ouverts : les verrous ML et Airflow ne sont pas audités (une CVE MEDIUM de `apache-airflow-providers-smtp` y reste invisible), aucune image n'est analysée, aucun scan de secrets ne tourne en CI, et les images sont reconstruites sur la machine plutôt que promues par empreinte. | | **A10 Server-Side Request Forgery** | **sans objet aujourd'hui** | Aucune URL sortante n'est pilotée par une donnée utilisateur. Le jour où l'adresse d'une source devient un champ de configuration, il faudra une liste blanche de schémas et d'hôtes, sans suivi de redirection. | -| **Cantonnement des accès ETL et ML** | **dette assumée** | Le compte applicatif porte l'identité, le rôle PostgreSQL porterait le cantonnement. Voir ADR 0003. Plus coûteuse depuis Airflow (#115) : ce service publie le port 8080, détient les identifiants Postgres complets (`ML_DATABASE_URL`, mêmes que le backend) et permet de déclencher l'exécution de code depuis son interface. Un compte Airflow compromis atteint donc toute la base, pas seulement `reading`/`site`. Aggravée par #116 : le conteneur reçoit aussi `DATABASE_URL` et exécute le code du backend en sous-processus (ADR 0008). Atténuations en place : le compte admin Airflow est distinct des `app_user` et son mot de passe passe par l'environnement, jamais par `argv` ; et l'`APP_SECRET_KEY` donnée à Airflow est distincte de celle de l'API, pour qu'une compromission ne livre pas la clé de signature des JWT. | +| **Cantonnement des accès ETL et ML** | **dette assumée** | Le compte applicatif porte l'identité, le rôle PostgreSQL porterait le cantonnement. Voir ADR 0003. Plus coûteuse depuis Airflow (#115) : ce service publie le port 8080 (sur le poste de développement ; sur la machine, sur la boucle locale seulement), détient les identifiants Postgres complets (`ML_DATABASE_URL`, mêmes que le backend) et permet de déclencher l'exécution de code depuis son interface. Un compte Airflow compromis atteint donc toute la base, pas seulement `reading`/`site`. Aggravée par #116 : le conteneur reçoit aussi `DATABASE_URL` et exécute le code du backend en sous-processus (ADR 0008). Atténuations en place : le compte admin Airflow est distinct des `app_user` et son mot de passe passe par l'environnement, jamais par `argv` ; et l'`APP_SECRET_KEY` donnée à Airflow est distincte de celle de l'API, pour qu'une compromission ne livre pas la clé de signature des JWT. | | **Non-répudiation de l'audit** | **dette assumée** | Les déclencheurs arrêtent les accidents, pas un compte détenant `ALTER TABLE`. Voir ADR 0004. | ## Ce qu'il faut répondre, et ne pas répondre diff --git a/etl/README.md b/etl/README.md index f5d69f5..14c43c3 100644 --- a/etl/README.md +++ b/etl/README.md @@ -1,4 +1,4 @@ -# Pipeline ETL — EnerVision +# Pipeline ETL · EnerVision ## Objectif @@ -236,11 +236,11 @@ apps/backend/ exécuter : -```powershell -uv run python -m app.etl.historical_import ` - --csv ..\..\data\raw\all_sites_combined.csv ` - --metadata ..\..\data\raw\dataset_metadata.json ` - --source-timezone UTC ` +```bash +uv run python -m app.etl.historical_import \ + --csv ../../data/raw/all_sites_combined.csv \ + --metadata ../../data/raw/dataset_metadata.json \ + --source-timezone UTC \ --dry-run ``` @@ -250,10 +250,10 @@ Aucune donnée n'est écrite dans la base pendant cette exécution. Depuis `apps/backend/` : -```powershell -uv run python -m app.etl.historical_import ` - --csv ..\..\data\raw\all_sites_combined.csv ` - --metadata ..\..\data\raw\dataset_metadata.json ` +```bash +uv run python -m app.etl.historical_import \ + --csv ../../data/raw/all_sites_combined.csv \ + --metadata ../../data/raw/dataset_metadata.json \ --source-timezone UTC ``` @@ -303,7 +303,7 @@ Une nouvelle exécution du même import ne crée donc pas de mesures supplément Depuis la racine du projet, vérifier le nombre d'enregistrements avec : -```powershell +```bash docker compose exec db psql -U enervision -d enervision -c "SELECT COUNT(*) AS datasets FROM dataset; SELECT COUNT(*) AS sites FROM site; SELECT COUNT(*) AS readings FROM reading;" ``` @@ -317,7 +317,7 @@ readings = 122647 Vérifier la source des mesures avec : -```powershell +```bash docker compose exec db psql -U enervision -d enervision -c "SELECT source, COUNT(*) FROM reading GROUP BY source ORDER BY source;" ``` @@ -384,7 +384,9 @@ end_time limit ``` -Le paramètre `limit` doit être compris entre 1 et 1000. +Le paramètre `limit` doit être compris entre 1 et 1000. Il n'est plus fourni par l'appelant : +`limit_for_window()` le dérive de la fenêtre demandée, pour obtenir une lecture par heure, +alignée sur l'heure pile. ### Configuration de l'API Mock @@ -485,8 +487,13 @@ data_quality = "degraded" L'import ne s'interrompt pas pour autant : le mock émet des anomalies par construction, et `raw_data` conserve la réponse d'origine. -La taille des réponses est plafonnée : au plus `MAX_SITES` sites, et au plus `--limit` mesures -par site. Au-delà, l'import échoue au lieu de charger. +La taille des réponses est plafonnée : au plus `MAX_SITES` sites, et au plus `limit` mesures par +site, une par heure de la fenêtre (`limit_for_window()`). Au-delà, l'import échoue au lieu de +charger. + +L'import refuse aussi une fenêtre qui recouvre le dataset historique +(`refuse_if_overlaps_historical_dataset()`) : le CSV couvre 2023 et 2024, une fenêtre de l'API Mock +doit donc commencer après le 31/12/2024, et démarrer pile sur une heure. Enfin, seuls les champs attendus sont recopiés vers la base. Une clé supplémentaire renvoyée par l'API n'atteint jamais une colonne. @@ -495,30 +502,36 @@ l'API n'atteint jamais une colonne. Le mode `--dry-run` permet de tester la connexion, la récupération des sites et la récupération des mesures sans écrire dans PostgreSQL. -Depuis `apps/backend/` : +Depuis `apps/backend/`, sur une fenêtre de deux heures postérieure au dataset historique (une +lecture par heure et par site) : -```powershell -uv run python -m app.etl.mock_api_import ` - --start-time "2024-06-15T12:00:00" ` - --end-time "2024-06-15T13:00:00" ` - --limit 60 ` +```bash +uv run python -m app.etl.mock_api_import \ + --start-time "2026-09-24T08:00:00" \ + --end-time "2026-09-24T10:00:00" \ --dry-run ``` +Les heures sans fuseau sont lues en UTC. Seuls `--start-time`, `--end-time` et `--dry-run` +existent. + ### Chargement réel depuis l'API Mock Depuis `apps/backend/` : -```powershell -uv run python -m app.etl.mock_api_import ` - --start-time "2024-06-15T12:00:00" ` - --end-time "2024-06-15T13:00:00" ` - --limit 60 +```bash +uv run python -m app.etl.mock_api_import \ + --start-time "2026-09-24T08:00:00" \ + --end-time "2026-09-24T10:00:00" ``` -### Résultat validé pour l'API Mock +En fonctionnement normal, cet import n'est pas lancé à la main : le DAG `mock_api_import` le +joue chaque heure (voir la fin de ce document). -Le scénario de validation utilisé couvre la période : +### Validation initiale du 18/09, antérieure à la réconciliation + +Le premier scénario de validation, joué le 18/09 avant la clôture de la réconciliation (#15), +couvrait la période : ```text 15/06/2024 12:00 UTC @@ -526,9 +539,11 @@ Le scénario de validation utilisé couvre la période : 15/06/2024 13:00 UTC ``` -avec une limite de 60 lectures par site. +avec une limite de 60 lectures par site. Il ne se rejoue plus tel quel : depuis le 23/09, cette +fenêtre est refusée parce qu'elle recouvre le dataset historique, et l'option `--limit` a disparu +au profit d'une lecture par heure. -Résultat obtenu : +Résultat obtenu à l'époque : ```text sites récupérés : 7 @@ -581,7 +596,8 @@ Les tests de l'import API Mock couvrent notamment : - la récupération des sites ; - l'appel à `/api/v1/readings` ; -- les paramètres `site_id`, `start_time`, `end_time` et `limit` ; +- les paramètres `site_id`, `start_time`, `end_time` et `limit`, dérivé de la fenêtre ; +- le refus d'une fenêtre qui recouvre le dataset historique ; - la gestion des erreurs HTTP ; - la validation du format de la réponse ; - la transformation des mesures ; @@ -594,48 +610,43 @@ Les tests de l'import API Mock couvrent notamment : Exécuter les tests ETL : -```powershell -uv run pytest tests\etl -v +```bash +uv run pytest tests/etl -v ``` Exécuter les tests unitaires de l'import API Mock : -```powershell -uv run pytest tests\etl\test_mock_api_import.py -v +```bash +uv run pytest tests/etl/test_mock_api_import.py -v ``` Exécuter le test d'intégration de l'import API Mock : -```powershell -uv run pytest tests\etl\test_mock_api_import.py -m integration -v +```bash +uv run pytest tests/etl/test_mock_api_import.py -m integration -v ``` Contrôler la qualité du code : -```powershell -uv run ruff check app\etl tests\etl +```bash +uv run ruff check app/etl tests/etl ``` Contrôler le typage : -```powershell +```bash uv run mypy app ``` Exécuter la suite complète avec le seuil de couverture : -```powershell +```bash uv run pytest --cov-fail-under=85 ``` -Lors de la validation de l'import API Mock : - -```text -8 tests unitaires passés -1 test d'intégration passé -``` - -La suite backend complète a également été validée avec une couverture supérieure au seuil de 85 %. +Lors de la première validation, le 18/09, le fichier comptait 8 tests unitaires et 1 test +d'intégration. Il en compte aujourd'hui 42, dont 2 d'intégration (`pytest --collect-only`), après +l'ajout des bornes physiques, de la réconciliation et de l'alignement horaire. ## Suite du pipeline Data @@ -663,10 +674,12 @@ mock_api_import.py La logique d'extraction, de transformation et de chargement est donc disponible pour les deux sources de données du MVP. -Airflow tourne désormais réellement (`etl/airflow/`, `make airflow-up`) et orchestre cinq DAGs : +Airflow tourne désormais réellement (`etl/airflow/`, `make airflow-up`) et orchestre sept DAGs : le pipeline ML (`ml_train` et `ml_score`, issue #115), la détection d'alertes et la génération des recommandations (`alertes`, issue #116), l'import historique (`historical_import`, -issue #119) et l'import périodique de l'API Mock (`mock_api_import`, issue #15). +issue #119), l'import périodique de l'API Mock (`mock_api_import`, issue #15), la surveillance +de dérive du modèle (`derive`, ADR 0013) et la rétention des relevés, exportés vers Garage puis +supprimés (`retention`, issue #36, ADR 0019). Le DAG `mock_api_import` s'exécute chaque heure, à la minute `:45`, sur une fenêtre qui part de l'heure pile précédant son déclenchement jusqu'à l'instant du déclenchement lui-même (pas @@ -681,7 +694,7 @@ Les deux pipelines normalisent leurs données vers les tables communes `site` et conservant leur source (`csv` ou `api_history`). La réconciliation entre les deux sources (issue #15) est close : voir `docs/architecture/40-data.md`. -Airflow permet de planifier les traitements, gérer leur ordre d'exécution, suivre leur état et remonter les erreurs. Il ne remplace pas la logique ETL Python existante : les scripts actuels restent responsables de l'extraction, de la validation, de la transformation et du chargement. `etl/airflow/dags/ml_train.py`, `ml_score.py`, `alertes.py`, `historical_import.py` et -`mock_api_import.py` montrent le patron retenu (des `BashOperator` qui invoquent le script tel quel, dans l'environnement `uv` que l'image embarque pour lui). +Airflow permet de planifier les traitements, gérer leur ordre d'exécution, suivre leur état et remonter les erreurs. Il ne remplace pas la logique ETL Python existante : les scripts actuels restent responsables de l'extraction, de la validation, de la transformation et du chargement. `etl/airflow/dags/ml_train.py`, `ml_score.py`, `alertes.py`, `historical_import.py`, +`mock_api_import.py`, `derive.py` et `retention.py` montrent le patron retenu (des `BashOperator` qui invoquent le script tel quel, dans l'environnement `uv` que l'image embarque pour lui). Le pipeline Data servira ensuite à préparer les données nécessaires au modèle de Machine Learning. diff --git a/infra/proxy/README.md b/infra/proxy/README.md index 0630576..23b92d4 100644 --- a/infra/proxy/README.md +++ b/infra/proxy/README.md @@ -1,7 +1,9 @@ # Reverse proxy -Terminaison TLS et routage de la stack déployée. Seul composant publié sur le réseau : il -écoute en 80 et 443, et rien d'autre ne sort du réseau Compose. +Terminaison TLS et routage de la stack déployée. Sur un poste, seul composant publié sur le +réseau : il écoute en 80 et 443, et rien d'autre ne sort du réseau Compose. Sur la VM, il +n'écoute plus que sur `127.0.0.1`, derrière le frontal SNI `infra/front`, seul composant exposé +([ADR 0018](../../docs/adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md)). - `nginx.conf` : bloc `http`, journalisation, compression, zones de limitation de débit. - `conf.d/enervision.conf` : redirection 80 vers 443, terminaison TLS, en-têtes de sécurité, @@ -15,11 +17,13 @@ configuration est montée en volume par `docker-compose.prod.yml`. L'overlay emploie les marqueurs `!override` et `!reset`, qui demandent **Docker Compose 2.24.4 ou plus récent**. Sur une version antérieure, la fusion échoue au lieu de dépublier les ports. -Les ports publiés sont `PROXY_HTTP_PORT` et `PROXY_HTTPS_PORT`, 80 et 443 par défaut. Quand deux -environnements partagent la machine ([ADR 0009](../../docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md)), -la recette publie `8443` et ramène son port 80 sur `127.0.0.1:8081` : la redirection ci-dessous -renvoie vers `https://$host` sans port, donc vers la production. `PUBLIC_ORIGIN` porte alors -l'origine avec son port pour le CORS et le lien de réinitialisation. +Les ports publiés sont `PROXY_HTTP_PORT`, `PROXY_HTTPS_PORT` et `PROXY_FRONT_PORT`, 80, 443 et un +port aléatoire de la boucle locale par défaut. Sur la VM, trois environnements partagent la +machine ([ADR 0009](../../docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md), +[ADR 0017](../../docs/adr/0017-environnement-dev-a-la-demande.md)) : `scripts/provision-host.sh` +place les ports de chaque proxy sur `127.0.0.1` (HTTPS en 10443, 8443 et 9443 pour la production, +la recette et le dev), et le frontal aiguille chaque nom vers le sien. Les URL publiques n'ont +donc plus de port, et `PUBLIC_ORIGIN` vaut `https://` suivi du nom de l'environnement. ## Routage @@ -55,15 +59,15 @@ make tls-selfsigned PUBLIC_HOST=enervision.local make stack-up ``` -Le navigateur avertira d'un émetteur inconnu : c'est attendu, et c'est le seul mode exploitable -tant que la machine cible n'a pas de nom de domaine public. +Le navigateur avertira d'un émetteur inconnu : c'est attendu. C'est le mode du poste de +développement et des tests e2e ; la VM utilise Let's Encrypt par DNS-01 (plus bas). ### Let's Encrypt Le défi HTTP-01 exige un nom de domaine **résolvable publiquement** et le port 80 joignable -depuis Internet. La cible documentée aujourd'hui (`ssh_host = "10.0.0.10"`, serveur de l'école) -ne remplit ni l'une ni l'autre condition : le chemin ci-dessous est livré et documenté, il n'a -pas été exercé. +depuis Internet. La VM de l'école ne remplit ni l'une ni l'autre condition : ce chemin reste +livré pour une machine publique, et n'a pas été exercé. La VM passe par DNS-01 (section +suivante). ```bash make stack-up # nginx doit tourner pour servir le défi diff --git a/infra/terraform/environments/vm-eni/terraform.tfvars.example b/infra/terraform/environments/vm-eni/terraform.tfvars.example index e5e8ec4..49f68e7 100644 --- a/infra/terraform/environments/vm-eni/terraform.tfvars.example +++ b/infra/terraform/environments/vm-eni/terraform.tfvars.example @@ -1,4 +1,4 @@ -ssh_host = "10.101.200.37" +ssh_host = "" ssh_port = 22 ssh_user = "root" ssh_private_key_path = "~/.ssh/id_ed25519" diff --git a/monitoring/README.md b/monitoring/README.md index b29bf92..75d2040 100644 --- a/monitoring/README.md +++ b/monitoring/README.md @@ -18,7 +18,7 @@ Les interfaces n'écoutent que sur `127.0.0.1`. Depuis un poste, on passe par un comme pour Airflow : ```bash -ssh -L 3001:127.0.0.1:3001 -L 9090:127.0.0.1:9090 enervision@10.101.200.37 +ssh -L 3001:127.0.0.1:3001 -L 9090:127.0.0.1:9090 enervision@ ``` ## Démarrer diff --git a/scripts/README.md b/scripts/README.md index 8914c5d..33a8eda 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -19,3 +19,35 @@ Rejouable, en root sur la VM : `COFFRE_TAILLE=30G COFFRE_MIGRER=1 bash scripts/c `COFFRE_MIGRER=1`, le coffre est préparé mais les volumes existants ne sont pas déplacés : la migration arrête Docker le temps de la copie. Variables : `COFFRE_IMAGE`, `COFFRE_CLE`, `COFFRE_MONTAGE`, `COFFRE_TAILLE`. La clé est à sauvegarder hors de la VM : perdue, tout est perdu. +Le script refuse de démarrer dans un conteneur LXC (`systemd-detect-virt`) ou sans device-mapper : +c'est le cas de la machine ENI, où le chiffrement du disque relève de l'hôte Proxmox. + +## provision-host.sh + +Prépare la machine et ses trois environnements, `prod` sur `main`, `rec` et `dev` sur `dev` +([ADR 0009](../docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md), +[ADR 0017](../docs/adr/0017-environnement-dev-a-la-demande.md)), sans démarrer aucune stack. Pour +chacun, sous `RACINE` (`/srv/enervision` par défaut) : un clone du dépôt, un `.env` en `600` dont +les secrets sont générés sur place et jamais réécrits s'ils existent, l'adressage réaligné, un +certificat Let's Encrypt par DNS-01 si le jeton dynv6 est dans `RACINE/dns.token` (auto-signé +sinon). Lancé en root avec `PROPRIETAIRE`, il pose aussi la tâche cron de renouvellement et donne +les dossiers au compte du runner. Joué par Terraform (`infra/terraform/environments/vm-eni`) ou à +la main : `PROPRIETAIRE= bash scripts/provision-host.sh`. +Rejouable. Variables : `REPO_URL`, `RACINE`, `DOMAINE`, `PUBLIC_IP`, `PROPRIETAIRE`. + +## tls-selfsigned.sh + +Écrit un certificat auto-signé dans `infra/proxy/tls/` (`fullchain.pem`, `privkey.pem`), là où +nginx lit toujours ses certificats, quel que soit le mode d'obtention. Sert au poste de +développement et aux tests e2e ; sur la machine, il ne reste en place que si Let's Encrypt échoue. +`make tls-selfsigned PUBLIC_HOST=enervision.local`, `FORCE=1` pour écraser. Variables : +`PUBLIC_HOST`, `PUBLIC_IP` (ajoutée au certificat), `TLS_DAYS` (365 par défaut). + +## comptes-test.sh + +Réservé à une base **jetable** (CI, e2e, charge sur le poste) : crée un administrateur par la CLI +du backend, puis un lecteur et un opérateur, leur fait passer le changement de mot de passe +obligatoire, et écrit leurs identifiants en JSON dans `COMPTES_FICHIER`. Appelé par `e2e.yml` et +par `make e2e-prepare`. Variables : `BASE_URL` (`http://localhost:8000` par défaut), +`COMPTES_FICHIER` (requis), `APP_CLI`, `ADMIN_SUPPLEMENTAIRE=1` pour la base du poste, qui a déjà +un administrateur. Nécessite `curl`, `jq` et `openssl`.