From cb23895026df2e2f0f91dfb05a904387f7b73038 Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Mon, 21 Sep 2026 14:59:32 +0200 Subject: [PATCH 1/3] =?UTF-8?q?feat(infra,ci):=20deux=20environnements=20r?= =?UTF-8?q?ec=20et=20prod=20sur=20la=20VM=20ENI,=20d=C3=A9ploy=C3=A9s=20pa?= =?UTF-8?q?r=20un=20runner=20auto-h=C3=A9berg=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un projet Compose par environnement sur la même machine : ports du proxy et origine publique en variables dans docker-compose.prod.yml, réglage mémoire des deux bases TimescaleDB et du webserver Airflow. Le workflow deploy.yml déploie dev en recette et main en production depuis un runner installé sur la VM, jamais sur pull_request. scripts/provision-host.sh prépare les deux dossiers, secrets et certificats compris, sans rien démarrer. L'image frontend quitte dhi.io/nginx, registre authentifié dont personne n'a l'accès, pour nginx:1.28-alpine : elle n'avait jamais été construite. ADR 0009, vues infra et CI/CD, README et .env.example mis à jour. Refs #21, #22 --- .env.example | 18 ++++ .github/workflows/deploy.yml | 54 +++++++++++ README.md | 6 ++ apps/frontend/Dockerfile | 2 +- docker-compose.prod.yml | 15 ++- docs/README.md | 1 + ...ux-environnements-compose-sur-la-vm-eni.md | 80 ++++++++++++++++ docs/architecture/00-vue-ensemble.md | 4 +- docs/architecture/10-infra.md | 30 +++++- docs/architecture/50-cicd.md | 65 +++++++++++-- infra/proxy/README.md | 6 ++ scripts/provision-host.sh | 96 +++++++++++++++++++ 12 files changed, 360 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md create mode 100755 scripts/provision-host.sh diff --git a/.env.example b/.env.example index e909bd1..b2e8b6b 100644 --- a/.env.example +++ b/.env.example @@ -53,3 +53,21 @@ AIRFLOW_APP_SECRET_KEY=change_me # PUBLIC_HOST alimente l'origine CORS, le lien de réinitialisation et le certificat. 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:`. +# 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. +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). +PROXY_HTTP_PORT= +PROXY_HTTPS_PORT= +# Réglages mémoire de la stack déployée. Sans eux, timescaledb-tune réserve 25 % de la RAM de la +# machine à chaque base au premier démarrage, et le webserver Airflow lance 4 workers gunicorn. +TS_TUNE_MEMORY=2GB +TS_TUNE_NUM_CPUS=2 +AIRFLOW_WEBSERVER_WORKERS=2 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f1f58d0 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,54 @@ +# Pourquoi : le runner tourne sur la VM ENI, adresse privée que les runners hébergés par GitHub +# ne joignent pas, et travaille dans un dossier stable par environnement plutôt que dans son +# espace de travail : `.env`, certificats et volumes y survivent d'un déploiement à l'autre. +# Piège : jamais de déclencheur `pull_request` ici. Sur un dépôt public, une PR de fork +# exécuterait son code sur la machine de production (ADR 0009) - job deploy. + +name: Déploiement + +on: + push: + branches: [dev, main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: deploy-${{ github.ref_name }} + cancel-in-progress: false + +jobs: + deploy: + runs-on: [self-hosted, linux, eni-g3] + environment: + name: ${{ github.ref_name == 'main' && 'prod' || 'rec' }} + url: ${{ github.ref_name == 'main' && 'https://enervision.local' || 'https://rec.enervision.local:8443' }} + env: + ENVIRONNEMENT: ${{ github.ref_name == 'main' && 'prod' || 'rec' }} + PORT_HTTPS: ${{ github.ref_name == 'main' && '443' || '8443' }} + steps: + - name: Aligner le dossier de l'environnement sur la branche poussée + run: | + cd "/srv/enervision/${ENVIRONNEMENT}" + git fetch --quiet origin "${GITHUB_REF_NAME}" + git checkout --quiet "${GITHUB_REF_NAME}" + git reset --quiet --hard "origin/${GITHUB_REF_NAME}" + git log -1 --format='%h %s' + + - name: Reconstruire et redémarrer la stack + run: | + cd "/srv/enervision/${ENVIRONNEMENT}" + make stack-up + + - name: Attendre que l'API réponde derrière le proxy + run: | + for tentative in $(seq 1 36); do + if curl --fail --silent --insecure "https://localhost:${PORT_HTTPS}/api/v1/health/ready"; then + exit 0 + fi + sleep 5 + done + echo "L'API ne répond pas après 3 minutes" >&2 + cd "/srv/enervision/${ENVIRONNEMENT}" && docker compose ps + exit 1 diff --git a/README.md b/README.md index cc139dc..6a2b21b 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,12 @@ nom de domaine public ne résout vers la machine. Routage, mode ACME et renouvel [`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 +workflow `deploy.yml` les redéploie à chaque push par un runner auto-hébergé. Ports, noms +d'hôte et garde-fous dans [`docs/architecture/10-infra.md`](docs/architecture/10-infra.md) et +[l'ADR 0009](docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md). + ## Conventions - Branches : `feat/`, `fix/`, `chore/`, `docs/`, `test/` suivi d'un libelle court. diff --git a/apps/frontend/Dockerfile b/apps/frontend/Dockerfile index 1890bc1..341e2fc 100644 --- a/apps/frontend/Dockerfile +++ b/apps/frontend/Dockerfile @@ -23,7 +23,7 @@ RUN npm run build # ================== -FROM dhi.io/nginx:1.28.0-alpine3.21-dev AS runner +FROM nginx:1.28-alpine AS runner # Copie de la configuration de nginx COPY --chown=root:root --chmod=755 nginx.conf /etc/nginx/nginx.conf diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 868ae17..c36c1ee 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -5,6 +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). name: enervision @@ -12,6 +14,9 @@ services: db: ports: !override - "127.0.0.1:${POSTGRES_PORT:-5433}:5432" + environment: + TS_TUNE_MEMORY: ${TS_TUNE_MEMORY:-2GB} + TS_TUNE_NUM_CPUS: ${TS_TUNE_NUM_CPUS:-2} mailpit: ports: !override @@ -20,6 +25,8 @@ services: airflow-webserver: ports: !override - "127.0.0.1:${AIRFLOW_PORT:-8080}:8080" + environment: + AIRFLOW__WEBSERVER__WORKERS: ${AIRFLOW_WEBSERVER_WORKERS:-2} backend: ports: !reset null @@ -37,8 +44,8 @@ services: APP_ENV: prod APP_DEBUG: "false" APP_TRUST_PROXY_HEADERS: "true" - APP_CORS_ORIGINS: https://${PUBLIC_HOST:-enervision.local} - APP_FRONTEND_RESET_PASSWORD_URL: https://${PUBLIC_HOST:-enervision.local}/reset-password + APP_CORS_ORIGINS: ${PUBLIC_ORIGIN:-https://${PUBLIC_HOST:-enervision.local}} + APP_FRONTEND_RESET_PASSWORD_URL: ${PUBLIC_ORIGIN:-https://${PUBLIC_HOST:-enervision.local}}/reset-password frontend: ports: !reset null @@ -51,8 +58,8 @@ services: frontend: condition: service_started ports: - - "80:80" - - "443:443" + - "${PROXY_HTTP_PORT:-80}:80" + - "${PROXY_HTTPS_PORT:-443}:443" volumes: - ./infra/proxy/nginx.conf:/etc/nginx/nginx.conf:ro - ./infra/proxy/conf.d:/etc/nginx/conf.d:ro diff --git a/docs/README.md b/docs/README.md index ca50a10..9d4604d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,3 +15,4 @@ | [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é | 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 new file mode 100644 index 0000000..0cc1a66 --- /dev/null +++ b/docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md @@ -0,0 +1,80 @@ +# 0009 - Deux environnements sur la VM ENI, un projet Compose chacun, déployés par un runner auto-hébergé + +- Statut : accepté +- Date : 2026-09-21 + +## Contexte + +La grille note EC03 à EC06 sur ce qui est déployé et fonctionnel au J10. Au 21/09, rien ne +l'est : la CI s'arrête au merge (issue #21), la topologie Compose avec reverse proxy +([ADR 0007](0007-terminaison-tls-et-reverse-proxy-nginx.md)) n'a jamais quitté le poste, et le +module Terraform k3s n'a jamais été appliqué. L'école met à disposition une seule VM, +`eadl-2025-nantes-g3`, sur une adresse privée que les runners hébergés par GitHub ne joignent +pas, sans DNS public. + +Il faut deux environnements, recette et production, parce que la stratégie de branches en a +déjà deux, `dev` et `main`, et qu'un déploiement direct en production à chaque merge sur `dev` +n'est pas défendable. + +La branche `feat/deploy` tentait de déployer par provisioners Terraform : nginx système et copie +du build Angular. La revue postée sur #21 relève huit points bloquants, dont des racines `rec` +et `prod` qui ne passent pas `terraform validate`. + +## Décision + +**Un projet Docker Compose par environnement, sur la même machine.** Deux clones du dépôt, +`/srv/enervision/rec` sur `dev` et `/srv/enervision/prod` sur `main`, chacun avec son `.env` et +son `COMPOSE_PROJECT_NAME`. Le nom de projet préfixe volumes, réseau et conteneurs : les deux +stacks ne partagent rien. + +**Les ports du proxy et l'origine publique deviennent des variables** de +`docker-compose.prod.yml`. La production garde 80 et 443. La recette publie 8443 et ramène sa +redirection HTTP sur la boucle locale, faute de quoi elle renverrait vers la production. Base, +Mailpit et Airflow restent sur `127.0.0.1`, décalés d'un port. + +**Deux noms d'hôte**, `enervision.local` et `rec.enervision.local`, sur la même IP. Le cookie de +rafraîchissement est posé par hôte, pas par port : un seul nom ferait se déconnecter la +production à chaque connexion en recette. + +**Un runner GitHub Actions auto-hébergé sur la VM** exécute `deploy.yml` : un `push` sur `dev` +déploie la recette, un `push` sur `main` déploie la production après approbation dans +l'environnement GitHub `prod`. Le job aligne le clone sur la branche puis lance `make stack-up`. +Les images sont construites sur la machine. + +**Les secrets vivent dans le `.env` de chaque dossier**, générés sur la machine par +`scripts/provision-host.sh`, jamais dans git ni dans GitHub. Le runner n'a besoin d'aucun +secret. + +## Alternatives écartées + +- **k3s avec un namespace par environnement** : le cluster serait vide, sans manifeste, sans + registre, sans stockage persistant. C'est la cible de `10-infra.md`, pas celle de la semaine. +- **Provisioners Terraform de `feat/deploy`** : voir la revue sur #21. Terraform reste l'outil + de provisionnement de la machine, pas de livraison applicative. +- **Deux machines**, VM Proxmox et VM Azure ENI : une deuxième infrastructure à justifier devant + le jury et à provisionner, pour un bénéfice nul sur la grille. +- **Un seul proxy frontal routant par nom d'hôte vers les deux stacks** : des URL sans port, + mais le proxy devrait joindre deux réseaux Compose où les services portent les mêmes noms. + La complexité dépasse le gain. +- **Images publiées sur GHCR et déployées par digest** : la bonne pratique, remise à plus tard. + Un registre à authentifier sur la machine, alors que le runner y construit déjà. + +## Conséquences + +- Deux TimescaleDB sur une machine de 8 Go : sans réglage, chacune se réserverait 25 % de la + RAM au premier démarrage. L'overlay fixe `TS_TUNE_MEMORY` à 2 Go et `TS_TUNE_NUM_CPUS` à 2 par + base, et 2 workers gunicorn par webserver Airflow. La montée à 32 Go prévue par les + consignes est à demander. +- Un runner auto-hébergé sur un dépôt public exécute le code qu'on lui envoie. `deploy.yml` ne + se déclenche jamais sur `pull_request`, le runner tourne sous un utilisateur dédié, et le + dépôt doit exiger une approbation pour les workflows des PR externes. +- Les deux environnements construisent leurs images séparément à partir du même commit : ce qui + tourne en production a été construit deux fois, pas promu. Le passage à GHCR lèvera cette + limite. +- Le `make stack-up` du runner reconstruit l'image Airflow, qui copie `ml/` et `apps/backend/`, + à chaque push : plusieurs minutes par déploiement, acceptable pour la cadence du projet. +- `environments/prod` de Terraform reste vide. Le provisionnement de la machine est porté par + `scripts/provision-host.sh`, que Terraform pourra appeler par `remote-exec` le jour où une + racine visant la VM existera. +- L'image frontend quitte `dhi.io/nginx`, registre authentifié dont personne n'a l'accès, pour + `nginx:1.28-alpine`, la même image que le proxy. Elle n'avait jamais été construite. diff --git a/docs/architecture/00-vue-ensemble.md b/docs/architecture/00-vue-ensemble.md index 7bdd1da..558a496 100644 --- a/docs/architecture/00-vue-ensemble.md +++ b/docs/architecture/00-vue-ensemble.md @@ -89,7 +89,7 @@ collecteur ne vient le lire. | 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)). Module d'installation k3s jamais appliqué, aucune ressource Kubernetes déclarée | | Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Cible` | Rien, hors le `/metrics` exposé par l'API | | ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver + scheduler (LocalExecutor) tournent via docker-compose, base de métadonnées Postgres dédiée. Trois DAGs en sous-processus `uv run` : `ml_train` manuel et `ml_score` `@hourly` pour le pipeline ML (issue #115), `alertes` à `15 * * * *` pour la détection et les recommandations (issue #116, [ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md)). L'ingestion (issues #15/#16) n'a pas encore de DAG | -| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | 5 workflows, 16 jobs : 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. Détail dans [50-cicd.md](50-cicd.md). **Aucun job de déploiement** (#21) | +| CI/CD | GitHub Actions | `.github/workflows` | `Fait` | 6 workflows, 17 jobs : 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. Déploiement continu vers la VM ENI par `deploy.yml` et un runner auto-hébergé : `dev` en recette, `main` en production après approbation ([ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)). Détail dans [50-cicd.md](50-cicd.md) | ## Flux bout en bout @@ -186,3 +186,5 @@ Elles vivent dans `../adr/`, pas ici. | [0005](../adr/0005-modele-prediction-lightgbm.md) | Modèle de prédiction de consommation : LightGBM | | [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é | diff --git a/docs/architecture/10-infra.md b/docs/architecture/10-infra.md index c6121e7..05bfb4b 100644 --- a/docs/architecture/10-infra.md +++ b/docs/architecture/10-infra.md @@ -7,6 +7,7 @@ quel contexte, quelles décisions sont arrêtées, et ce qui manque encore entre |---|---|---| | 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 | `Fait` | | k3s single-node | Cible à terme | `En cours` | ## Poste de développement @@ -169,6 +170,30 @@ Deux conséquences se propagent jusqu'à l'application, et elles ne se devinent - `APP_TRUST_PROXY_HEADERS` passe à vrai en même temps, sinon la limitation de débit par IP compte sur l'IP du proxy et devient globale. +### Deux environnements sur la même machine + +Statut : `Fait`. Décision et motifs dans l'[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md). +La VM `eadl-2025-nantes-g3` porte la recette et la production, chacune dans son clone du dépôt, +son `.env` et son projet Compose. Le nom de projet préfixe volumes, réseau et conteneurs : rien +n'est partagé. `scripts/provision-host.sh` prépare les deux dossiers, génère les secrets et les +certificats, et ne démarre rien. + +| | Recette | Production | +|---|---|---| +| Branche, environnement GitHub | `dev`, `rec` | `main`, `prod` | +| Dossier, projet Compose | `/srv/enervision/rec`, `enervision-rec` | `/srv/enervision/prod`, `enervision-prod` | +| URL | `https://rec.enervision.local:8443` | `https://enervision.local` | +| Proxy HTTP, HTTPS | `127.0.0.1:8081`, `8443` | `80`, `443` | +| PostgreSQL, Mailpit, Airflow, sur `127.0.0.1` | `5434`, `8026`, `8082` | `5433`, `8025`, `8080` | + +Les deux noms d'hôte visent la même IP, à déclarer dans le `/etc/hosts` des postes. Deux noms +distincts sont nécessaires : le cookie `__Secure-ev_refresh` est posé par hôte, pas par port. +La redirection HTTP de la recette est ramenée sur la boucle locale parce que la configuration +Nginx renvoie vers `https://$host` sans port, c'est-à-dire vers la production. + +Le déploiement est décrit dans [50-cicd.md](50-cicd.md) : un runner GitHub Actions installé sur +la VM aligne le dossier sur la branche poussée et lance `make stack-up`. + ## Cible à terme, k3s Statut : `En cours`. Le module `infra/terraform/modules/k3s/` installe le cluster. Il n'a jamais @@ -227,6 +252,9 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de | Deux racines, `dev` et `prod` | Séparation des états et des variables par environnement | `environments/` | | 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` | +| 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` | ## Ports et noms @@ -237,7 +265,7 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de | API | `8000` | Identique en conteneur et hors conteneur | | Frontend, `ng serve` | `4200` | Boucle de développement. Valeur par défaut d'`APP_CORS_ORIGINS` | | Frontend en conteneur | `3000` | Ce qu'écoute le nginx de l'image, en conteneur comme côté hôte | -| Reverse proxy | `80` et `443` | Les seuls ports publiés par `docker-compose.prod.yml`. 80 ne sert que la redirection et le défi ACME | +| Reverse proxy | `80` et `443` | Les seuls ports publiés par `docker-compose.prod.yml`, via `PROXY_HTTP_PORT` et `PROXY_HTTPS_PORT`. 80 ne sert que la redirection et le défi ACME. La recette publie `8443` et `127.0.0.1:8081` | | SSH du serveur | `22` par défaut | `ssh_port`, redéfinissable | | Base applicative | `enervision` | Variable `POSTGRES_DB` | | Base de test | `enervision_test` | Créée par `db/init/110-test-database.sql`, nom attendu en dur par `apps/backend/tests/conftest.py` | diff --git a/docs/architecture/50-cicd.md b/docs/architecture/50-cicd.md index 4fcd851..07f63d6 100644 --- a/docs/architecture/50-cicd.md +++ b/docs/architecture/50-cicd.md @@ -6,11 +6,13 @@ 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 | Porter un artefact vérifié jusqu'à la machine de déploiement | `Cible` | +| Livraison continue | Déployer chaque branche d'intégration sur son environnement de la VM ENI | `Fait` | -Le **D** de CI/CD n'existe pas encore : aucun job de déploiement, aucune construction d'image -publiée, aucun environnement GitHub. L'issue #21 le porte. C'est la limite principale de cet -étage, et elle est nommée ici plutôt que découverte en soutenance. +Le **D** de CI/CD existe 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)). 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. ## Vue d'ensemble @@ -54,7 +56,12 @@ flowchart TB push --> mv & ms push --> av & ab push --> sb1 & sb2 --> sscan - sscan -.-> cd["deploy
issue #21"] + + subgraph cd["Déploiement · deploy.yml"] + dep["deploy
runner eni-g3, environnement rec ou prod"] + end + + push -->|"push sur dev ou main"| dep ``` ## Déclenchement @@ -85,6 +92,39 @@ git avec `cancel-in-progress`, ce qui annule un run devenu obsolète par un push ne supporte pas encore 3.14. Le 3.14 du module ML ne vit, dans ce contexte, que dans l'image Docker et son propre environnement. +## Déploiement + +`deploy.yml` est le sixième workflow, et le seul qui ne tourne pas chez GitHub : il s'exécute sur +un runner auto-hébergé installé sur la VM ENI, label `eni-g3`, parce que les runners hébergés ne +joignent pas une adresse privée d'école. Le runner se connecte en sortie vers GitHub, aucun port +entrant n'est ouvert. + +| Événement | Environnement GitHub | Dossier sur la VM | Garde | +|---|---|---|---| +| `push` sur `dev` | `rec` | `/srv/enervision/rec` | aucune : la recette suit `dev` | +| `push` sur `main` | `prod` | `/srv/enervision/prod` | approbation d'un relecteur dans l'environnement `prod`, branche `main` seule autorisée | + +Le job aligne le clone sur la branche (`fetch`, `checkout`, `reset --hard`), lance +`make stack-up`, qui reconstruit les images et redémarre les conteneurs, puis attend jusqu'à trois +minutes que `/api/v1/health/ready` réponde derrière le proxy. Un groupe de concurrence par +branche, sans annulation, empêche deux déploiements simultanés du même environnement. + +Le job ne fait pas de `actions/checkout` dans son espace de travail, et c'est voulu : le dossier +de l'environnement est stable, hors du runner, parce que `.env`, certificats et volumes doivent +survivre d'un déploiement à l'autre. + +**Piège à connaître.** Un runner auto-hébergé sur un dépôt public exécute ce qu'un workflow lui +envoie, et une PR de fork peut réécrire un workflow. Trois parades, et les trois sont +nécessaires : `deploy.yml` ne se déclenche jamais sur `pull_request` ; le runner tourne sous un +utilisateur dédié membre du groupe `docker`, jamais root ; le dépôt exige une approbation pour +les workflows des PR externes (Settings, Actions, « Require approval for all outside +collaborators »). Les workflows de CI restent sur `ubuntu-latest`. + +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 deux environnements, ports et noms d'hôte, est +dans [10-infra.md](10-infra.md). + ## Ce qui bloque un merge | Gate | Où | Seuil | Effet d'un échec | @@ -180,18 +220,23 @@ les tests ne se merge pas. ## Secrets -Un seul secret est consommé par la CI : **`SONAR_TOKEN`**, porté par les dépôts GitHub Actions. +Un seul secret est consommé côté GitHub : **`SONAR_TOKEN`**, porté par les secrets du dépôt. Les identifiants de la base du job d'intégration sont des valeurs de test en clair dans le workflow, ce qui est volontaire : elles ne protègent rien, la base est créée et détruite avec le -run. Aucune clé de déploiement n'existe encore, puisqu'il n'y a pas de déploiement : le job de -déploiement est porté par l'issue #21, les secrets qu'il consommera et leur injection par -l'issue #22. +run. + +Le déploiement ne consomme **aucun secret GitHub** (issue #22). Les secrets de chaque +environnement, mots de passe PostgreSQL et Airflow, clés de signature, clé Fernet, vivent dans le +`.env` de son dossier sur la VM, en `600`, générés sur la machine par `scripts/provision-host.sh`. +Ils ne transitent ni par git ni par GitHub, et le runner, qui travaille dans ce dossier, n'a rien +à recevoir. Le revers : ils ne sont sauvegardés nulle part ailleurs. Un `.env` perdu se +régénère, ce qui invalide les sessions et les connexions chiffrées par Airflow. ## Ce qui manque, et pourquoi | Manque | Issue | Conséquence assumée | |---|---|---| -| Job de déploiement (CD) | #21 | La chaîne s'arrête au merge. Rien ne part vers une machine | +| Images publiées et promues par digest (GHCR) | aucune | Chaque environnement reconstruit ses images : la production n'exécute pas l'artefact validé en recette, mais un second build du même commit | | DAST (OWASP ZAP) | #41 | Aucune vérification sur l'application en fonctionnement, seulement sur le code et les dépendances | | Tests end to end | #46 | Les parcours utilisateur ne sont pas vérifiés en CI | | Tests de charge | #47 | Aucun garde-fou de performance | diff --git a/infra/proxy/README.md b/infra/proxy/README.md index 558e8b9..2576387 100644 --- a/infra/proxy/README.md +++ b/infra/proxy/README.md @@ -15,6 +15,12 @@ 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. + ## Routage | Chemin | Destination | Remarque | diff --git a/scripts/provision-host.sh b/scripts/provision-host.sh new file mode 100755 index 0000000..3765575 --- /dev/null +++ b/scripts/provision-host.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Pourquoi : la machine porte deux environnements, chacun un clone du dépôt, un `.env` et un +# projet Compose (ADR 0009). Ce script prépare la machine et les deux dossiers sans rien +# démarrer : construction des images et démarrage restent à l'opérateur, puis au runner GitHub. +# Rejouable : un dossier déjà cloné est réaligné sur sa branche, un `.env` existant n'est jamais +# réécrit, un certificat présent n'est jamais régénéré. + +set -euo pipefail + +DEPOT="${REPO_URL:-https://github.com/ineszang/ProjetPiscine_EnerVision.git}" +RACINE="${RACINE:-/srv/enervision}" +ADRESSE="${PUBLIC_IP:-$(hostname -I | awk '{print $1}')}" +PROPRIETAIRE="${PROPRIETAIRE:-${SUDO_USER:-}}" +COMPOSE_MINIMALE="2.24.4" + +erreur() { echo "erreur : $*" >&2; exit 1; } +secret() { openssl rand -base64 48 | tr -d '/+=\n' | cut -c1-48; } +# Clé Fernet : 32 octets en base64 urlsafe, padding compris. +fernet() { openssl rand -base64 32 | tr '+/' '-_'; } + +verifier_outils() { + for outil in git make openssl curl; do + command -v "$outil" >/dev/null || erreur "$outil absent (apt-get install $outil)" + done + command -v docker >/dev/null || erreur "Docker absent : https://docs.docker.com/engine/install/debian/" + docker info >/dev/null 2>&1 || erreur "le démon Docker ne répond pas, ou l'utilisateur n'est pas dans le groupe docker" + local version + version="$(docker compose version --short 2>/dev/null || true)" + [[ -n "$version" ]] || erreur "plugin docker compose absent (paquet docker-compose-plugin)" + [[ "$(printf '%s\n%s\n' "$COMPOSE_MINIMALE" "${version#v}" | sort -V | head -1)" == "$COMPOSE_MINIMALE" ]] \ + || erreur "docker compose $version trop ancien : $COMPOSE_MINIMALE requis pour !override et !reset" + curl -fsSI --max-time 10 https://github.com >/dev/null || erreur "pas de sortie HTTPS vers github.com" + echo "docker compose $version, sortie Internet : ok" +} + +preparer() { + local env="$1" branche="$2" hote="$3" origine="$4" + local port_https="$5" port_http="$6" port_pg="$7" port_mailpit="$8" port_airflow="$9" + local dossier="$RACINE/$env" + + if [[ -d "$dossier/.git" ]]; then + git -C "$dossier" fetch --quiet origin "$branche" + git -C "$dossier" checkout --quiet "$branche" + git -C "$dossier" reset --quiet --hard "origin/$branche" + else + git clone --quiet --branch "$branche" "$DEPOT" "$dossier" + fi + + if [[ ! -f "$dossier/.env" ]]; then + sed -e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(secret)|" \ + -e "s|^POSTGRES_PORT=.*|POSTGRES_PORT=$port_pg|" \ + -e "s|^APP_SECRET_KEY=.*|APP_SECRET_KEY=$(secret)|" \ + -e "s|^MAILPIT_UI_PORT=.*|MAILPIT_UI_PORT=$port_mailpit|" \ + -e "s|^AIRFLOW_PORT=.*|AIRFLOW_PORT=$port_airflow|" \ + -e "s|^AIRFLOW_FERNET_KEY=.*|AIRFLOW_FERNET_KEY=$(fernet)|" \ + -e "s|^AIRFLOW_WEBSERVER_SECRET_KEY=.*|AIRFLOW_WEBSERVER_SECRET_KEY=$(secret)|" \ + -e "s|^AIRFLOW_ADMIN_PASSWORD=.*|AIRFLOW_ADMIN_PASSWORD=$(secret | cut -c1-20)|" \ + -e "s|^AIRFLOW_APP_SECRET_KEY=.*|AIRFLOW_APP_SECRET_KEY=$(secret)|" \ + -e "s|^PUBLIC_HOST=.*|PUBLIC_HOST=$hote|" \ + -e "s|^PUBLIC_ORIGIN=.*|PUBLIC_ORIGIN=$origine|" \ + -e "s|^COMPOSE_PROJECT_NAME=.*|COMPOSE_PROJECT_NAME=enervision-$env|" \ + -e "s|^PROXY_HTTP_PORT=.*|PROXY_HTTP_PORT=$port_http|" \ + -e "s|^PROXY_HTTPS_PORT=.*|PROXY_HTTPS_PORT=$port_https|" \ + "$dossier/.env.example" > "$dossier/.env" + # Branche antérieure à l'ADR 0009 : ces clés manquent alors dans .env.example. + for cle in "COMPOSE_PROJECT_NAME=enervision-$env" "PUBLIC_ORIGIN=$origine" \ + "PROXY_HTTP_PORT=$port_http" "PROXY_HTTPS_PORT=$port_https"; do + grep -q "^${cle%%=*}=" "$dossier/.env" || echo "$cle" >> "$dossier/.env" + done + chmod 600 "$dossier/.env" + echo "$env : .env généré. Reste à renseigner APP_MOCK_API_USERNAME et APP_MOCK_API_PASSWORD." + fi + + if [[ ! -f "$dossier/infra/proxy/tls/fullchain.pem" ]]; then + (cd "$dossier" && PUBLIC_HOST="$hote" PUBLIC_IP="$ADRESSE" ./scripts/tls-selfsigned.sh) + fi + echo "$env : $dossier sur $branche, $origine" +} + +verifier_outils +mkdir -p "$RACINE" + +# env branche hôte origine https http pg mailpit airflow +preparer prod main enervision.local https://enervision.local 443 80 5433 8025 8080 +preparer rec dev rec.enervision.local https://rec.enervision.local:8443 8443 127.0.0.1:8081 5434 8026 8082 + +if [[ -n "$PROPRIETAIRE" && "$(id -u)" -eq 0 ]]; then + chown -R "$PROPRIETAIRE" "$RACINE" +fi + +cat < Date: Mon, 21 Sep 2026 15:58:03 +0200 Subject: [PATCH 2/3] =?UTF-8?q?fix(infra,ci):=20le=20d=C3=A9ploiement=20mi?= =?UTF-8?q?gre=20la=20base,=20et=20la=20doc=20cesse=20de=20dire=20d=C3=A9p?= =?UTF-8?q?loy=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `make stack-up` enchaîne `alembic upgrade head` dans le conteneur backend. Rien ne migrait la base sur le chemin de déploiement, et `/api/v1/health/ready`, qui ne teste que la connexion et l'extension TimescaleDB, aurait laissé passer un déploiement vert sur une base sans schéma applicatif. deploy.yml borne le job à 30 minutes et sort les journaux du backend et du proxy quand la sonde échoue. provision-host.sh rappelle la création du premier administrateur, et la propriété de /srv/enervision sans laquelle le runner ne peut ni manipuler les clones ni lire un `.env` en 600. Les statuts de livraison continue repassent à `En cours` : le code est écrit, la machine n'est pas provisionnée, le runner n'est pas enregistré, rien n'a été déployé. À basculer sur `Fait` au premier déploiement vert. Décompte des jobs corrigé, 18 et non 17. Refs #21, #22 --- .github/workflows/deploy.yml | 5 ++++- Makefile | 5 ++++- docs/architecture/00-vue-ensemble.md | 2 +- docs/architecture/10-infra.md | 7 ++++--- docs/architecture/50-cicd.md | 31 ++++++++++++++++++---------- scripts/provision-host.sh | 9 ++++++-- 6 files changed, 40 insertions(+), 19 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f1f58d0..4055e9c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -21,6 +21,7 @@ concurrency: jobs: deploy: runs-on: [self-hosted, linux, eni-g3] + timeout-minutes: 30 environment: name: ${{ github.ref_name == 'main' && 'prod' || 'rec' }} url: ${{ github.ref_name == 'main' && 'https://enervision.local' || 'https://rec.enervision.local:8443' }} @@ -50,5 +51,7 @@ jobs: sleep 5 done echo "L'API ne répond pas après 3 minutes" >&2 - cd "/srv/enervision/${ENVIRONNEMENT}" && docker compose ps + cd "/srv/enervision/${ENVIRONNEMENT}" + docker compose ps + docker compose logs --tail=50 backend proxy exit 1 diff --git a/Makefile b/Makefile index 9d4beb4..7543607 100644 --- a/Makefile +++ b/Makefile @@ -124,12 +124,15 @@ docker-build: ## Construit l'image du backend tls-selfsigned: ## Génère le certificat de démonstration. PUBLIC_HOST=..., FORCE=1 pour écraser ./scripts/tls-selfsigned.sh $(if $(FORCE),--force,) -stack-up: ## Démarre la stack complète derrière le reverse proxy (80/443). PUBLIC_HOST=... au besoin +# Piège : l'image backend ne migre pas au démarrage, et `/health/ready` ne teste que la connexion +# et l'extension. Sans `alembic upgrade head`, la stack démarre verte sur une base sans schéma. +stack-up: ## Démarre la stack derrière le reverse proxy, puis migre la base. PUBLIC_HOST=... au besoin @test -f infra/proxy/tls/fullchain.pem \ || { echo "Aucun certificat dans infra/proxy/tls. Lancer d'abord make tls-selfsigned"; exit 1; } @openssl x509 -in infra/proxy/tls/fullchain.pem -noout -checkhost "$(PUBLIC_HOST)" >/dev/null \ || { echo "Le certificat ne couvre pas $(PUBLIC_HOST). Relancer make tls-selfsigned PUBLIC_HOST=$(PUBLIC_HOST) FORCE=1"; exit 1; } $(COMPOSE_PROD) up -d --build + $(COMPOSE_PROD) exec -T backend alembic upgrade head stack-down: ## Arrête la stack complète en conservant les données $(COMPOSE_PROD) stop diff --git a/docs/architecture/00-vue-ensemble.md b/docs/architecture/00-vue-ensemble.md index 558a496..619f671 100644 --- a/docs/architecture/00-vue-ensemble.md +++ b/docs/architecture/00-vue-ensemble.md @@ -89,7 +89,7 @@ collecteur ne vient le lire. | 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)). Module d'installation k3s jamais appliqué, aucune ressource Kubernetes déclarée | | Monitoring | Prometheus, Grafana, Alertmanager | `monitoring` | `Cible` | Rien, hors le `/metrics` exposé par l'API | | ETL | Apache Airflow | `etl/airflow` | `En cours` | Webserver + scheduler (LocalExecutor) tournent via docker-compose, base de métadonnées Postgres dédiée. Trois DAGs en sous-processus `uv run` : `ml_train` manuel et `ml_score` `@hourly` pour le pipeline ML (issue #115), `alertes` à `15 * * * *` pour la détection et les recommandations (issue #116, [ADR 0008](../adr/0008-airflow-execute-le-code-du-backend.md)). L'ingestion (issues #15/#16) n'a pas encore de DAG | -| CI/CD | GitHub Actions | `.github/workflows` | `Fait` | 6 workflows, 17 jobs : 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. Déploiement continu vers la VM ENI par `deploy.yml` et un runner auto-hébergé : `dev` en recette, `main` en production après approbation ([ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)). Détail dans [50-cicd.md](50-cicd.md) | +| CI/CD | GitHub Actions | `.github/workflows` | `En cours` | 6 workflows, 18 jobs : 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. Déploiement continu vers la VM ENI écrit par `deploy.yml`, `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é : la machine n'est pas provisionnée et le runner n'y est pas enregistré. Détail dans [50-cicd.md](50-cicd.md) | ## Flux bout en bout diff --git a/docs/architecture/10-infra.md b/docs/architecture/10-infra.md index 05bfb4b..545af7a 100644 --- a/docs/architecture/10-infra.md +++ b/docs/architecture/10-infra.md @@ -7,7 +7,7 @@ quel contexte, quelles décisions sont arrêtées, et ce qui manque encore entre |---|---|---| | 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 | `Fait` | +| Deux projets Compose sur la VM ENI, recette et production | Déploiement continu depuis GitHub | `En cours` | | k3s single-node | Cible à terme | `En cours` | ## Poste de développement @@ -172,8 +172,9 @@ Deux conséquences se propagent jusqu'à l'application, et elles ne se devinent ### Deux environnements sur la même machine -Statut : `Fait`. Décision et motifs dans l'[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md). -La VM `eadl-2025-nantes-g3` porte la recette et la production, chacune dans son clone du dépôt, +Statut : `En cours`, la machine n'étant pas encore provisionnée. Décision et motifs dans +l'[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md). +La VM `eadl-2025-nantes-g3` portera la recette et la production, chacune dans son clone du dépôt, son `.env` et son projet Compose. Le nom de projet préfixe volumes, réseau et conteneurs : rien n'est partagé. `scripts/provision-host.sh` prépare les deux dossiers, génère les secrets et les certificats, et ne démarre rien. diff --git a/docs/architecture/50-cicd.md b/docs/architecture/50-cicd.md index 07f63d6..d608b32 100644 --- a/docs/architecture/50-cicd.md +++ b/docs/architecture/50-cicd.md @@ -6,13 +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 | `Fait` | +| Livraison continue | Déployer chaque branche d'intégration sur son environnement de la VM ENI | `En cours` | -Le **D** de CI/CD existe depuis le 21/09 : `deploy.yml` déploie `dev` en recette et `main` en +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)). 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. +[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md)). Il n'a encore rien +déployé : la machine n'est pas provisionnée et le runner n'y est pas enregistré. Statut à +basculer sur `Fait` au premier déploiement vert. 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. ## Vue d'ensemble @@ -105,9 +107,12 @@ entrant n'est ouvert. | `push` sur `main` | `prod` | `/srv/enervision/prod` | approbation d'un relecteur dans l'environnement `prod`, branche `main` seule autorisée | Le job aligne le clone sur la branche (`fetch`, `checkout`, `reset --hard`), lance -`make stack-up`, qui reconstruit les images et redémarre les conteneurs, puis attend jusqu'à trois -minutes que `/api/v1/health/ready` réponde derrière le proxy. Un groupe de concurrence par -branche, sans annulation, empêche deux déploiements simultanés du même environnement. +`make stack-up`, qui reconstruit les images, redémarre les conteneurs puis applique les +migrations Alembic dans le conteneur backend, et attend jusqu'à trois minutes que +`/api/v1/health/ready` réponde derrière le proxy. Cette sonde ne vérifie que la connexion à la +base et la présence de TimescaleDB : sans la migration, le déploiement serait vert sur une base +sans schéma, et c'est pourquoi `make stack-up` la porte. Un groupe de concurrence par branche, +sans annulation, empêche deux déploiements simultanés du même environnement. Le job ne fait pas de `actions/checkout` dans son espace de travail, et c'est voulu : le dossier de l'environnement est stable, hors du runner, parce que `.env`, certificats et volumes doivent @@ -116,9 +121,13 @@ survivre d'un déploiement à l'autre. **Piège à connaître.** Un runner auto-hébergé sur un dépôt public exécute ce qu'un workflow lui envoie, et une PR de fork peut réécrire un workflow. Trois parades, et les trois sont nécessaires : `deploy.yml` ne se déclenche jamais sur `pull_request` ; le runner tourne sous un -utilisateur dédié membre du groupe `docker`, jamais root ; le dépôt exige une approbation pour -les workflows des PR externes (Settings, Actions, « Require approval for all outside -collaborators »). Les workflows de CI restent sur `ubuntu-latest`. +utilisateur dédié membre du groupe `docker`, jamais root ; le dépôt doit exiger une approbation +pour les workflows des PR externes (Settings, Actions, « Require approval for all outside +collaborators »), ce qui reste à activer. Les workflows de CI restent sur `ubuntu-latest`. + +Cet utilisateur dédié doit posséder `/srv/enervision` : sinon git refuse les deux 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 diff --git a/scripts/provision-host.sh b/scripts/provision-host.sh index 3765575..601969c 100755 --- a/scripts/provision-host.sh +++ b/scripts/provision-host.sh @@ -90,7 +90,12 @@ fi cat < +Le runner GitHub Actions (label eni-g3) rejouera le déploiement à chaque push sur dev et main. +L'installer sous le propriétaire de $RACINE, sinon git refuse ces dépôts et le .env en 600 lui +échappe : relancer au besoin ce script avec PROPRIETAIRE=. Depuis un poste : ajouter « $ADRESSE enervision.local rec.enervision.local » à /etc/hosts. FIN From ebf6476fb6fd8fb769977f8f01f66f868b790fe3 Mon Sep 17 00:00:00 2001 From: Johan LEROY Date: Tue, 22 Sep 2026 08:22:07 +0200 Subject: [PATCH 3/3] docs(infra): rappelle que data/raw n'est pas dans git au provisionnement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le DAG `historical_import` arrivé par #138 lit `./data/raw`, monté en lecture seule dans le scheduler. Le dossier est vide dans un clone : sans dépôt manuel des fichiers, le DAG n'a rien à charger sur la VM. --- scripts/provision-host.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/provision-host.sh b/scripts/provision-host.sh index 601969c..0f3d4e1 100755 --- a/scripts/provision-host.sh +++ b/scripts/provision-host.sh @@ -97,5 +97,7 @@ Premier administrateur, stack démarrée, dans chaque dossier : Le runner GitHub Actions (label eni-g3) rejouera le déploiement à chaque push sur dev et main. L'installer sous le propriétaire de $RACINE, sinon git refuse ces dépôts et le .env en 600 lui échappe : relancer au besoin ce script avec PROPRIETAIRE=. +Données historiques : git ne porte pas data/raw, déposer les fichiers dans chaque dossier avant +de déclencher le DAG historical_import. Depuis un poste : ajouter « $ADRESSE enervision.local rec.enervision.local » à /etc/hosts. FIN