web_dorsal : rôle webapps dynamiques (2e brique plateforme, 100% natif)

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-07-05 18:37:47 -04:00
parent 72b7579c6e
commit eee7dbd108
9 changed files with 271 additions and 0 deletions

View file

@ -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

View file

@ -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/<nom>` ;
2. venv Python + `pip install -r requirements.txt` ;
3. **unité systemd** (l'app écoute en `127.0.0.1:<port>`, user `webapp`, durcie :
NoNewPrivileges, PrivateTmp) ;
4. **vhost nginx local** qui fronte l'app (`server_name` → `127.0.0.1:<port>`).
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: <noeud>, port: 80, expose: [registraire.<domaine>]`.
## 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).

View file

@ -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:<port>)
# 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: []

View file

@ -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

View file

@ -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

View file

@ -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."

View file

@ -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

View file

@ -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

View file

@ -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;
}
}