From eee7dbd108e391dbe497069dd7715b99db0751cf Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Sun, 5 Jul 2026 18:37:47 -0400 Subject: [PATCH] =?UTF-8?q?web=5Fdorsal=20:=20r=C3=B4le=20webapps=20dynami?= =?UTF-8?q?ques=20(2e=20brique=20plateforme,=20100%=20natif)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runtime (venv/paquets) + service systemd + nginx local, derrière l'edge, ZÉRO conteneur. Chaque app : git clone (depth 1) → venv + pip → unité systemd durcie (app en localhost, user webapp, NoNewPrivileges/PrivateTmp) → vhost nginx local qui la fronte. data_dir persistant pour l'état local (SQLite) ; BD réseau via resoudre_base sinon. Éprouvé bout-en-bout sur web-frontal-01 par le rôle : monregistraire (FastAPI+ uvicorn+SQLite) cloné de Forgejo, servi via l'edge en HTTPS step-ca, API OK. Avec web_frontal, la plateforme webapp native est complète. lint production: 0. Co-Authored-By: Claude Opus 4.8 --- playbooks/groupes/web_dorsal.yml | 15 +++ roles/web_dorsal/README.md | 44 ++++++++ roles/web_dorsal/defaults/main.yml | 23 +++++ roles/web_dorsal/handlers/main.yml | 16 +++ roles/web_dorsal/meta/empreinte.yml | 6 ++ roles/web_dorsal/meta/flux.yml | 16 +++ roles/web_dorsal/tasks/main.yml | 116 ++++++++++++++++++++++ roles/web_dorsal/templates/app.service.j2 | 21 ++++ roles/web_dorsal/templates/proxy.conf.j2 | 14 +++ 9 files changed, 271 insertions(+) create mode 100644 playbooks/groupes/web_dorsal.yml create mode 100644 roles/web_dorsal/README.md create mode 100644 roles/web_dorsal/defaults/main.yml create mode 100644 roles/web_dorsal/handlers/main.yml create mode 100644 roles/web_dorsal/meta/empreinte.yml create mode 100644 roles/web_dorsal/meta/flux.yml create mode 100644 roles/web_dorsal/tasks/main.yml create mode 100644 roles/web_dorsal/templates/app.service.j2 create mode 100644 roles/web_dorsal/templates/proxy.conf.j2 diff --git a/playbooks/groupes/web_dorsal.yml b/playbooks/groupes/web_dorsal.yml new file mode 100644 index 0000000..3fdc79e --- /dev/null +++ b/playbooks/groupes/web_dorsal.yml @@ -0,0 +1,15 @@ +--- +- name: Appliquer le groupe web_dorsal + hosts: web_dorsal + become: true + gather_facts: true + + pre_tasks: + - name: Vérifier que la cible est Debian + ansible.builtin.assert: + that: + - ansible_facts.distribution == "Debian" + fail_msg: "Ce playbook est prévu pour Debian." + + roles: + - web_dorsal diff --git a/roles/web_dorsal/README.md b/roles/web_dorsal/README.md new file mode 100644 index 0000000..dea9a33 --- /dev/null +++ b/roles/web_dorsal/README.md @@ -0,0 +1,44 @@ +# web_dorsal + +Hébergement de **webapps dynamiques** (runtime + service systemd), derrière l'edge. +Deuxième brique de la **plateforme webapp** de Set-OPS : **100 % natif** — venv/paquets ++ systemd + nginx, **zéro conteneur**. Codifié depuis le spike du 2026-07-05 +(monregistraire : FastAPI + uvicorn + SQLite). + +## Patron +Pour chaque app déclarée dans `web_dorsal_apps` : +1. `git clone` (depth 1) → `/srv/webapp/` ; +2. venv Python + `pip install -r requirements.txt` ; +3. **unité systemd** (l'app écoute en `127.0.0.1:`, user `webapp`, durcie : + NoNewPrivileges, PrivateTmp) ; +4. **vhost nginx local** qui fronte l'app (`server_name` → `127.0.0.1:`). + +L'app reste **en localhost** (sûr) ; l'edge proxie `server_name` → nginx local → app. +Exposé via `applications.yml` (port **80**, pas le port interne de l'app). + +## Déclaration (group_vars du groupe web_dorsal) +```yaml +web_dorsal_apps: + - nom: registraire + server_name: "registraire.{{ domaine_interne }}" + depot: "https://forge.{{ domaine_interne }}/…/monregistraire.git" + branche: main + port: 8001 + commande: ".venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8001" + env: { IMMOS_DB_PATH: /var/lib/registraire/immos.db } + data_dir: /var/lib/registraire # état local (SQLite), optionnel +``` +Et une app `groupe: web_dorsal, hote: , port: 80, expose: [registraire.]`. + +## BD +Les apps à état **local** (SQLite) : `data_dir` persistant. Les apps à **BD réseau** +(PostgreSQL) : déclarer la base dans `bases-donnees.yml` et passer le DSN via `env` +(resoudre_base) — même motif que les autres services Set-OPS. + +## Runtimes +Éprouvé sur **Python (FastAPI/uvicorn)**. Node/autres : adapter `commande` + les +paquets de runtime (le patron systemd + nginx + git reste identique). + +## Validation +`ansible-lint` profil production + `--syntax-check`. Éprouvé sur web-frontal-01 +(monregistraire servi via l'edge, HTTPS step-ca, API fonctionnelle). diff --git a/roles/web_dorsal/defaults/main.yml b/roles/web_dorsal/defaults/main.yml new file mode 100644 index 0000000..786aab8 --- /dev/null +++ b/roles/web_dorsal/defaults/main.yml @@ -0,0 +1,23 @@ +--- +# ========================================================================= +# web_dorsal — webapps DYNAMIQUES (runtime + service systemd), derriere l'edge. +# 2e brique de la plateforme webapp : 100% natif (venv/paquets + systemd + nginx), +# ZERO conteneur. L'app ecoute en localhost ; un vhost nginx local la fronte ; +# l'edge proxie vers ce nginx. BD reseau (PG) via resoudre_base si l'app en a une ; +# les apps a etat local (SQLite) utilisent un data_dir persistant. +# Codifie depuis le spike du 2026-07-05 (monregistraire, FastAPI + uvicorn + SQLite). +# ========================================================================= +web_dorsal_racine: "/srv/webapp" +web_dorsal_utilisateur: "webapp" + +# Liste des webapps servies par ce noeud. Chaque entree : +# nom identifiant court (dossier + service + vhost) +# server_name FQDN servi (doit etre expose via applications.yml -> edge, port 80) +# depot URL du depot git de l'app (souverain, ex. Forgejo) +# branche branche a deployer (defaut: main) +# port port local ecoute par l'app (127.0.0.1:) +# commande ExecStart, relatif au dossier de l'app (ex: .venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8001) +# requirements fichier pip (defaut: requirements.txt) ; venv cree automatiquement +# env dict de variables d'environnement (secrets via voute) +# data_dir repertoire d'etat persistant (optionnel ; cree, possede par webapp) +web_dorsal_apps: [] diff --git a/roles/web_dorsal/handlers/main.yml b/roles/web_dorsal/handlers/main.yml new file mode 100644 index 0000000..837174f --- /dev/null +++ b/roles/web_dorsal/handlers/main.yml @@ -0,0 +1,16 @@ +--- +- name: Recharger nginx web_dorsal + ansible.builtin.systemd: + name: nginx + state: reloaded + when: not ansible_check_mode + +- name: Redemarrer les webapps + ansible.builtin.systemd: + name: "{{ item.nom }}" + state: restarted + daemon_reload: true + loop: "{{ web_dorsal_apps }}" + loop_control: + label: "{{ item.nom }}" + when: not ansible_check_mode diff --git a/roles/web_dorsal/meta/empreinte.yml b/roles/web_dorsal/meta/empreinte.yml new file mode 100644 index 0000000..e4e2601 --- /dev/null +++ b/roles/web_dorsal/meta/empreinte.yml @@ -0,0 +1,6 @@ +--- +# Empreinte de dimensionnement (propriete du role). Webapp dynamique (runtime +# Python/Node + service) : memoire moderee. Ajuster selon les apps hebergees. +coeurs: 1 +memoire_mo: 1024 +disque_go: 10 diff --git a/roles/web_dorsal/meta/flux.yml b/roles/web_dorsal/meta/flux.yml new file mode 100644 index 0000000..2769943 --- /dev/null +++ b/roles/web_dorsal/meta/flux.yml @@ -0,0 +1,16 @@ +--- +# Flux reseau du dorsal web (propriete du role). Voir docs/flux-conception.md. +# L'app ecoute en LOCALHOST (pas de flux inter-noeud) ; le nginx local la fronte. +flux: + - sens: ingress + port: 80 + protocole: tcp + pair: edge + chiffrement: clair + raison: "Front nginx local des webapps, proxie par l'edge (TLS termine a l'edge)." + - sens: egress + port: 443 + protocole: tcp + pair: flotte + chiffrement: tls + raison: "git clone/pull du depot de chaque app (Forgejo souverain) au deploiement." diff --git a/roles/web_dorsal/tasks/main.yml b/roles/web_dorsal/tasks/main.yml new file mode 100644 index 0000000..5d60e83 --- /dev/null +++ b/roles/web_dorsal/tasks/main.yml @@ -0,0 +1,116 @@ +--- +- name: Paquets (python venv/pip, git, nginx) + ansible.builtin.apt: + name: + - python3-venv + - python3-pip + - git + - nginx + state: present + update_cache: true + cache_valid_time: 3600 + +- name: Utilisateur de service des webapps + ansible.builtin.user: + name: "{{ web_dorsal_utilisateur }}" + system: true + home: "{{ web_dorsal_racine }}" + shell: /usr/sbin/nologin + create_home: false + +- name: Creer la racine des webapps + ansible.builtin.file: + path: "{{ web_dorsal_racine }}" + state: directory + owner: root + group: root + mode: "0755" + +- name: Cloner / mettre a jour chaque app (clone superficiel) + ansible.builtin.git: + repo: "{{ item.depot }}" + dest: "{{ web_dorsal_racine }}/{{ item.nom }}" + version: "{{ item.branche | default('main') }}" + depth: 1 + force: true + loop: "{{ web_dorsal_apps }}" + loop_control: + label: "{{ item.nom }}" + notify: Redemarrer les webapps + +- name: Creer le venv + installer les dependances de chaque app + ansible.builtin.pip: + requirements: "{{ web_dorsal_racine }}/{{ item.nom }}/{{ item.requirements | default('requirements.txt') }}" + virtualenv: "{{ web_dorsal_racine }}/{{ item.nom }}/.venv" + virtualenv_command: python3 -m venv + loop: "{{ web_dorsal_apps }}" + loop_control: + label: "{{ item.nom }}" + notify: Redemarrer les webapps + +- name: Repertoire d'etat persistant (apps avec data_dir) + ansible.builtin.file: + path: "{{ item.data_dir }}" + state: directory + owner: "{{ web_dorsal_utilisateur }}" + group: "{{ web_dorsal_utilisateur }}" + mode: "0750" + loop: "{{ web_dorsal_apps }}" + loop_control: + label: "{{ item.nom }}" + when: item.data_dir is defined + +- name: Donner le code au user de service + ansible.builtin.file: + path: "{{ web_dorsal_racine }}/{{ item.nom }}" + owner: "{{ web_dorsal_utilisateur }}" + group: "{{ web_dorsal_utilisateur }}" + recurse: true + loop: "{{ web_dorsal_apps }}" + loop_control: + label: "{{ item.nom }}" + +- name: Deployer l'unite systemd de chaque app + ansible.builtin.template: + src: app.service.j2 + dest: "/etc/systemd/system/{{ item.nom }}.service" + owner: root + group: root + mode: "0644" + loop: "{{ web_dorsal_apps }}" + loop_control: + label: "{{ item.nom }}" + notify: Redemarrer les webapps + +- name: Deployer le vhost proxy local de chaque app + ansible.builtin.template: + src: proxy.conf.j2 + dest: "/etc/nginx/sites-available/{{ item.nom }}" + owner: root + group: root + mode: "0644" + loop: "{{ web_dorsal_apps }}" + loop_control: + label: "{{ item.nom }}" + notify: Recharger nginx web_dorsal + +- name: Activer le vhost proxy de chaque app + ansible.builtin.file: + src: "/etc/nginx/sites-available/{{ item.nom }}" + dest: "/etc/nginx/sites-enabled/{{ item.nom }}" + state: link + loop: "{{ web_dorsal_apps }}" + loop_control: + label: "{{ item.nom }}" + notify: Recharger nginx web_dorsal + +- name: Activer et demarrer chaque webapp + ansible.builtin.systemd: + name: "{{ item.nom }}" + enabled: true + state: started + daemon_reload: true + loop: "{{ web_dorsal_apps }}" + loop_control: + label: "{{ item.nom }}" + when: not ansible_check_mode diff --git a/roles/web_dorsal/templates/app.service.j2 b/roles/web_dorsal/templates/app.service.j2 new file mode 100644 index 0000000..64f1386 --- /dev/null +++ b/roles/web_dorsal/templates/app.service.j2 @@ -0,0 +1,21 @@ +# Webapp {{ item.nom }} — unite generee par Set-OPS (role web_dorsal). Ne pas editer. +[Unit] +Description=Webapp {{ item.nom }} ({{ item.server_name }}) +After=network.target + +[Service] +Type=simple +User={{ web_dorsal_utilisateur }} +Group={{ web_dorsal_utilisateur }} +WorkingDirectory={{ web_dorsal_racine }}/{{ item.nom }} +{% for k, v in (item.env | default({}) | dictsort) %} +Environment="{{ k }}={{ v }}" +{% endfor %} +ExecStart={{ web_dorsal_racine }}/{{ item.nom }}/{{ item.commande }} +Restart=on-failure +RestartSec=3 +NoNewPrivileges=true +PrivateTmp=true + +[Install] +WantedBy=multi-user.target diff --git a/roles/web_dorsal/templates/proxy.conf.j2 b/roles/web_dorsal/templates/proxy.conf.j2 new file mode 100644 index 0000000..8531841 --- /dev/null +++ b/roles/web_dorsal/templates/proxy.conf.j2 @@ -0,0 +1,14 @@ +# Front nginx local de la webapp {{ item.nom }} — genere par Set-OPS (web_dorsal). +# L'app ecoute en localhost ; l'edge proxie server_name -> ce vhost -> l'app. +server { + listen 80; + server_name {{ item.server_name }}; + + location / { + proxy_pass http://127.0.0.1:{{ item.port }}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +}