Merge pull request #156 from ineszang/feat/domaine-duckdns-tls

feat(deploy): URL sans port et certificats Let's Encrypt sur la VM ENI
This commit is contained in:
Johan LEROY
2026-09-23 12:01:41 +02:00
committed by GitHub
17 changed files with 411 additions and 75 deletions
+3
View File
@@ -69,6 +69,9 @@ PUBLIC_ORIGIN=
# l'extérieur. Décaler aussi POSTGRES_PORT, MAILPIT_UI_PORT et AIRFLOW_PORT (5434, 8026, 8082). # l'extérieur. Décaler aussi POSTGRES_PORT, MAILPIT_UI_PORT et AIRFLOW_PORT (5434, 8026, 8082).
PROXY_HTTP_PORT= PROXY_HTTP_PORT=
PROXY_HTTPS_PORT= PROXY_HTTPS_PORT=
# Écouteur PROXY protocol du proxy, que seul le frontal de la VM joint (infra/front, ADR 0018).
# Vide : port aléatoire sur 127.0.0.1. VM : 127.0.0.1:10444 en prod, 8444 en recette, 9444 en dev.
PROXY_FRONT_PORT=
# Réglages mémoire de la stack déployée. Sans eux, timescaledb-tune réserve 25 % de la RAM de la # 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. L'api-server Airflow 3 n'a rien à régler ici : son # machine à chaque base au premier démarrage. L'api-server Airflow 3 n'a rien à régler ici : son
# nombre de workers vaut 1 par défaut, contre 4 pour le webserver d'Airflow 2. # nombre de workers vaut 1 par défaut, contre 4 pour le webserver d'Airflow 2.
+1
View File
@@ -81,6 +81,7 @@ jobs:
compose: compose:
- "docker-compose*.yml" - "docker-compose*.yml"
- ".env.example" - ".env.example"
- "infra/front/**"
- "monitoring/**" - "monitoring/**"
- ".github/workflows/infra.yml" - ".github/workflows/infra.yml"
workflows: workflows:
+5 -2
View File
@@ -28,10 +28,10 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
environment: environment:
name: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }} name: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }}
url: ${{ github.ref_name == 'main' && 'https://enervision.local' || github.ref_name == 'dev' && 'https://rec.enervision.local:8443' || 'https://dev.enervision.local:9443' }} url: ${{ github.ref_name == 'main' && 'https://enervision-g3.dynv6.net' || github.ref_name == 'dev' && 'https://rec.enervision-g3.dynv6.net' || 'https://dev.enervision-g3.dynv6.net' }}
env: env:
ENVIRONNEMENT: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }} ENVIRONNEMENT: ${{ github.ref_name == 'main' && 'prod' || github.ref_name == 'dev' && 'rec' || 'dev' }}
PORT_HTTPS: ${{ github.ref_name == 'main' && '443' || github.ref_name == 'dev' && '8443' || '9443' }} PORT_HTTPS: ${{ github.ref_name == 'main' && '10443' || github.ref_name == 'dev' && '8443' || '9443' }}
steps: steps:
# Un seul step : le verrou tombe avec le shell qui l'a posé. # Un seul step : le verrou tombe avec le shell qui l'a posé.
- name: Déploie le commit testé, sans jamais reculer - name: Déploie le commit testé, sans jamais reculer
@@ -54,7 +54,10 @@ jobs:
echo "::endgroup::" echo "::endgroup::"
echo "::group::Reconstruit et redémarre la stack" echo "::group::Reconstruit et redémarre la stack"
# Un `.env` pas encore réaligné par provision-host.sh porte encore un nom en `.local`.
if [ -r ../dns.token ] && ! grep -q '^PUBLIC_HOST=.*\.local$' .env; then make tls-dns01; fi
make stack-up make stack-up
if [ "${ENVIRONNEMENT}" = prod ]; then make front-up; fi
echo "::endgroup::" echo "::endgroup::"
echo "::group::Attend que l'API réponde derrière le proxy" echo "::group::Attend que l'API réponde derrière le proxy"
+5
View File
@@ -73,6 +73,11 @@ jobs:
- name: Valide la stack déployée, profils compris - name: Valide la stack déployée, profils compris
run: docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile acme --profile monitoring --profile load config --quiet run: docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile acme --profile monitoring --profile load config --quiet
- name: Valide le frontal SNI de la VM
run: |
docker compose -f infra/front/compose.yml config --quiet
docker run --rm -v "$PWD/infra/front/nginx.conf:/etc/nginx/nginx.conf:ro" nginx:1.31-alpine nginx -t
# Mêmes commandes que `make monitoring-check` : images et montages viennent du fichier Compose. # Mêmes commandes que `make monitoring-check` : images et montages viennent du fichier Compose.
- name: Valide la configuration de Prometheus et ses règles - name: Valide la configuration de Prometheus et ses règles
run: docker compose --profile monitoring run --rm --no-deps --entrypoint promtool prometheus check config /etc/prometheus/prometheus.yml run: docker compose --profile monitoring run --rm --no-deps --entrypoint promtool prometheus check config /etc/prometheus/prometheus.yml
+1
View File
@@ -75,6 +75,7 @@ etl/airflow/tests/.airflow_home/
# TLS : certificats du reverse proxy, générés par script ou par certbot # TLS : certificats du reverse proxy, générés par script ou par certbot
infra/proxy/tls/*.pem infra/proxy/tls/*.pem
infra/proxy/acme/
# IDE et OS # IDE et OS
.idea/ .idea/
+28 -1
View File
@@ -73,7 +73,7 @@ DEMO_NOW ?= 2024-12-31T00:00:00Z
migrate migrate-test bootstrap-admin services-up demo-data demo-data-force \ migrate migrate-test bootstrap-admin services-up demo-data demo-data-force \
ml-lint ml-typecheck ml-test ml-check ml-train ml-score mlflow-up detect-alerts recommendations \ ml-lint ml-typecheck ml-test ml-check ml-train ml-score mlflow-up detect-alerts recommendations \
airflow-lint airflow-test airflow-check airflow-up airflow-down airflow-logs \ airflow-lint airflow-test airflow-check airflow-up airflow-down airflow-logs \
tls-selfsigned tls-acme tls-renew stack-up stack-down stack-logs \ tls-selfsigned tls-acme tls-renew tls-dns01 front-up stack-up stack-down stack-logs \
e2e-install e2e-prepare e2e load-smoke load-test load-stress load-limits \ e2e-install e2e-prepare e2e load-smoke load-test load-stress load-limits \
db-ensure-supervision monitoring-up monitoring-down monitoring-logs monitoring-check db-ensure-supervision monitoring-up monitoring-down monitoring-logs monitoring-check
@@ -235,6 +235,33 @@ tls-renew: ## Renouvelle les certificats Let's Encrypt et recharge le proxy
$(COMPOSE_PROD) --profile acme run --rm certbot renew --deploy-hook /deploy-hook.sh $(COMPOSE_PROD) --profile acme run --rm certbot renew --deploy-hook /deploy-hook.sh
$(COMPOSE_PROD) exec proxy nginx -s reload $(COMPOSE_PROD) exec proxy nginx -s reload
# Pourquoi : la VM n'a qu'une IP privée, que Let's Encrypt ne joint pas ; le défi DNS-01 passe
# par l'API du fournisseur DNS, dynv6 par défaut (ADR 0018). Le jeton ne passe jamais par `argv`.
ACME_SH := neilpang/acme.sh:3.1.6
DNS01_API ?= dns_dynv6
DNS01_JETON_VAR ?= DYNV6_TOKEN
DNS01_JETON_FICHIER ?= $(abspath $(CURDIR)/../dns.token)
acme-sh = docker run --rm --user "$$(id -u):$$(id -g)" -e $(DNS01_JETON_VAR) -e AUTO_UPGRADE=0 \
-v "$(CURDIR)/infra/proxy/acme:/acme.sh" -v "$(CURDIR)/infra/proxy/tls:/tls" $(ACME_SH)
# acme.sh sort en 2 quand le certificat n'est pas encore à renouveler, et recopie le jeton dans
# acme/account.conf : d'où le chmod, qui le soustrait aux autres comptes de la machine.
tls-dns01: ## Certificat Let's Encrypt par DNS-01, renouvelé seulement à échéance. Jeton : ../dns.token
@case "$(PUBLIC_HOST)" in *.local | localhost) echo "PUBLIC_HOST=$(PUBLIC_HOST) n'est pas un nom public"; exit 1 ;; esac
@test -r "$(DNS01_JETON_FICHIER)" || { echo "Jeton DNS illisible : $(DNS01_JETON_FICHIER)"; exit 1; }
@mkdir -p infra/proxy/acme && chmod 700 infra/proxy/acme
@$(DNS01_JETON_VAR)="$$(tr -d '[:space:]' < "$(DNS01_JETON_FICHIER)")"; export $(DNS01_JETON_VAR); \
$(acme-sh) --issue --server letsencrypt --dns $(DNS01_API) -d "$(PUBLIC_HOST)"; \
code=$$?; chmod -R go-rwx infra/proxy/acme; [ $$code -eq 0 ] || [ $$code -eq 2 ] || exit $$code
@$(acme-sh) --install-cert --ecc -d "$(PUBLIC_HOST)" \
--fullchain-file /tls/fullchain.pem --key-file /tls/privkey.pem
@$(COMPOSE_PROD) exec -T proxy nginx -s reload 2>/dev/null \
|| echo "Proxy arrêté : il lira le certificat à son démarrage"
front-up: ## Démarre ou recharge le frontal SNI de la VM, sur les ports 80 et 443 de l'hôte
docker compose -f infra/front/compose.yml up -d
docker compose -f infra/front/compose.yml exec -T front nginx -s reload
e2e-install: ## Installe Playwright et Chromium pour les tests de bout en bout e2e-install: ## Installe Playwright et Chromium pour les tests de bout en bout
cd $(E2E) && npm ci && npx playwright install chromium cd $(E2E) && npm ci && npx playwright install chromium
+2
View File
@@ -58,6 +58,8 @@ services:
ports: ports:
- "${PROXY_HTTP_PORT:-80}:80" - "${PROXY_HTTP_PORT:-80}:80"
- "${PROXY_HTTPS_PORT:-443}:443" - "${PROXY_HTTPS_PORT:-443}:443"
# Vide : port aléatoire sur la boucle locale, pour que deux stacks sans frontal cohabitent.
- "${PROXY_FRONT_PORT:-127.0.0.1:}:4443"
volumes: volumes:
- ./infra/proxy/nginx.conf:/etc/nginx/nginx.conf:ro - ./infra/proxy/nginx.conf:/etc/nginx/nginx.conf:ro
- ./infra/proxy/conf.d:/etc/nginx/conf.d:ro - ./infra/proxy/conf.d:/etc/nginx/conf.d:ro
@@ -0,0 +1,89 @@
# 0018 - Noms publics, certificats Let's Encrypt par DNS-01 et frontal SNI sans port
- Statut : accepté
- Date : 2026-09-23
## Contexte
Les trois environnements de la VM ENI ([ADR 0009](0009-deux-environnements-compose-sur-la-vm-eni.md),
[ADR 0017](0017-environnement-dev-a-la-demande.md)) répondaient sur `enervision.local`,
`rec.enervision.local:8443` et `dev.enervision.local:9443`, avec des certificats auto-signés.
Chaque poste devait éditer son `/etc/hosts` et accepter trois avertissements du navigateur :
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
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
et API, depuis les postes comme depuis la VM.
## Décision
**Des noms publics qui visent l'IP privée.** `enervision-g3.dynv6.net`, zone gratuite de dynv6,
porte la prod, et deux enregistrements A portent `rec.` et `dev.`. `provision-host.sh` les publie
par l'API dynv6 : le DNS est décrit par le code comme le reste. Tout
poste du réseau de l'école les résout sans configuration ; hors de ce réseau, l'IP ne mène
nulle part.
**Des certificats Let's Encrypt par défi DNS-01.** Le défi passe par l'API dynv6, qui pose
l'enregistrement TXT : Let's Encrypt n'a jamais à joindre la VM. `make tls-dns01` (acme.sh
épinglé) le joue dans chaque stack ; il ne renouvelle qu'à échéance, d'où son rejeu à chaque
déploiement et chaque nuit par cron. Un certificat par environnement plutôt qu'un joker : chaque
stack garde le sien, et la clé de la prod n'est pas lisible depuis le clone de dev.
**Un frontal SNI sur 443, le seul composant exposé.** `infra/front`, un nginx sur le réseau de
l'hôte, lit le nom demandé dans le ClientHello et relaie le flux TLS intact vers la stack visée,
publiée sur la boucle locale. Il ne détient aucun certificat. Le port 80 y redirige vers
HTTPS. Les URL perdent leur port.
**Le PROXY protocol entre frontal et stacks.** Relayé tel quel, le flux arriverait avec l'IP du
frontal : `limit_req` et `get_client_ip()` compteraient tous les postes comme un seul, et un
utilisateur bloquerait la connexion de tous. Chaque proxy de stack reçoit donc le frontal sur un
écouteur dédié, 4443, qui exige l'en-tête PROXY protocol et en tire l'IP du client. Le 443 de
la stack reste sans PROXY protocol, pour les postes de développement et la sonde du déploiement.
**Le fournisseur est un paramètre.** `DNS01_API` et `DNS01_JETON_VAR` nomment le greffon acme.sh,
le jeton vit dans `dns.token` quel que soit le fournisseur : passer à un domaine acheté chez
Cloudflare ou OVH ne demande que ces deux variables et `domaine`, plus `publier_dns()`.
**`scripts/provision-host.sh` fait foi pour l'adressage et les secrets.** Un `.env` existant
garde ses secrets, reçoit ceux qui lui manquent et voit hôte, ports et profils réalignés sur le
tableau du script. C'est ce qui permet de migrer trois `.env` nés avant ce changement, et le
clone de la prod, en retard sur `main`, sans dépendre de son `.env.example`.
## Alternatives écartées
- **Garder `/etc/hosts` et l'auto-signé** : trois manipulations par poste et trois
avertissements, précisément ce qu'il fallait supprimer.
- **DuckDNS** : premier choix, inscription en un clic, mais bloqué par le filtrage de l'école :
sans son API, pas de défi DNS-01.
- **deSEC (`dedyn.io`)** : joignable et associatif, mais les inscriptions de nouveaux domaines
`dedyn.io` étaient fermées le 23/09 ; il reste le bon choix pour un domaine acheté.
- **nip.io ou sslip.io** : résolution sans compte, mais aucun moyen d'y obtenir un certificat.
- **Services à certificat joker public (traefik.me, local-ip.co)** : leur clé privée est publiée
par conception, n'importe qui peut usurper ces noms.
- **Tunnel vers Internet (Cloudflare Tunnel, Tailscale Funnel)** : accès depuis l'extérieur,
mais l'application serait exposée hors de l'école, décision refusée.
- **Autorité de certification interne (mkcert, step-ca)** : chaque poste devrait l'installer.
- **Terminaison TLS au frontal** : un seul endroit pour les certificats, mais les stacks
recevraient du HTTP clair que leur proxy redirige vers HTTPS, et en-têtes de sécurité comme
limitation de débit seraient à déplacer. Le relais SNI ne touche à rien de tout cela.
- **Domaine acheté** : plus présentable, mais un achat et un compte de plus pour un bénéfice nul
sur l'accès. Seule `DOMAINE` changerait.
## Conséquences
- L'objection de l'ADR 0009 à un proxy frontal, qui aurait dû joindre plusieurs réseaux Compose
aux services homonymes, tombe : le frontal ne joint que des ports de la boucle locale.
- Sans le frontal, plus rien n'est joignable sur la VM. `deploy.yml` le relance à chaque
déploiement de la prod, et son `restart: unless-stopped` le ramène après un redémarrage.
- Le jeton dynv6 vit dans `/srv/enervision/dns.token`, jamais dans git, GitHub ni le state
Terraform ; acme.sh en garde une copie dans `infra/proxy/acme/`, retirée à la lecture des
autres comptes. Qui le détient peut repointer les trois noms.
- dynv6 devient une dépendance : s'il tombe, les noms cessent de résoudre et les
renouvellements échouent. Les certificats valent 90 jours, la marge est large.
- Un filtrage de l'école qui viendrait à bloquer dynv6 arrêterait les renouvellements, pas les
noms : la résolution passe par les serveurs DNS de l'école, pas par le site.
- Les noms sont publics mais ne mènent qu'à une IP privée : ils révèlent l'existence de la VM,
pas son contenu.
+15 -8
View File
@@ -237,7 +237,8 @@ Deux conséquences se propagent jusqu'à l'application, et elles ne se devinent
Statut : `En cours`. Décision et motifs dans Statut : `En cours`. Décision et motifs dans
l'[ADR 0009](../adr/0009-deux-environnements-compose-sur-la-vm-eni.md), étendue à un troisième 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). 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).
La VM `eadl-2025-nantes-g3` porte le développement, la recette et la production, chacun dans son La VM `eadl-2025-nantes-g3` porte le développement, la recette et la production, chacun dans son
clone du dépôt, son `.env` et son projet Compose. Le nom de projet préfixe volumes, réseau et 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 trois dossiers, génère conteneurs : rien n'est partagé. `scripts/provision-host.sh` prépare les trois dossiers, génère
@@ -247,16 +248,21 @@ les secrets et les certificats, et ne démarre rien.
|---|---|---|---| |---|---|---|---|
| Branche, environnement GitHub | toute branche lancée à la main, `dev` | `dev`, `rec` | `main`, `prod` | | Branche, environnement GitHub | toute branche lancée à la main, `dev` | `dev`, `rec` | `main`, `prod` |
| Dossier, projet Compose | `/srv/enervision/dev`, `enervision-dev` | `/srv/enervision/rec`, `enervision-rec` | `/srv/enervision/prod`, `enervision-prod` | | Dossier, projet Compose | `/srv/enervision/dev`, `enervision-dev` | `/srv/enervision/rec`, `enervision-rec` | `/srv/enervision/prod`, `enervision-prod` |
| URL | `https://dev.enervision.local:9443` | `https://rec.enervision.local:8443` | `https://enervision.local` | | URL | `https://dev.enervision-g3.dynv6.net` | `https://rec.enervision-g3.dynv6.net` | `https://enervision-g3.dynv6.net` |
| Proxy HTTP, HTTPS | `127.0.0.1:8083`, `9443` | `127.0.0.1:8081`, `8443` | `80`, `443` | | Proxy HTTP, HTTPS, PROXY protocol, sur `127.0.0.1` | `8083`, `9443`, `9444` | `8081`, `8443`, `8444` | `10080`, `10443`, `10444` |
| PostgreSQL, Mailpit, Airflow, sur `127.0.0.1` | `5435`, `8027`, `8084` | `5434`, `8026`, `8082` | `5433`, `8025`, `8080` | | PostgreSQL, Mailpit, Airflow, sur `127.0.0.1` | `5435`, `8027`, `8084` | `5434`, `8026`, `8082` | `5433`, `8025`, `8080` |
| Supervision (profil `monitoring`) | à la demande, `make monitoring-up` | à la demande, `make monitoring-up` | active, `COMPOSE_PROFILES=monitoring` | | Supervision (profil `monitoring`) | à la demande, `make monitoring-up` | à la demande, `make monitoring-up` | active, `COMPOSE_PROFILES=monitoring` |
| Grafana, Prometheus, Alertmanager, sur `127.0.0.1` | `3003`, `9092`, `9095` | `3002`, `9091`, `9094` | `3001`, `9090`, `9093` | | Grafana, Prometheus, Alertmanager, sur `127.0.0.1` | `3003`, `9092`, `9095` | `3002`, `9091`, `9094` | `3001`, `9090`, `9093` |
Les trois noms d'hôte visent la même IP, à déclarer dans le `/etc/hosts` des postes. Deux noms Les trois noms sont publics chez dynv6 et visent l'IP privée de la VM : rien à déclarer sur
distincts sont nécessaires : le cookie `__Secure-ev_refresh` est posé par hôte, pas par port. les postes du réseau de l'école, et rien n'est joignable hors de ce réseau. Trois noms distincts
La redirection HTTP de la recette et du développement est ramenée sur la boucle locale parce que la configuration sont nécessaires : le cookie `__Secure-ev_refresh` est posé par hôte, pas par port.
Nginx renvoie vers `https://$host` sans port, c'est-à-dire vers la production.
Aucune stack ne publie hors de la boucle locale. Le frontal `infra/front`, sur le réseau de
l'hôte, écoute 80 et 443 : il redirige le premier, et aiguille le second d'après le nom demandé
(SNI) vers l'écouteur PROXY protocol de la stack visée, sans déchiffrer le TLS. Chaque stack
garde son certificat Let's Encrypt, obtenu par défi DNS-01 (`make tls-dns01`) et renouvelé à
chaque déploiement ainsi que chaque nuit par `/etc/cron.d/enervision-tls`.
Le déploiement est décrit dans [50-cicd.md](50-cicd.md) : un runner GitHub Actions installé sur 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`. la VM aligne le dossier sur la branche poussée et lance `make stack-up`.
@@ -362,7 +368,8 @@ Ces arbitrages sont pris. Ils ne vivaient jusqu'ici que dans des commentaires de
| API | `8000` | Identique en conteneur et hors conteneur | | 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, `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 | | 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`, 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` | | Reverse proxy | `80` et `443`, plus `4443` | Les seuls ports publiés par `docker-compose.prod.yml`, via `PROXY_HTTP_PORT`, `PROXY_HTTPS_PORT` et `PROXY_FRONT_PORT`. 80 ne sert que la redirection et le défi ACME ; 4443 n'accepte que le PROXY protocol du frontal. Sur la VM, tous sur `127.0.0.1` |
| Frontal SNI de la VM | `80` et `443` de l'hôte | `infra/front`, seul composant exposé sur le réseau de l'école (ADR 0018) |
| SSH du serveur | `22` par défaut | `ssh_port`, redéfinissable | | SSH du serveur | `22` par défaut | `ssh_port`, redéfinissable |
| Base applicative | `enervision` | Variable `POSTGRES_DB` | | 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` | | Base de test | `enervision_test` | Créée par `db/init/110-test-database.sql`, nom attendu en dur par `apps/backend/tests/conftest.py` |
+6
View File
@@ -38,6 +38,12 @@ Apres l'apply, la machine porte `/srv/enervision/dev`, `/srv/enervision/rec` et
manuel, `make stack-up` dans chaque dossier ; les suivants sont joues par le runner a chaque push manuel, `make stack-up` dans chaque dossier ; les suivants sont joues par le runner a chaque push
sur `dev` et sur `main`, et a chaque lancement manuel d'une autre branche pour `dev`. sur `dev` et sur `main`, et a chaque lancement manuel d'une autre branche pour `dev`.
Noms et certificats (ADR 0018) : avant l'apply, la zone `domaine` doit exister chez dynv6 et son
jeton se trouver dans `<racine>/dns.token` (600, proprietaire). L'apply fait alors pointer la
zone, `rec` et `dev` vers la machine, obtient un certificat Let's Encrypt par environnement et planifie leur
renouvellement ; sans jeton, chaque environnement garde un certificat auto-signe. Le frontal SNI (`infra/front`)
se demarre une fois depuis le dossier de la prod, `make front-up`.
Retirer le runner se fait a la main, depuis les parametres du depot : `terraform destroy` ne le Retirer le runner se fait a la main, depuis les parametres du depot : `terraform destroy` ne le
desinscrit pas. desinscrit pas.
+12
View File
@@ -0,0 +1,12 @@
# Pourquoi : le réseau de l'hôte, parce que les trois stacks publient leur écouteur PROXY protocol
# sur 127.0.0.1 et que seul un conteneur sur l'hôte joint cette boucle locale (ADR 0018).
name: enervision-front
services:
front:
image: nginx:1.31-alpine
network_mode: host
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
restart: unless-stopped
+46
View File
@@ -0,0 +1,46 @@
# Pourquoi : trois environnements sur une seule IP, des URL sans port (ADR 0018). Ce frontal lit
# le nom demandé dans le ClientHello (SNI) et relaie le flux TLS intact vers le proxy de la
# stack visée : il ne détient aucun certificat, chaque stack garde le sien et ses en-têtes.
# Piège : relayé tel quel, le flux arriverait avec l'IP du frontal, et les limitations de débit
# de nginx et du backend deviendraient globales. D'où `proxy_protocol on`, reçu sur l'écouteur
# 4443 de chaque stack (infra/proxy/conf.d/enervision.conf), qui y restaure l'IP du client.
# Contrainte : ces ports sont ceux que `scripts/provision-host.sh` donne à PROXY_FRONT_PORT.
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
stream {
log_format aiguillage '$remote_addr [$time_local] $ssl_preread_server_name '
'-> $upstream_addr $status $session_time';
access_log /var/log/nginx/access.log aiguillage;
map $ssl_preread_server_name $stack {
~^rec\. 127.0.0.1:8444;
~^dev\. 127.0.0.1:9444;
default 127.0.0.1:10444;
}
server {
listen 443;
ssl_preread on;
proxy_pass $stack;
proxy_protocol on;
proxy_connect_timeout 5s;
}
}
http {
server_tokens off;
access_log off;
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
}
+22 -2
View File
@@ -76,8 +76,28 @@ Renouvellement, à passer en tâche planifiée sur la machine :
17 3 * * * cd /srv/enervision && make tls-renew >> /var/log/enervision-tls.log 2>&1 17 3 * * * cd /srv/enervision && make tls-renew >> /var/log/enervision-tls.log 2>&1
``` ```
Pour un domaine sans port 80 entrant, le défi DNS-01 est l'alternative : elle demande un ### Let's Encrypt par DNS-01, le mode de la VM
greffon certbot propre au fournisseur DNS et un jeton d'API, hors périmètre à ce jour.
La VM n'a qu'une IP privée : le défi HTTP-01 y est impossible. Ses trois noms sont chez dynv6,
dont l'API pose l'enregistrement TXT du défi DNS-01, et acme.sh le fait sans rien ouvrir
([ADR 0018](../../docs/adr/0018-noms-publics-certificats-dns01-et-frontal-sni.md)).
```bash
make tls-dns01 # PUBLIC_HOST lu dans .env, jeton dans ../dns.token (600)
```
Autre fournisseur : `DNS01_API` et `DNS01_JETON_VAR` nomment le greffon acme.sh et sa variable
(`dns_cf` et `CF_Token` pour Cloudflare, par exemple). La cible est rejouable : acme.sh ne renouvelle qu'à trente jours de l'échéance, installe le
résultat dans `tls/` et recharge le proxy s'il tourne. Son état vit dans `acme/`, ignoré par git.
`deploy.yml` la rejoue avant chaque `make stack-up`, et `/etc/cron.d/enervision-tls` chaque nuit.
## Écouteur PROXY protocol
Sur la VM, le frontal `infra/front` relaie les connexions TLS sans les déchiffrer. Reçues sur
443, elles porteraient son adresse, et `limit_req` comme `get_client_ip()` compteraient tous les
postes comme un seul. Le port 4443 ne les accepte qu'avec l'en-tête PROXY protocol, d'où
`real_ip_header proxy_protocol` tire l'IP du client ; seules les adresses des réseaux Docker ont
le droit de l'annoncer, et le port n'est publié que sur `127.0.0.1` (`PROXY_FRONT_PORT`).
## Vérifier la configuration sans démarrer la stack ## Vérifier la configuration sans démarrer la stack
+7
View File
@@ -7,6 +7,8 @@
# variable et le résolveur interne de Docker : la résolution redevient dynamique. # variable et le résolveur interne de Docker : la résolution redevient dynamique.
# Pourquoi : la redirection 80 vers 443 conserve `$host` plutôt qu'un nom canonique, faute de # Pourquoi : la redirection 80 vers 443 conserve `$host` plutôt qu'un nom canonique, faute de
# quoi l'accès par IP cesserait de fonctionner sur la cible. Risque acté dans l'ADR 0007. # quoi l'accès par IP cesserait de fonctionner sur la cible. Risque acté dans l'ADR 0007.
# Piège : 4443 n'accepte que le PROXY protocol du frontal (infra/front, ADR 0018), qui y porte
# l'IP du client. Seules les adresses des réseaux Docker ont le droit de l'annoncer.
server { server {
listen 80 default_server; listen 80 default_server;
@@ -23,9 +25,14 @@ server {
server { server {
listen 443 ssl default_server; listen 443 ssl default_server;
listen 4443 ssl proxy_protocol default_server;
http2 on; http2 on;
server_name _; server_name _;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header proxy_protocol;
resolver 127.0.0.11 valid=10s ipv6=off; resolver 127.0.0.11 valid=10s ipv6=off;
ssl_certificate /etc/nginx/tls/fullchain.pem; ssl_certificate /etc/nginx/tls/fullchain.pem;
@@ -60,6 +60,7 @@ resource "null_resource" "environnements" {
script = filesha256(local.provisionneur) script = filesha256(local.provisionneur)
racine = var.racine racine = var.racine
depot = var.depot_url depot = var.depot_url
domaine = var.domaine
} }
connection { connection {
@@ -83,6 +84,7 @@ resource "null_resource" "environnements" {
${local.sudo}env RACINE='${var.racine}' \ ${local.sudo}env RACINE='${var.racine}' \
REPO_URL='${var.depot_url}' \ REPO_URL='${var.depot_url}' \
PROPRIETAIRE='${var.proprietaire}' \ PROPRIETAIRE='${var.proprietaire}' \
DOMAINE='${var.domaine}' \
PUBLIC_IP='${var.adresse_publique}' \ PUBLIC_IP='${var.adresse_publique}' \
bash /tmp/provision-host.sh bash /tmp/provision-host.sh
rm -f /tmp/provision-host.sh rm -f /tmp/provision-host.sh
@@ -43,6 +43,12 @@ variable "depot_url" {
default = "https://github.com/ineszang/ProjetPiscine_EnerVision.git" default = "https://github.com/ineszang/ProjetPiscine_EnerVision.git"
} }
variable "domaine" {
type = string
description = "Zone dynv6 de la prod, rec. et dev. en sous-domaines (ADR 0018). Son jeton doit se trouver dans <racine>/dns.token sur la machine : provision-host.sh y fait pointer la zone et ses deux sous-domaines vers la machine."
default = "enervision-g3.dynv6.net"
}
variable "adresse_publique" { variable "adresse_publique" {
type = string type = string
description = "Adresse annoncee dans les certificats auto-signes. Vide : la premiere adresse de la VM." description = "Adresse annoncee dans les certificats auto-signes. Vide : la premiere adresse de la VM."
+151 -52
View File
@@ -1,25 +1,39 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Pourquoi : la machine porte trois environnements, chacun un clone du dépôt, un `.env` et un # Pourquoi : la machine porte trois environnements, chacun un clone du dépôt, un `.env` et un
# projet Compose (ADR 0009, ADR 0017). Ce script prépare la machine et les trois dossiers sans # projet Compose (ADR 0009, 0017), derrière un frontal SNI sur 443 (ADR 0018). Ce script prépare
# rien démarrer : construction des images et démarrage restent à l'opérateur, puis au runner. # la machine et les trois dossiers sans démarrer aucune stack : c'est le rôle du runner.
# Piège : le script, et non le `.env.example` du clone, fait foi pour les secrets (GENERATEURS)
# et l'adressage (tableau du bas). Un `.env` existant garde ses secrets, reçoit ceux qui lui
# manquent et voit son adressage réaligné : sans ça, un `.env` né avant une clé ne la reçoit
# jamais, et le clone de la prod, en retard sur `main`, ne connaîtrait pas les nouvelles.
# Piège : lancé en root, git refuse un clone déjà chowné au runner (propriété douteuse). D'où # Piège : lancé en root, git refuse un clone déjà chowné au runner (propriété douteuse). D'où
# `safe.directory` passé en ligne de commande, seule portée où git l'accepte - preparer(). # `safe.directory` passé en ligne de commande, seule portée où git l'accepte - preparer().
# Rejouable : un dossier déjà cloné est réaligné sur sa branche, un `.env` existant n'est jamais # Rejouable : un certificat n'est refait que s'il ne couvre plus l'hôte, et celui de Let's Encrypt
# réécrit, un certificat présent n'est jamais régénéré. # n'est renouvelé qu'à échéance.
set -euo pipefail set -euo pipefail
DEPOT="${REPO_URL:-https://github.com/ineszang/ProjetPiscine_EnerVision.git}" DEPOT="${REPO_URL:-https://github.com/ineszang/ProjetPiscine_EnerVision.git}"
RACINE="${RACINE:-/srv/enervision}" RACINE="${RACINE:-/srv/enervision}"
DOMAINE="${DOMAINE:-enervision-g3.dynv6.net}"
ADRESSE="${PUBLIC_IP:-$(hostname -I | awk '{print $1}')}" ADRESSE="${PUBLIC_IP:-$(hostname -I | awk '{print $1}')}"
PROPRIETAIRE="${PROPRIETAIRE:-${SUDO_USER:-}}" PROPRIETAIRE="${PROPRIETAIRE:-${SUDO_USER:-}}"
JETON_DNS="$RACINE/dns.token"
COMPOSE_MINIMALE="2.24.4" COMPOSE_MINIMALE="2.24.4"
erreur() { echo "erreur : $*" >&2; exit 1; } erreur() { echo "erreur : $*" >&2; exit 1; }
secret() { openssl rand -base64 48 | tr -d '/+=\n' | cut -c1-48; } secret() { openssl rand -base64 48 | tr -d '/+=\n' | cut -c1-48; }
court() { secret | cut -c1-20; }
# Clé Fernet : 32 octets en base64 urlsafe, padding compris. # Clé Fernet : 32 octets en base64 urlsafe, padding compris.
fernet() { openssl rand -base64 32 | tr '+/' '-_'; } fernet() { openssl rand -base64 32 | tr '+/' '-_'; }
declare -A GENERATEURS=(
[POSTGRES_PASSWORD]=secret [APP_SECRET_KEY]=secret [AIRFLOW_FERNET_KEY]=fernet
[AIRFLOW_API_SECRET_KEY]=secret [AIRFLOW_JWT_SECRET]=secret [AIRFLOW_ADMIN_PASSWORD]=court
[AIRFLOW_APP_SECRET_KEY]=secret [APP_METRICS_TOKEN]=secret [GRAFANA_ADMIN_PASSWORD]=court
[SUPERVISION_DB_PASSWORD]=secret
)
verifier_outils() { verifier_outils() {
for outil in git make openssl curl; do for outil in git make openssl curl; do
command -v "$outil" >/dev/null || erreur "$outil absent (apt-get install $outil)" command -v "$outil" >/dev/null || erreur "$outil absent (apt-get install $outil)"
@@ -35,11 +49,27 @@ verifier_outils() {
echo "docker compose $version, sortie Internet : ok" echo "docker compose $version, sortie Internet : ok"
} }
valeur() { sed -n "s/^$2=//p" "$1" | tail -1; }
poser() {
local fichier="$1" cle="$2" contenu="$3"
if grep -q "^$cle=" "$fichier"; then
CLE="$cle" CONTENU="$contenu" awk -F= '
$1 == ENVIRON["CLE"] { print ENVIRON["CLE"] "=" ENVIRON["CONTENU"]; next } { print }
' "$fichier" > "$fichier.tmp"
mv "$fichier.tmp" "$fichier"
else
printf '%s=%s\n' "$cle" "$contenu" >> "$fichier"
fi
}
preparer() { preparer() {
local env="$1" branche="$2" hote="$3" origine="$4" local env="$1" branche="$2" hote="$3"
local port_https="$5" port_http="$6" port_pg="$7" port_mailpit="$8" port_airflow="$9" local port_https="$4" port_http="$5" port_front="$6" port_pg="$7" port_mailpit="$8"
local profils="${10}" port_grafana="${11}" port_prometheus="${12}" port_alertmanager="${13}" local port_airflow="$9" profils="${10}" port_grafana="${11}" port_prometheus="${12}"
local port_alertmanager="${13}"
local dossier="$RACINE/$env" local dossier="$RACINE/$env"
local fichier="$dossier/.env" brouillon="$dossier/.env.brouillon" cle oubliees ajoutees=""
if [[ -d "$dossier/.git" ]]; then if [[ -d "$dossier/.git" ]]; then
local git=(git -c "safe.directory=$dossier" -C "$dossier") local git=(git -c "safe.directory=$dossier" -C "$dossier")
@@ -50,71 +80,140 @@ preparer() {
git clone --quiet --branch "$branche" "$DEPOT" "$dossier" git clone --quiet --branch "$branche" "$DEPOT" "$dossier"
fi fi
if [[ ! -f "$dossier/.env" ]]; then local masque
local brouillon="$dossier/.env.brouillon" oubliees masque="$(umask)"
sed -e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(secret)|" \ umask 077
-e "s|^POSTGRES_PORT=.*|POSTGRES_PORT=$port_pg|" \ if [[ -f "$fichier" ]]; then
-e "s|^APP_SECRET_KEY=.*|APP_SECRET_KEY=$(secret)|" \ cp -p "$fichier" "$brouillon"
-e "s|^MAILPIT_UI_PORT=.*|MAILPIT_UI_PORT=$port_mailpit|" \ awk -F= 'NR == FNR { connues[$1]; next } /^[A-Z_][A-Z0-9_]*=/ && !($1 in connues)' \
-e "s|^AIRFLOW_PORT=.*|AIRFLOW_PORT=$port_airflow|" \ "$fichier" "$dossier/.env.example" >> "$brouillon"
-e "s|^AIRFLOW_FERNET_KEY=.*|AIRFLOW_FERNET_KEY=$(fernet)|" \ else
-e "s|^AIRFLOW_API_SECRET_KEY=.*|AIRFLOW_API_SECRET_KEY=$(secret)|" \ cp "$dossier/.env.example" "$brouillon"
-e "s|^AIRFLOW_JWT_SECRET=.*|AIRFLOW_JWT_SECRET=$(secret)|" \ fi
-e "s|^AIRFLOW_ADMIN_PASSWORD=.*|AIRFLOW_ADMIN_PASSWORD=$(secret | cut -c1-20)|" \
-e "s|^AIRFLOW_APP_SECRET_KEY=.*|AIRFLOW_APP_SECRET_KEY=$(secret)|" \ for cle in "${!GENERATEURS[@]}"; do
-e "s|^PUBLIC_HOST=.*|PUBLIC_HOST=$hote|" \ case "$(valeur "$brouillon" "$cle")" in
-e "s|^PUBLIC_ORIGIN=.*|PUBLIC_ORIGIN=$origine|" \ "" | change_me) poser "$brouillon" "$cle" "$("${GENERATEURS[$cle]}")"; ajoutees+=" $cle" ;;
-e "s|^COMPOSE_PROJECT_NAME=.*|COMPOSE_PROJECT_NAME=enervision-$env|" \ esac
-e "s|^PROXY_HTTP_PORT=.*|PROXY_HTTP_PORT=$port_http|" \
-e "s|^PROXY_HTTPS_PORT=.*|PROXY_HTTPS_PORT=$port_https|" \
-e "s|^COMPOSE_PROFILES=.*|COMPOSE_PROFILES=$profils|" \
-e "s|^APP_METRICS_TOKEN=.*|APP_METRICS_TOKEN=$(secret)|" \
-e "s|^GRAFANA_ADMIN_PASSWORD=.*|GRAFANA_ADMIN_PASSWORD=$(secret | cut -c1-20)|" \
-e "s|^SUPERVISION_DB_PASSWORD=.*|SUPERVISION_DB_PASSWORD=$(secret)|" \
-e "s|^GRAFANA_PORT=.*|GRAFANA_PORT=$port_grafana|" \
-e "s|^PROMETHEUS_PORT=.*|PROMETHEUS_PORT=$port_prometheus|" \
-e "s|^ALERTMANAGER_PORT=.*|ALERTMANAGER_PORT=$port_alertmanager|" \
"$dossier/.env.example" > "$brouillon"
# 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%%=*}=" "$brouillon" || echo "$cle" >> "$brouillon"
done done
# Piège : une clé renommée en amont garde sa valeur d'exemple, que le `:?` du compose ne
# voit pas puisqu'elle n'est pas vide. Cas vécu : AIRFLOW_WEBSERVER_SECRET_KEY, Airflow 3. declare -A adressage=(
[PUBLIC_HOST]="$hote" [PUBLIC_ORIGIN]="https://$hote" [COMPOSE_PROJECT_NAME]="enervision-$env"
[PROXY_HTTPS_PORT]="$port_https" [PROXY_HTTP_PORT]="$port_http" [PROXY_FRONT_PORT]="$port_front"
[POSTGRES_PORT]="$port_pg" [MAILPIT_UI_PORT]="$port_mailpit" [AIRFLOW_PORT]="$port_airflow"
[COMPOSE_PROFILES]="$profils" [GRAFANA_PORT]="$port_grafana"
[PROMETHEUS_PORT]="$port_prometheus" [ALERTMANAGER_PORT]="$port_alertmanager"
)
for cle in "${!adressage[@]}"; do
poser "$brouillon" "$cle" "${adressage[$cle]}"
done
# Piège : une clé renommée en amont garde sa valeur d'exemple, que le `:?` du compose ne voit
# pas puisqu'elle n'est pas vide. Cas vécu : AIRFLOW_WEBSERVER_SECRET_KEY, Airflow 3.
oubliees="$(grep '=change_me$' "$brouillon" | grep -v '^APP_MOCK_API_' | cut -d= -f1 | tr '\n' ' ' || true)" oubliees="$(grep '=change_me$' "$brouillon" | grep -v '^APP_MOCK_API_' | cut -d= -f1 | tr '\n' ' ' || true)"
if [[ -n "$oubliees" ]]; then if [[ -n "$oubliees" ]]; then
rm -f "$brouillon" rm -f "$brouillon"
erreur "$env : secrets non générés, .env non écrit : $oubliees" erreur "$env : clés sans générateur, .env inchangé : $oubliees"
fi fi
chmod 600 "$brouillon" chmod 600 "$brouillon"
mv "$brouillon" "$dossier/.env" if [[ -f "$fichier" ]]; then
cat "$brouillon" > "$fichier"
rm -f "$brouillon"
echo "$env : .env réaligné sur le tableau${ajoutees:+, secrets ajoutés :$ajoutees}"
else
mv "$brouillon" "$fichier"
echo "$env : .env généré. Reste à renseigner APP_MOCK_API_USERNAME et APP_MOCK_API_PASSWORD." echo "$env : .env généré. Reste à renseigner APP_MOCK_API_USERNAME et APP_MOCK_API_PASSWORD."
fi fi
umask "$masque"
if [[ ! -f "$dossier/infra/proxy/tls/fullchain.pem" ]]; then if ! openssl x509 -in "$dossier/infra/proxy/tls/fullchain.pem" -noout -checkhost "$hote" 2>/dev/null \
(cd "$dossier" && PUBLIC_HOST="$hote" PUBLIC_IP="$ADRESSE" ./scripts/tls-selfsigned.sh) | grep -q " does match"; then
(cd "$dossier" && PUBLIC_HOST="$hote" PUBLIC_IP="$ADRESSE" ./scripts/tls-selfsigned.sh --force)
fi fi
echo "$env : $dossier sur $branche, $origine" if [[ -r "$JETON_DNS" && "$hote" != *.local ]]; then
make -C "$dossier" --no-print-directory tls-dns01 PUBLIC_HOST="$hote" \
|| echo "$env : pas de certificat Let's Encrypt, l'auto-signé reste en place" >&2
fi
echo "$env : $dossier sur $branche, https://$hote"
}
# Pourquoi : dynv6 est le fournisseur que le filtrage de l'école laisse passer (ADR 0018). Le
# jeton passe par l'environnement du seul processus Python, jamais par `argv`.
publier_dns() {
[[ -r "$JETON_DNS" ]] || { echo "pas de jeton $JETON_DNS : ni DNS ni Let's Encrypt"; return 0; }
DNS_TOKEN="$(tr -d '[:space:]' < "$JETON_DNS")" python3 - "$DOMAINE" "$ADRESSE" rec dev <<'PY' \
|| echo "DNS : dynv6 refuse la mise à jour de $DOMAINE, enregistrements inchangés" >&2
import json, os, sys, time, urllib.request
domaine, adresse, *sous_noms = sys.argv[1:]
def appel(methode, chemin, corps=None):
requete = urllib.request.Request(
f"https://dynv6.com/api/v2/{chemin}", method=methode,
data=None if corps is None else json.dumps(corps).encode(),
headers={"Authorization": f"Bearer {os.environ['DNS_TOKEN']}", "User-Agent": "enervision-provision",
"Content-Type": "application/json", "Accept": "application/json"})
with urllib.request.urlopen(requete, timeout=60) as reponse:
contenu = reponse.read()
return json.loads(contenu) if contenu else None
def synchroniser():
zone = appel("GET", f"zones/by-name/{domaine}")
if zone.get("ipv4address") != adresse:
appel("PATCH", f"zones/{zone['id']}", {"ipv4address": adresse})
existants = {(r["type"], r["name"]): r for r in appel("GET", f"zones/{zone['id']}/records")}
for nom in sous_noms:
actuel = existants.get(("A", nom))
if actuel is None:
appel("POST", f"zones/{zone['id']}/records", {"type": "A", "name": nom, "data": adresse})
elif actuel["data"] != adresse:
appel("PATCH", f"zones/{zone['id']}/records/{actuel['id']}", {"data": adresse})
# dynv6 laisse parfois une écriture sans réponse, appliquée ou non : chaque essai relit l'état
# avant d'écrire, si bien qu'une création aboutie malgré le délai n'est jamais dupliquée.
for essai in range(3):
try:
synchroniser()
break
except OSError:
if essai == 2:
raise
time.sleep(5)
print(f"DNS : {domaine}, {', '.join(sous_noms)} visent {adresse}")
PY
}
planifier_renouvellement() {
[[ "$(id -u)" -eq 0 && -n "$PROPRIETAIRE" && -d /etc/cron.d ]] || return 0
cat > /etc/cron.d/enervision-tls <<CRON
# Renouvellement Let's Encrypt des trois environnements (ADR 0018), écrit par provision-host.sh.
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
23 4 * * * $PROPRIETAIRE for e in prod rec dev; do make -C $RACINE/\$e --no-print-directory tls-dns01; done 2>&1 | logger -t enervision-tls
CRON
chmod 644 /etc/cron.d/enervision-tls
echo "renouvellement planifié : /etc/cron.d/enervision-tls"
} }
verifier_outils verifier_outils
mkdir -p "$RACINE" mkdir -p "$RACINE"
publier_dns
# Supervision active en prod seulement (ADR 0016) ; les ports de la recette et de dev restent # Supervision active en prod seulement (ADR 0016). Le frontal (infra/front) publie 80 et 443 et
# décalés au cas où on l'y lancerait à la demande. # relaie vers les ports `front` ; les stacks ne publient plus rien hors de la boucle locale.
# env branche hôte origine https http pg mailpit airflow profils grafana prometheus alertmanager # env branche hôte https http front pg mailpit airflow profils grafana prometheus alertmanager
preparer prod main enervision.local https://enervision.local 443 80 5433 8025 8080 monitoring 3001 9090 9093 preparer prod main "$DOMAINE" 127.0.0.1:10443 127.0.0.1:10080 127.0.0.1:10444 5433 8025 8080 monitoring 3001 9090 9093
preparer rec dev rec.enervision.local https://rec.enervision.local:8443 8443 127.0.0.1:8081 5434 8026 8082 "" 3002 9091 9094 preparer rec dev "rec.$DOMAINE" 127.0.0.1:8443 127.0.0.1:8081 127.0.0.1:8444 5434 8026 8082 "" 3002 9091 9094
preparer dev dev dev.enervision.local https://dev.enervision.local:9443 9443 127.0.0.1:8083 5435 8027 8084 "" 3003 9092 9095 preparer dev dev "dev.$DOMAINE" 127.0.0.1:9443 127.0.0.1:8083 127.0.0.1:9444 5435 8027 8084 "" 3003 9092 9095
planifier_renouvellement
if [[ -n "$PROPRIETAIRE" && "$(id -u)" -eq 0 ]]; then if [[ -n "$PROPRIETAIRE" && "$(id -u)" -eq 0 ]]; then
chown -R "$PROPRIETAIRE" "$RACINE" chown -R "$PROPRIETAIRE" "$RACINE"
fi fi
cat <<FIN cat <<FIN
Démarrage, dans chaque dossier : make stack-up, qui applique aussi les migrations. Démarrage, dans chaque dossier : make stack-up, qui applique aussi les migrations. Puis, une
fois, depuis $RACINE/prod : make front-up, que chaque déploiement de la prod rejoue ensuite.
Premier administrateur, stack démarrée, dans chaque dossier : Premier administrateur, stack démarrée, dans chaque dossier :
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backend \\ docker compose -f docker-compose.yml -f docker-compose.prod.yml exec backend \\
python -m app.cli create-admin --email <adresse> python -m app.cli create-admin --email <adresse>
@@ -124,6 +223,6 @@ L'installer sous le propriétaire de $RACINE, sinon git refuse ces dépôts et l
échappe : relancer au besoin ce script avec PROPRIETAIRE=<utilisateur du runner>. échappe : relancer au besoin ce script avec PROPRIETAIRE=<utilisateur du runner>.
Données historiques : git ne porte pas data/raw, déposer les fichiers dans chaque dossier avant Données historiques : git ne porte pas data/raw, déposer les fichiers dans chaque dossier avant
de déclencher le DAG historical_import. de déclencher le DAG historical_import.
Depuis un poste : ajouter « $ADRESSE enervision.local rec.enervision.local dev.enervision.local » Noms et certificats : le jeton dynv6 de la zone $DOMAINE doit se trouver dans $JETON_DNS (600,
à /etc/hosts. propriétaire du runner). Sans lui, ni enregistrement DNS ni Let's Encrypt : auto-signé.
FIN FIN