From 6b3908d3216def7909fdf3af60cc6dd463f4c2ea Mon Sep 17 00:00:00 2001 From: ineszang <163989672+ineszang@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:34:33 +0200 Subject: [PATCH] Add files via upload --- .../0011-enervision-procedure-deploiement.md | 162 ++++++++++++++++++ ...-enervision-deploiement-rec-prod-vm-eni.md | 120 +++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 docs/adr/0011-enervision-procedure-deploiement.md create mode 100644 docs/adr/0012-enervision-deploiement-rec-prod-vm-eni.md diff --git a/docs/adr/0011-enervision-procedure-deploiement.md b/docs/adr/0011-enervision-procedure-deploiement.md new file mode 100644 index 0000000..fb74241 --- /dev/null +++ b/docs/adr/0011-enervision-procedure-deploiement.md @@ -0,0 +1,162 @@ +# 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). + +| | 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` | +| Postgres / Mailpit / Airflow (locaux) | `5434` / `8026` / `8082` | `5433` / `8025` / `8080` | + +## 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`. + 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. +3. **Jeton d'enregistrement du runner** : Settings > Actions > Runners > New self-hosted runner. + Valable 1 h, une seule inscription, créé par un administrateur du dépôt (ineszang). +4. **`main` est en retard de 64 commits** et ne porte ni `deploy.yml`, ni `provision-host.sh`, ni + le Terraform, ni l'overlay paramétré (ports et `PUBLIC_ORIGIN` en dur). Tant que `dev` n'est pas + remonté dans `main`, seule la recette est déployable : le clone `prod` sera préparé mais son + `make stack-up` publierait 80/443 sans les variables, et aucun push sur `main` ne déclencherait + de déploiement (le workflow n'y existe pas). **Remonter `dev` → `main` avant de toucher à prod.** + +## 1. Provisionner la machine (depuis le poste) + +```bash +cd infra/terraform/environments/vm-eni +cp terraform.tfvars.example terraform.tfvars +terraform init +terraform apply +``` + +`terraform.tfvars`, ignoré par git, trois valeurs à renseigner : + +```hcl +proprietaire = "enervision" # doit exister sur la VM +runner_version = "2.330.0" # épingler depuis github.com/actions/runner/releases +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"`, +`ssh_private_key_path = "~/.ssh/id_ed25519"`, `racine = "/srv/enervision"`, +`runner_labels = "eni-g3"` (ciblé par `deploy.yml`), `runner_dossier = "/opt/actions-runner"`. + +L'apply fait trois choses, dans cet ordre : Docker + plugin Compose et `usermod -aG docker`, +puis `scripts/provision-host.sh`, puis l'installation et l'enregistrement du runner en service. +Il ne construit aucune image et ne démarre aucun conteneur : un apply n'interrompt pas la stack. + +Rejouable : un clone existant est réaligné, un `.env` présent n'est **jamais** réécrit, un +certificat présent n'est jamais régénéré. Un nouvel apply de la ressource runner redemande un +jeton frais (il expire en 1 h). + +## 2. Variables d'environnement + +Un `.env` par dossier, en `600`, généré sur la machine depuis `.env.example`. **Aucun secret ne +passe par git ni par GitHub** : le runner n'en reçoit aucun (seul `SONAR_TOKEN` existe côté CI). + +**Générés automatiquement** : `POSTGRES_PASSWORD`, `APP_SECRET_KEY`, `AIRFLOW_FERNET_KEY`, +`AIRFLOW_API_SECRET_KEY`, `AIRFLOW_JWT_SECRET`, `AIRFLOW_ADMIN_PASSWORD`, `AIRFLOW_APP_SECRET_KEY`. + +**Fixés par environnement** : `COMPOSE_PROJECT_NAME`, `PUBLIC_HOST`, `PUBLIC_ORIGIN`, +`PROXY_HTTP_PORT`, `PROXY_HTTPS_PORT`, `POSTGRES_PORT`, `MAILPIT_UI_PORT`, `AIRFLOW_PORT`. + +**À renseigner à la main**, dans chaque `.env`, avant le premier démarrage : + +``` +APP_MOCK_API_USERNAME=... +APP_MOCK_API_PASSWORD=... +``` + +Garde-fou : le script refuse d'écrire un `.env` s'il reste un `change_me` hors `APP_MOCK_API_*` +(cas vécu d'une clé renommée en amont, `AIRFLOW_WEBSERVER_SECRET_KEY` sous Airflow 3). + +`APP_ENV=prod` et `APP_DEBUG=false` sont en dur dans l'overlay, pas dans le `.env` : la valeur +`local` du poste reprendrait le dessus et rouvrirait `/docs` sans cookie `__Secure-`. + +`TS_TUNE_MEMORY=2GB` et `TS_TUNE_NUM_CPUS=2` sont obligatoires : deux TimescaleDB sur 8 Go se +réserveraient 25 % de la RAM chacune. La montée à 32 Go est à demander. + +Certificats auto-signés générés par le script (`infra/proxy/tls/`), couvrant le nom d'hôte, +`localhost` et l'IP. Let's Encrypt (`make tls-acme`, `ACME_EMAIL`) reste hors d'atteinte sans +domaine public résolvable. + +## 3. Premier démarrage (manuel, une seule fois, sur la VM) + +```bash +cd /srv/enervision/rec && make stack-up # build + up + alembic upgrade head +cd /srv/enervision/prod && make stack-up # seulement après la remontée dev → main +``` + +`stack-up` refuse de démarrer si le certificat manque ou ne couvre pas `PUBLIC_HOST`, et applique +les migrations : sans elles la stack démarrerait verte sur une base sans schéma. + +Premier administrateur, stack démarrée, dans chaque dossier : + +```bash +docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backend \ + python -m app.cli create-admin --email +``` + +Données historiques : `data/raw` n'est pas dans git. Déposer les fichiers dans chaque dossier +avant de déclencher le DAG `historical_import`. + +## 4. Réglages GitHub (administrateur du dépôt) + +- Environnement `prod` : branche `main` seule autorisée, **approbation d'un relecteur** requise. +- Environnement `rec` : branche `dev` seule autorisée, sans approbation. +- Settings > Actions : **« Require approval for all outside collaborators »**. Un runner + auto-hébergé sur un dépôt public exécute ce qu'on lui envoie ; `deploy.yml` ne se déclenche + jamais sur `pull_request`, et le runner ne tourne jamais en root. + +## 5. Déploiement continu, ensuite + +Un push sur `dev` déploie la recette, un push sur `main` la production après approbation. +Le job (runner `eni-g3`) aligne le clone (`fetch`, `checkout`, `reset --hard`), lance +`make stack-up`, puis sonde `/api/v1/health/ready` derrière le proxy pendant 3 minutes ; en cas +d'échec il publie `ps` et les 50 dernières lignes de `backend` et `proxy`. Pas de `checkout` dans +l'espace du runner : `.env`, certificats et volumes doivent survivre d'un déploiement à l'autre. +Concurrence par branche, sans annulation. + +Déclenchement manuel possible : `workflow_dispatch`. + +## 6. Vérifier + +```bash +curl -k https://localhost:8443/api/v1/health/ready # recette, sur la VM +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 +``` + +Les deux noms sont obligatoires : le cookie `__Secure-ev_refresh` est posé par hôte et non par +port ; un seul nom déconnecterait la production à chaque connexion en recette. + +## Pièges à connaître + +- Compose **2.24.4 minimum** : l'overlay emploie `!override` et `!reset`, sans quoi l'API resterait + joignable en clair à côté du proxy. Le script le vérifie. +- Le runner doit tourner sous le propriétaire de `/srv/enervision` : sinon git refuse les clones + (propriété douteuse) et le `.env` en `600` lui échappe. Correctif : + `PROPRIETAIRE= bash scripts/provision-host.sh`. +- Chaque environnement reconstruit ses images à partir du même commit : la production n'exécute + pas l'artefact validé en recette, mais un second build. Le passage à GHCR lèvera cette limite. +- Un `.env` perdu se régénère, mais invalide les sessions et les connexions chiffrées par Airflow : + ils ne sont sauvegardés nulle part ailleurs. +- Retirer le runner se fait à la main, depuis les paramètres du dépôt : `terraform destroy` ne le + désinscrit pas. + +## Références dans le dépôt + +`docs/adr/0009-deux-environnements-compose-sur-la-vm-eni.md`, +`docs/adr/0010-terraform-provisionne-github-actions-deploie.md`, +`docs/architecture/50-cicd.md`, `docs/architecture/10-infra.md`, `infra/README.md`, +`scripts/provision-host.sh`, `.github/workflows/deploy.yml`, `docker-compose.prod.yml`. diff --git a/docs/adr/0012-enervision-deploiement-rec-prod-vm-eni.md b/docs/adr/0012-enervision-deploiement-rec-prod-vm-eni.md new file mode 100644 index 0000000..8d01d39 --- /dev/null +++ b/docs/adr/0012-enervision-deploiement-rec-prod-vm-eni.md @@ -0,0 +1,120 @@ +# 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 +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. + +## 1. La décision en une phrase + +**Deux projets Docker Compose sur la même VM, un par environnement, déployés par un runner GitHub +Actions installé sur la VM.** `dev` alimente la recette, `main` alimente la production. Terraform +reste ce qu'il est : le module k3s, cible à terme, non utilisé pour cette mise en ligne. + +| | Recette (`rec`) | Production (`prod`) | +|---|---|---| +| Branche | `dev` | `main` | +| Environnement GitHub | `rec` (créé ce midi) | `prod` (créé ce midi) | +| Dossier sur la VM | `/srv/enervision/rec` | `/srv/enervision/prod` | +| Projet Compose | `enervision-rec` | `enervision-prod` | +| URL | `https://rec.enervision.local:8443` | `https://enervision.local` | +| Proxy HTTPS | `8443` | `443` | +| Proxy HTTP (redirection) | `127.0.0.1:8081`, inutilisé | `80` | +| PostgreSQL, Mailpit, Airflow | `127.0.0.1` : `5434`, `8026`, `8082` | `127.0.0.1` : `5433`, `8025`, `8080` | +| Certificat | auto-signé, SAN `rec.enervision.local` | auto-signé, SAN `enervision.local` | +| Déclenchement | chaque push sur `dev` | push sur `main`, après approbation dans GitHub | + +Les deux noms d'hôte pointent sur la même IP. Deux lignes dans le `/etc/hosts` des postes de +l'équipe suffisent. Deux noms distincts sont indispensables : le cookie de rafraîchissement +`__Secure-ev_refresh` est posé par hôte, pas par port, et un seul nom ferait se déconnecter la +prod à chaque connexion sur la recette. + +## 2. Pourquoi c'est la solution la plus simple + +- **Tout existe déjà.** L'overlay `docker-compose.prod.yml`, le proxy Nginx TLS, les scripts de + certificat et `make stack-up` sont écrits et validés sur poste (PR #117, ADR 0007). Il ne + manque que quatre variables pour que deux instances cohabitent sur une machine. +- **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 + 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 + même code est déployé en recette, puis en production, sans troisième mécanisme. + +Ce qu'on écarte, et pourquoi : + +| Piste | Pourquoi pas cette semaine | +|---|---| +| k3s avec deux namespaces | Le cluster serait vide : aucun manifeste, aucun registre d'images, aucun stockage persistant. Trois jours de travail sans valeur visible au J10 | +| Terraform de `feat/deploy` (nginx système + copie de fichiers) | Revue postée sur l'issue #21 : huit points bloquants, `rec` et `prod` ne passent pas `terraform validate`. On abandonne cette voie | +| Azure ENI pour la prod | Deuxième infrastructure à provisionner, choix à justifier devant le jury (document 03), et rien n'est prêt côté Azure | +| Images publiées sur GHCR | Meilleure pratique, mais un registre de plus à authentifier sur la VM. Les images se construisent sur la VM, où le runner tourne déjà. À faire ensuite, issue à ouvrir | +| Let's Encrypt | Aucun domaine public ne résout vers la VM. Auto-signé assumé, chemin ACME déjà câblé | + +## 3. Ce qui change dans le dépôt (une PR vers `dev`) + +| Fichier | Changement | Raison | +|---|---|---| +| `apps/frontend/Dockerfile` | `FROM nginx:1.28-alpine` à la place de `dhi.io/nginx:...` | Le registre Docker Hardened Images demande une authentification. L'image frontend n'a jamais été construite, sur aucun poste : c'est le premier point où `make stack-up` échouerait sur la VM | +| `docker-compose.prod.yml` | Ports du proxy en variables `PROXY_HTTP_PORT` et `PROXY_HTTPS_PORT`. Origine publique `PUBLIC_ORIGIN` pour CORS et le lien de réinitialisation. `TS_TUNE_MEMORY` sur la base | Deux proxys ne peuvent pas publier 80 et 443. L'origine de la recette porte un port. Deux TimescaleDB sur 8 Go se réserveraient chacune 2 Go sans réglage | +| `.env.example` | `COMPOSE_PROJECT_NAME`, les variables ci-dessus, ports de la recette en commentaire | Le `.env` de chaque dossier est la seule différence entre les deux environnements | +| `.github/workflows/deploy.yml` | Nouveau. `on: push` sur `dev` et `main`, `runs-on: [self-hosted, eni-g3]`, `environment: rec` ou `prod`, puis `git reset --hard origin/` et `make stack-up` dans le dossier de l'environnement | Le D de CI/CD, issue #21 | +| `scripts/provision-host.sh` | Nouveau. Vérifie Docker et Compose 2.24.4 ou plus, crée `/srv/enervision/{rec,prod}`, clone les deux branches | Rejouable, et réutilisable par Terraform plus tard | +| `docs/adr/0009-...md`, `10-infra.md`, `50-cicd.md`, `infra/proxy/README.md` | Décision, vue infra, vue CI/CD, tableau des ports | Règle du dépôt : la vue change dans la même PR que le composant | + +Ce qui ne change pas : `docker-compose.yml`, la configuration Nginx, `infra/terraform`. + +## 4. Déroulé de l'après-midi + +| # | 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 | +| 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 | +| 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 | + +Contrôle en fin de chaîne, depuis la VM : + +```bash +curl -k https://localhost/api/v1/health/ready # prod +curl -k https://localhost:8443/api/v1/health/ready # rec +docker compose -p enervision-prod ps +docker compose -p enervision-rec ps +``` + +## 5. Ce qui peut faire échouer la journée, et la parade + +| Risque | Parade | +|---|---| +| **8 Go de RAM pour deux stacks complètes** (deux Airflow, deux TimescaleDB, deux API) | Demander dès maintenant le passage à 32 Go, prévu par les consignes. En attendant : `TS_TUNE_MEMORY=2GB` et deux workers gunicorn pour Airflow. Si la RAM ne suit pas, démarrer la recette sans Airflow (`docker compose up -d --scale airflow-webserver=0 --scale airflow-scheduler=0`) | +| **Compose trop ancien sur la VM** (les marqueurs `!override` et `!reset` exigent 2.24.4) | `docker compose version` en premier. Sinon installer le paquet `docker-compose-plugin` depuis le dépôt Docker | +| **Pas de sortie Internet depuis la VM** | `curl -sI https://github.com` et `docker pull hello-world` avant tout. Sans sortie, ni construction d'image ni runner : déploiement manuel par `scp` d'images, plan B lourd | +| **Runner auto-hébergé sur un dépôt public** | Le workflow de déploiement ne s'exécute que sur `push` vers `dev` et `main`, jamais sur `pull_request`. Réglage d'approbation des PR externes (étape 1). Runner sous un utilisateur dédié, jamais root | +| **Premier démarrage avec un volume `pgdata` vide** | C'est le cas nominal sur la VM : `db/init` crée les bases `enervision`, `enervision_test` et `airflow`. Ne pas restaurer un volume de poste | +| **Le jury accepte mal un certificat auto-signé** | Dire pourquoi avant qu'on le demande : aucun DNS public, ACME câblé et documenté, ADR 0007. Un clic « continuer » dans le navigateur | +| **Conflit avec `feat/deploy`** (ineszang y a mergé `dev` à 14h06) | Partager ce document avant de pousser. La PR remplace `feat/deploy`, elle ne s'y ajoute pas | + +## 6. Ce que ça donne pour la grille + +- **EC03, CI/CD** : la chaîne ne s'arrête plus au merge. Deux environnements, déploiement + automatique en recette, promotion approuvée en production, journal des déploiements dans + l'onglet Environments de GitHub. +- **EC04, cloud et sécurisation** : une application déployée et fonctionnelle, une seule surface + exposée par environnement, secrets hors de git et hors de GitHub, base et Airflow joignables + uniquement par tunnel SSH. +- **Dossier EC01** : le choix 16 (runner auto-hébergé, déploiement automatique) passe de « non + fait » à « tenu ». Le choix 12 (Ansible) reste non fait, et la réponse est prête : le + durcissement de la machine n'est pas automatisé, le script de provisionnement en est la + première brique, Terraform pourra l'appeler. + +## 7. Après vendredi, si on continue + +Dans l'ordre de valeur : images construites une fois en CI et publiées sur GHCR, puis déployées +par digest (vraie promotion d'artefact). Racine Terraform `environments/eni-g3` qui provisionne +la machine et le runner à partir du script. Sauvegarde de `pgdata` par `pg_dump` planifié. +Monitoring (issue #26). Et seulement ensuite la bascule k3s, si elle garde un sens.