corentin_wakdo/docs/journal/2026-04-30--smoke-test-infra.md
Imugiii b09c461e94 docs: journal entry for session 4 smoke test infra
Captures the full bottom-end-to-bottom-end validation of the Docker
stack on the deployment host: env file merge with the BYAN tooling,
FQDN switch from acadenice.fr to stark.a3n.fr (using the existing
wildcard DNS), and three corrective fixes that surfaced during
'make init' (explicit IPAM subnet for the saturated host, init: true
on the cron service for dcron PID 1, static healthz file outside the
src bind-mount).

Documents 4 reasoned decisions with alternatives, 5 anticipated jury
questions, 5 conscious points of improvement, and maps 6 RNCP 37805
criteria (1.e.4, 5.b, 7.a.1, 7.a.2, 7.b.3, 7.c.4).
2026-04-30 11:52:45 +00:00

15 KiB

Smoke test infra Docker — passage prod-ready

Date : 2026-04-30 Branche : feat/infra-docker PR : a creer (suite de la session) Duree estimee : 2h30


Ce qui a ete fait

Validation bout-en-bout de la stack Docker livree en Session 3 (ac8b6a6), sur le serveur de deploiement reel.

  1. Fusion .env : le fichier existant ne contenait que les vars BYAN_API_* (outil tiers, lit ce fichier). Fusion avec le template .env.example Wakdo dans un seul .env (gitignore), sans deplacer les vars BYAN. Mots de passe DB generes via openssl rand -base64 32.
  2. Switch FQDN : corentin-wakdo.acadenice.fr -> corentin-wakdo.stark.a3n.fr (idem admin). Modifie dans README.md, docs/PROJECT_CONTEXT.md, .env. Commit 4edabf2.
  3. Smoke test make init : echec puis 3 corrections successives, finalement OK avec 4 conteneurs healthy. Commit d9890cf.
  4. Validation HTTPS externe via curl : cert Let's Encrypt provisionne automatiquement par Traefik sur les 2 FQDN, isolation /healthz confirmee (non expose publiquement).

Pourquoi — decisions et alternatives

Decision 1 : Switch FQDN sur *.stark.a3n.fr plutot qu'ajout de records sur acadenice.fr

  • Decision retenue : utiliser le wildcard DNS existant sur *.stark.a3n.fr (deja configure pour ce serveur).
  • Alternatives : ajouter 2 records A corentin-wakdo et corentin-wakdo-admin dans la zone acadenice.fr.
  • Raison : la zone acadenice.fr n'a pas de wildcard, son apex pointe vers un autre serveur (195.15.210.22), et Traefik d'ici utilise un challenge HTTP-01 (pas DNS-01), donc le FQDN cible doit resoudre vers cet hote avant de pouvoir provisionner un cert. Le wildcard *.stark.a3n.fr (verifie a coup de dig sur des sous-domaines aleatoires : foo-test-9999.stark.a3n.fr resout vers 62.210.93.152) supprime cette etape. Cout : la documentation projet ne reflete plus la branding acadenice.fr cible, mais c'est defendable a l'oral comme "sous-domaine d'infrastructure dev sur l'hote stark, branding final restera flexible cote DNS public quand le projet sortira".

Decision 2 : Subnet explicite 192.168.148.0/24 sur wakdo_internal

  • Decision retenue : declarer un subnet IPAM fixe dans docker-compose.yml au lieu de laisser Docker auto-allouer.
  • Alternatives :
    • docker network prune pour liberer 3 reseaux orphelins (~2 /20 + 1 /24)
    • Etendre default-address-pools dans /etc/docker/daemon.json + restart Docker daemon
    • Subnet en 10.x plutot que 192.168.x
  • Raison : sur cet hote mutualise, l'auto-allocateur Docker echoue (all predefined address pools have been fully subnetted) parce que les 15 /16 du pool par defaut (172.17.0.0/16 a 172.31.0.0/16) et 13 sur 16 /20 du 192.168.0.0/16 sont deja pris par d'autres stacks. Un prune liberait l'instant t mais le probleme reviendrait. Restart du daemon = blast radius eleve (les autres apps de l'hote coupees pendant ~30s). Subnet explicite = deterministe, defendable, isole Wakdo des fluctuations d'allocation auto. Choix de 192.168.148.0/24 : milieu du gap libre 192.168.144-159, hors collision avec les /24 acquagest voisins (150, 154, 155, 157), /24 = 254 IP = right-sized pour 4 services (RFC 1918 192.168.0.0/16, classes A/B/C deprecated par CIDR/RFC 1519 depuis 1993).

Decision 3 : init: true sur wakdo-cron (au lieu d'installer tini dans le Dockerfile)

  • Decision retenue : ajouter init: true au service wakdo-cron dans docker-compose.yml, qui declenche l'injection automatique de tini par Docker comme PID 1.
  • Alternative : installer tini dans le Dockerfile cron et prefixer le CMD par /sbin/tini -- (comme c'est fait pour wakdo-app).
  • Raison : symptome rencontre = dcron boucle sur setpgid: Operation not permitted apres demarrage. Cause = un processus tournant en PID 1 dans un namespace PID Linux sans init parent ne peut souvent pas changer son groupe de processus pour ses enfants forkes (limite kernel sur setpgid() quand le pere est PID 1). dcron exige cette capacite pour isoler chaque job dans son propre process group. La solution canonique = un init reaper (tini, dumb-init, s6-overlay). init: true est l'option Docker Compose qui demande au runtime d'injecter automatiquement un init minimal — moins de code que de modifier le Dockerfile, semantique declarative. Trade-off : le wakdo-app utilise son propre tini installe explicitement (heritage Session 3) — incoherence stylistique a accepter ou unifier plus tard.

Decision 4 : healthz servi en fichier statique depuis /usr/local/apache2/htdocs/

  • Decision retenue : Alias /healthz /usr/local/apache2/htdocs/healthz.txt + un fichier healthz.txt (3 octets, OK\n) embarque dans l'image Apache.
  • Alternatives :
    • RewriteRule ^/healthz$ - [R=200,L] (config initiale).
    • Alias /healthz /var/www/html/public/healthz.txt (premier essai du fix).
  • Raison : la directive R=200 declenche le mecanisme ErrorDocument interne d'Apache, qui rend un template HTML generique meme pour un statut 200 — d'ou le body parasite "The server encountered an internal error or misconfiguration..." au milieu d'une reponse 200 OK. Servir un vrai fichier supprime ce comportement. Le fichier doit vivre HORS du chemin bind-monte (./src -> /var/www/html) qui ecrase le contenu de l'image au runtime, sinon le COPY est masque. /usr/local/apache2/htdocs/ est un chemin Apache natif que le compose Wakdo ne bind-monte pas, donc adapte aux artefacts d'infrastructure.

Comment — points techniques cles

Saturation des pools d'adresses Docker

Docker daemon alloue les subnets bridge depuis une liste configurable default-address-pools. Les valeurs par defaut (cf. Docker engine source) :

172.17.0.0/16 -> 172.31.0.0/16   (15 pools de /16)
192.168.0.0/16 carve en /20      (16 pools de /20)

Sur cet hote : 15/15 /16 pris, 13/16 /20 pris dans 192.168. Quand docker network create (sans --subnet) ne trouve aucun bloc contigu disponible, le daemon retourne :

Error: all predefined address pools have been fully subnetted

C'est un constat operationnel : sur un hote partage qui heberge >30 stacks, declarer ses subnets explicitement est plus une discipline qu'un nice-to-have. La RFC 1918 donne 10.0.0.0/8 (16M IP) presque vide ici, et 192.168.144.0/20 (4096 IP) en gap dans le second pool. Le choix 192.168.148.0/24 reste dans la convention 192.168.x familiere tout en evitant la collision.

setpgid() et init dans un container PID namespace

Le kernel Linux accorde setpgid(pid, pgid) au processus appelant pour changer le pgid de ses enfants, sous conditions (cf. man 2 setpgid) :

  • l'enfant doit etre dans la meme session
  • la cible pgid doit appartenir a une session existante
  • l'appelant doit avoir des droits sur l'enfant

Quand un processus est PID 1 dans un namespace PID (cas du conteneur sans init explicite), il devient le process group leader d'office et ne peut pas se sortir de son propre PG facilement. dcron essaie de mettre chaque job dans un PG isole pour pouvoir l'attacher proprement et envoyer des signaux groupes — il echoue, le job exit 1, restart: unless-stopped relance, boucle. L'injection de tini (via init: true ou --init) place tini en PID 1 et dcron en PID 2+, donc dcron peut faire ses setpgid() sans souci.

Bind-mount masque le COPY de l'image

Comportement Docker : un bind-mount sur un chemin destination ecrase entierement le contenu de l'image a ce chemin. Erreur classique : on COPY config.txt /var/www/config.txt dans le Dockerfile, puis on bind-mount ./src:/var/www, et config.txt disparait au runtime. Le COPY a bien lieu au build, mais le bind-mount au run prend la priorite. C'est ce qui s'est passe pour healthz.txt au premier fix.

Solution propre : placer les artefacts d'infrastructure (healthchecks, scripts internes) dans un chemin que le compose ne bind-monte pas (/usr/local/apache2/htdocs/, /opt/app/, etc.), et reserver /var/www/html/ au code applicatif bind-monte en dev.

Validation cert Let's Encrypt sans config DNS prealable

Le wildcard DNS *.stark.a3n.fr resout deja corentin-wakdo.stark.a3n.fr et corentin-wakdo-admin.stark.a3n.fr vers 62.210.93.152. Au premier hit HTTPS externe, Traefik :

  1. Voit le label traefik.http.routers.wakdo-kiosk.rule=Host(...) et le certResolver=letsencrypt
  2. Lance le challenge HTTP-01 : LE leur envoie un GET /.well-known/acme-challenge/<token> en HTTP
  3. Le FQDN resout vers cet hote -> reponse correcte servie par Traefik (route specifique sur l'entrypoint web)
  4. LE valide, emet le cert, Traefik le stocke dans /acme.json et le sert sur :443

Le tout est invisible : pas de config a faire cote Wakdo. La seule contrainte = le FQDN resout vers l'hote avant le premier hit. C'etait le bloqueur derriere le switch FQDN en Decision 1.


Criteres RNCP couverts

  • Cr 7.a.1 ("Le candidat a bien analyse les contraintes en termes d'infrastructure et de securite") : decisions Subnet et FQDN documentent une analyse explicite de l'environnement reel (hote mutualise, pools satures, wildcard disponible, Traefik partage). Pas de copie-colle d'un tutorial generique.
  • Cr 7.a.2 ("...justifie ses choix") : 4 decisions argumentees avec alternatives evaluees ci-dessus.
  • Cr 7.b.3 ("Mise en place d'un planificateur de tache, type cron tab") : wakdo-cron operationnel (verifie : crontab parse OK, 0 3 * * * backup-db.sh actif, init: true resout les bouclages). Impl detail : docker/cron/Dockerfile, docker/cron/crontab.
  • Cr 7.c.4 ("...mise en place d'une procedure de deploiement automatisee") : make init deploie l'integralite de la stack en une commande, idempotent, avec checks prealables (.env present, network existant, vars critiques renseignees). Echec cli net si pre-requis manquant.
  • Cr 1.e.4 ("Le candidat a respecte les bonnes pratiques de cloisonnement reseau") : verifie via curl externe que /healthz renvoie 403 quand requis avec un Host applicatif, donc le vhost healthz est bien isole des vhosts publics. Pas de fuite d'observabilite vers l'exterieur dans ce setup.
  • Cr 5.b ("Journalisation et tracabilite") : les services redirigent stdout/stderr vers docker logs (Apache ErrorLog /proc/self/fd/2, php-fpm via tini, dcron -d 8, mariadb defaut). docker compose -p wakdo logs suffit pour audit.

Questions anticipees du jury

  • Q : "Pourquoi avoir fusionne le .env BYAN avec celui du projet ? C'est pas du couplage ?" R : C'est une dette assumee. L'outil BYAN lit .env du dossier de travail ; je n'ai pas creuse comment changer ce chemin sans risquer de casser sa liaison avec son API. La separation en deux fichiers (docker compose --env-file .env.wakdo) etait l'option propre, mais cout de modifier le Makefile + risque de regression sur l'outil tiers. J'ai choisi la fusion en sachant que .env est gitignore (donc le couplage ne pollue pas le repo public) et documente la raison dans le .env lui-meme.

  • Q : "Pourquoi 192.168.148.0/24 exactement et pas 10.99.0.0/24 ?" R : Choix arbitre par convention de l'ecosysteme local. Les autres stacks de l'hote utilisent souvent 172.x ou 192.168.x ; rester dans la meme famille facilite le mental model des admins qui suivront. 10.x aurait ete plus "datacenter-pro" mais introduit une plage absente des autres stacks de cet hote — visibilite cognitive moindre. C'est un choix de coherence locale, pas une regle universelle.

  • Q : "Le redirect HTTP -> HTTPS retourne 404. Est-ce normal ?" R : Non. Mes labels Traefik definissent bien un router wakdo-kiosk-http sur l'entrypoint web avec middleware redirectscheme. Le 404 vient probablement d'une interaction avec le middleware global du Traefik d'hote (redirect-except-acqua@file declare dans traefik.toml) qui s'applique avant et ne match pas mes routers. Ce point est documente comme "a investiguer" dans le SESSION_RESUME ; en pratique l'usage final sera du HTTPS direct via les liens jury, donc non bloquant pour la demo.

  • Q : "Comment je sais que make init va marcher chez quelqu'un d'autre ?" R : Honnetement : il ne marchera pas sans adaptation. Le .env.example pointe sur des FQDN neutres (example.com, traefik_proxy) qu'il faut adapter, et la cible make init echoue cleanly avec un message d'aide si le reseau Traefik n'existe pas ou si .env manque des vars. Le smoke test ici a valide la chaine sur l'hote stark precis ; un autre hote demanderait probablement un autre subnet (ou un retour a l'auto-alloc si le daemon n'est pas sature).

  • Q : "Pourquoi init: true plutot qu'installer tini comme dans wakdo-app ?" R : Difference de timing. Pour wakdo-app, Session 3 a pose tini explicitement parce que c'etait la maniere "showcase" — montrer que je sais l'installer. Pour wakdo-cron, c'est apparu en correctif au smoke test ; init: true est plus court (1 ligne vs Dockerfile + ENTRYPOINT) et atteint le meme resultat car Docker injecte un init binaire compatible (en realite docker-init, qui est tini repackage). Coherence stylistique a faire dans une iteration ulterieure si jugee importante.


Points d'amelioration conscients

  • Redirect HTTP -> HTTPS : retourne 404 au lieu de 301. Sera traite dans une branche feat/infra-polish separee, pas avant validation que ce n'est pas un faux probleme (l'usage demo se fera via lien HTTPS direct).
  • Coherence init : wakdo-app installe tini explicitement, wakdo-cron utilise init: true. Acceptable en l'etat (les deux marchent), a unifier en passant l'un ou l'autre style si on touche au compose.
  • docker network prune pas execute : 3 reseaux orphelins detectes (traefik-net, crm-chirurgien-network, acquagest-prod_acquaprocess_staging) consomment des subnets sans usage. Pas mon role d'effacer des reseaux que je n'ai pas crees ; signaler a l'admin de l'hote.
  • Pas de DNS-01 + wildcard cert : Traefik utilise HTTP-01 par FQDN. DNS-01 + wildcard ferait un seul cert pour *.stark.a3n.fr et reduirait les renouvellements LE. Mais c'est une optim de l'infra hote, hors scope projet examen.
  • ./src/ vide : les FQDN repondent 403 sur /. Resolution prevue Phase P2 (creation des stubs public/borne/index.html et public/admin/index.php).

Liens vers artefacts

  • Commits :
    • 4edabf2docs: switch project FQDN from acadenice.fr to stark.a3n.fr
    • d9890cfchore(docker): smoke test fixes for stack startup and healthz
  • Fichiers principaux modifies :
    • docker-compose.yml (subnet IPAM + init: true cron)
    • docker/apache/Dockerfile (COPY healthz vers htdocs)
    • docker/apache/vhost.conf (Alias healthz)
    • docker/apache/healthz.txt (nouveau)
    • README.md, docs/PROJECT_CONTEXT.md (FQDN switch)
  • Documentation associee :
    • docs/journal/2026-04-24--infra-docker.md (session precedente, decisions architecturales)
    • docs/notes/docker-volumes-vs-bind-mounts.md (note technique perso, complement)