deploiement : deux attentes de premier demarrage, et trois defauts leves

`idm-01` a prouve le routage inter-zone dans le VRF (10.27.19.21 -> 10.27.17.11,
deux passerelles anycast) et leve trois defauts qu'aucun devis ne pouvait
montrer :

1. Le handler de `client_pki` rechargeait `slapd` avant son installation —
   consequence directe de l'ordre retabli. Un consommateur absent n'est pas une
   erreur ; un vrai echec de rechargement reste fatal.

2. `resoudre_base` ne trouvait aucune base de portee `application` : Keycloak
   declare `consommateur: keycloak`, le role cherchait `serveur_keycloak`. Le
   lien est declare dans applications.yml — on le suit au lieu de retirer un
   prefixe a la main.

3. Keycloak attend PostgreSQL, pas encore deploye. Pas un defaut : `deployer`
   ne connait que l'ordre intra-hote, l'ordre inter-hotes est celui de `site`.

Et deux attentes, sans lesquelles on ne peut pas enchainer creation et
deploiement — donc sans lesquelles `myDay` ne reconstruit pas seul : SSH, puis
cloud-init et les maj automatiques. `creer-vm` rend desormais une VM PRETE.
`lock_timeout: 300` sur chaque tache apt complete l'attente : le verrou peut
etre repris ENTRE deux taches, ce qu'une verification en amont ne previent pas.

Defaut dans l'attente elle-meme : `unattended-upgrades` est un demon, toujours
actif — l'inclure rendait la condition impossible a satisfaire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-07 09:30:48 -04:00
parent f18715bc78
commit 41413c7f04
6 changed files with 152 additions and 4 deletions

View file

@ -2,6 +2,57 @@
## 2026-08-06 — le chemin nord-sud devient dérivable
### Trois défauts que seul un vrai déploiement pouvait montrer
`idm-01` — première VM d'une autre zone de sécurité (`t17iden`) — a prouvé le **routage
inter-zone** dans le VRF : `10.27.19.21` joint `10.27.17.11`, à travers deux passerelles
anycast. Puis elle a levé trois défauts, tous invisibles jusqu'à ce qu'on déploie pour de vrai.
**Le handler de `client_pki` rechargeait un service pas encore installé.** Conséquence directe
de l'ordre rétabli : `client_pki` s'exécute **avant** les services — ils ont besoin du
certificat — et son handler tentait `systemctl reload slapd`. Un consommateur absent n'est pas
une erreur : il prendra le certificat déjà posé à son installation. Le silence est resserré sur
ce cas précis ; un vrai échec de rechargement reste fatal, sinon un service servirait un
certificat périmé sans que personne ne l'apprenne.
**`resoudre_base` ne trouvait aucune base de portée `application`.** Le registre accepte deux
portées : `groupe` désigne un groupe opérationnel, `application` une application du plan.
Keycloak déclare `consommateur: keycloak` — valide, le validateur l'accepte — mais le rôle
cherchait `serveur_keycloak`. Le lien entre les deux était **déclaré** (`applications.yml`
nomme le `groupe` de chaque application) : on le suit désormais, plutôt que de retirer un
préfixe à la main. Une convention de nommage se contredit un jour ; une déclaration se corrige.
**Keycloak attend une base qui n'existe pas encore.** `data-sql-01` n'est pas déployée : le
service démarre, se connecte, échoue. Ce n'est pas un défaut — `make deployer HOTE=x` ne connaît
que l'ordre **intra-hôte**. L'ordre inter-hôtes existe (`couches-deploiement.yml` place
`serveur_postgresql` avant les applications) et c'est `make site` qui l'exploite.
### Deux attentes, sans lesquelles `make myDay` ne peut pas reconstruire
Enchaîner `creer-vm` puis `deployer` échouait presque toujours sur une machine neuve, pour deux
raisons distinctes :
- **SSH n'est pas levé** quand Proxmox rend la main. Le symptôme trompe : à travers la
frontière le TCP s'établit (SYN proxy) et l'échec se lit *« timed out during banner
exchange »*.
- **Le verrou dpkg** est tenu par les mises à jour automatiques de Debian, par vagues, pendant
plusieurs minutes après le premier démarrage.
`_attendre-hote` attend les deux, et `creer-vm` rend désormais une VM **prête** plutôt que
seulement démarrée. `ATTENTE_HOTE` (600 s) borne l'attente : dépasser reste un échec, pour
qu'une panne ne devienne pas une attente infinie.
**Deux couches, pas une.** Le premier essai vérifiait le verrou avec `fuser` — un instantané.
Il était libre au test, repris juste après. Chaque tâche `apt` de `common_packages` porte donc
`lock_timeout: 300` : le Makefile attend la fin des vagues, le rôle survit à une vague qui
repart **entre** deux tâches.
Et un défaut dans l'attente elle-même : `unattended-upgrades.service` est un **démon**
(`Type=simple`), toujours `active`. L'inclure dans la condition la rendait impossible à
satisfaire — elle échouait au délai, systématiquement. Seules les unités `apt-daily*` sont des
one-shot ; c'est le verrou qui dit si dpkg est occupé.
### `make deployer` ignorait l'ordre des couches
`client_metrique` échouait sur les deux premières VM : il exige le certificat TLS du

View file

@ -666,6 +666,12 @@ creer-vm: _instance-requise
CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \
DEMARRER="$(DEMARRER)" \
CLONE_COMPLET="$(CLONE_COMPLET)"
@# `creer-vm` rend une VM PRETE, pas seulement demarree : sans cette attente,
@# enchainer `creer-vm` puis `deployer` echoue presque toujours sur une machine
@# neuve. C'est ce qui separe une suite de commandes d'une reconstruction.
@if [[ "$(ATTENDRE)" != "false" ]]; then \
$(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \
fi
inventaire-verifier: ansible-runtime _instance-requise
ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null
@ -756,7 +762,50 @@ nettoyer-modele: ansible-runtime
fi
ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_NETTOYER_MODELE) -e template_cleanup_confirm=true
.PHONY: _verifier-acces-hote _verifier-privileges-hote faits verifier-hote
.PHONY: _verifier-acces-hote _verifier-privileges-hote _attendre-hote faits verifier-hote
# Deux ATTENTES ACTIVES, sans lesquelles on ne peut pas enchainer creation et
# deploiement — donc sans lesquelles `make myDay` ne peut pas reconstruire seul.
#
# 1. SSH. `make creer-vm` rend la main des que Proxmox a DEMARRE la VM, pas quand elle
# repond. Verifier l'acces aussitot echoue presque toujours sur une machine neuve —
# et le symptome trompe : a travers la frontiere, le TCP s'etablit (SYN proxy) et
# l'echec se lit « Connection timed out during banner exchange ».
#
# 2. Le verrou dpkg. L'image Debian lance ses propres mises a jour au premier
# demarrage et tient `/var/lib/dpkg/lock-frontend` plusieurs minutes. Le premier
# `apt` d'Ansible echoue alors sur un verrou, pas sur une vraie erreur.
#
# `unattended-upgrades.service` est volontairement ABSENT de la condition : c'est un
# DEMON (Type=simple), toujours `active`. L'y inclure rendait l'attente impossible a
# satisfaire — elle echouait au bout du delai, systematiquement. Seules `apt-daily*`
# sont des one-shot, et c'est le VERROU qui dit si dpkg est reellement occupe.
#
# Les deux sont des COURSES de premier demarrage, pas des defauts de conception. On les
# attend au lieu de les subir. ATTENTE_HOTE (secondes) borne chacune : depasser le
# delai reste un echec, pour ne pas transformer une panne en attente infinie.
ATTENTE_HOTE ?= 600
_attendre-hote: ansible-runtime
@set -e; \
if [[ -z "$(LIMITE)" ]]; then printf '%s\n' 'Refus: LIMITE requis.'; exit 2; fi; \
fin=$$(( SECONDS + $(ATTENTE_HOTE) )); \
printf '%s' "Attente de SSH sur $(LIMITE) "; \
until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false >/dev/null 2>&1; do \
if (( SECONDS > fin )); then printf '%s\n' " ECHEC: injoignable apres $(ATTENTE_HOTE)s."; exit 4; fi; \
printf '.'; sleep 5; \
done; \
printf '%s\n' " ok"; \
printf '%s' "Attente de cloud-init et des maj automatiques "; \
until ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m shell -a \
'cloud-init status --wait >/dev/null 2>&1; \
! systemctl is-active --quiet apt-daily.service apt-daily-upgrade.service \
&& ! fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1' >/dev/null 2>&1; do \
if (( SECONDS > fin )); then printf '%s\n' " ECHEC: dpkg toujours occupe apres $(ATTENTE_HOTE)s."; exit 4; fi; \
printf '.'; sleep 10; \
done; \
printf '%s\n' " libere"
_verifier-acces-hote: ansible-runtime
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false

View file

@ -6,6 +6,12 @@
# Apres une re-emission du cert (ex: nouveau SAN), recharger les services qui le
# servent (nginx, postfix, slapd...) sinon ils continuent de servir l'ancien cert.
#
# Un consommateur PAS ENCORE INSTALLE n'est pas une erreur : `client_pki` s'execute
# AVANT les services (couches de deploiement), donc `slapd` n'existe pas quand l'hote
# recoit son premier certificat. Le service prendra le cert deja pose a son
# installation. On ne tait que ce cas precis — un vrai echec de rechargement doit
# rester fatal, sinon un service continuerait a servir un certificat perime en silence.
- name: Recharger les consommateurs du cert
ansible.builtin.systemd:
name: "{{ item }}"
@ -13,6 +19,10 @@
loop: "{{ client_pki_reload_services }}"
loop_control:
label: "{{ item }}"
register: client_pki_rechargements
failed_when:
- client_pki_rechargements is failed
- "'Could not find the requested service' not in (client_pki_rechargements.msg | default(''))"
when:
- not ansible_check_mode
- client_pki_reload_services | length > 0

View file

@ -46,3 +46,7 @@ common_packages_list:
- unattended-upgrades
- apt-listchanges
- needrestart
# Attente du verrou dpkg (secondes). Genereux : au premier demarrage, les mises a
# jour automatiques de Debian le tiennent par vagues pendant plusieurs minutes.
common_packages_lock_timeout: 300

View file

@ -1,18 +1,29 @@
---
# `lock_timeout` sur CHAQUE tache apt : au premier demarrage, l'image Debian lance ses
# propres mises a jour (apt-daily, unattended-upgrades) et tient le verrou dpkg par
# vagues. Sans attente, la premiere tache echoue sur un verrou — pas sur une vraie
# erreur — et le deploiement d'une VM neuve devient un tirage au sort.
#
# Attendre ici plutot que seulement avant le playbook : le verrou peut etre repris
# ENTRE deux taches, ce qu'une verification ponctuelle en amont ne peut pas empecher.
- name: Mettre à jour le cache APT
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
lock_timeout: "{{ common_packages_lock_timeout }}"
- name: Appliquer les mises à jour disponibles
ansible.builtin.apt:
upgrade: full
lock_timeout: "{{ common_packages_lock_timeout }}"
- name: Installer les paquets communs
ansible.builtin.apt:
name: "{{ common_packages_list }}"
state: present
lock_timeout: "{{ common_packages_lock_timeout }}"
- name: Supprimer les dépendances devenues inutiles
ansible.builtin.apt:
autoremove: true
lock_timeout: "{{ common_packages_lock_timeout }}"

View file

@ -14,12 +14,33 @@
ansible.builtin.include_vars:
file: "{{ setops_plan_dir }}/bases-donnees.yml"
- name: Résoudre l'entrée de base (par consommateur)
# Le `consommateur` d'une base n'est PAS toujours un groupe : le validateur du registre
# (inventory_rules.py) accepte deux portées — `groupe` désigne un groupe opérationnel,
# `application` désigne une APPLICATION de plan/applications.yml. Chercher uniquement le
# nom du groupe faisait donc échouer toute base de portée `application` : Keycloak
# déclare `consommateur: keycloak`, le rôle demandait `serveur_keycloak`.
#
# Le lien entre les deux est DÉCLARÉ — chaque application nomme son `groupe`. On le suit
# plutôt que de retirer un préfixe à la main : une convention de nommage se contredit un
# jour, une déclaration se corrige.
- name: Charger le registre des applications (lien application → groupe)
ansible.builtin.include_vars:
file: "{{ setops_plan_dir }}/applications.yml"
- name: Établir les noms sous lesquels ce groupe peut être consommateur
ansible.builtin.set_fact:
resoudre_base_noms: >-
{{ [resoudre_base_groupe] + ((applications | default({})) | dict2items
| selectattr('value.groupe', 'defined')
| selectattr('value.groupe', 'equalto', resoudre_base_groupe)
| map(attribute='key') | list) }}
- name: Résoudre l'entrée de base (par consommateur, groupe ou application)
ansible.builtin.set_fact:
resoudre_base_entree: >-
{{ ((bases_donnees | default({})) | dict2items
| selectattr('value.consommateur', 'defined')
| selectattr('value.consommateur', 'equalto', resoudre_base_groupe)
| selectattr('value.consommateur', 'in', resoudre_base_noms)
| map(attribute='value') | list | first) | default({}) }}
- name: Exiger une entrée de registre et son secret
@ -28,7 +49,9 @@
- resoudre_base_entree.base is defined
- resoudre_base_entree.proprietaire is defined
- resoudre_base_entree.secret is defined
fail_msg: "Aucune base avec consommateur '{{ resoudre_base_groupe }}' dans instance/plan/bases-donnees.yml."
fail_msg: >-
Aucune base dont le `consommateur` soit l'un de {{ resoudre_base_noms }}
dans instance/plan/bases-donnees.yml (portée `groupe` ou `application`).
- name: Résoudre le mot de passe de base (Vault)
ansible.builtin.set_fact: