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:
parent
f18715bc78
commit
41413c7f04
6 changed files with 152 additions and 4 deletions
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -2,6 +2,57 @@
|
||||||
|
|
||||||
## 2026-08-06 — le chemin nord-sud devient dérivable
|
## 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
|
### `make deployer` ignorait l'ordre des couches
|
||||||
|
|
||||||
`client_metrique` échouait sur les deux premières VM : il exige le certificat TLS du
|
`client_metrique` échouait sur les deux premières VM : il exige le certificat TLS du
|
||||||
|
|
|
||||||
51
Makefile
51
Makefile
|
|
@ -666,6 +666,12 @@ creer-vm: _instance-requise
|
||||||
CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \
|
CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \
|
||||||
DEMARRER="$(DEMARRER)" \
|
DEMARRER="$(DEMARRER)" \
|
||||||
CLONE_COMPLET="$(CLONE_COMPLET)"
|
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
|
inventaire-verifier: ansible-runtime _instance-requise
|
||||||
ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null
|
ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null
|
||||||
|
|
@ -756,7 +762,50 @@ nettoyer-modele: ansible-runtime
|
||||||
fi
|
fi
|
||||||
ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_NETTOYER_MODELE) -e template_cleanup_confirm=true
|
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
|
_verifier-acces-hote: ansible-runtime
|
||||||
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false
|
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m ping -e ansible_become=false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,12 @@
|
||||||
|
|
||||||
# Apres une re-emission du cert (ex: nouveau SAN), recharger les services qui le
|
# 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.
|
# 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
|
- name: Recharger les consommateurs du cert
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
name: "{{ item }}"
|
name: "{{ item }}"
|
||||||
|
|
@ -13,6 +19,10 @@
|
||||||
loop: "{{ client_pki_reload_services }}"
|
loop: "{{ client_pki_reload_services }}"
|
||||||
loop_control:
|
loop_control:
|
||||||
label: "{{ item }}"
|
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:
|
when:
|
||||||
- not ansible_check_mode
|
- not ansible_check_mode
|
||||||
- client_pki_reload_services | length > 0
|
- client_pki_reload_services | length > 0
|
||||||
|
|
|
||||||
|
|
@ -46,3 +46,7 @@ common_packages_list:
|
||||||
- unattended-upgrades
|
- unattended-upgrades
|
||||||
- apt-listchanges
|
- apt-listchanges
|
||||||
- needrestart
|
- 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
|
||||||
|
|
|
||||||
|
|
@ -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
|
- name: Mettre à jour le cache APT
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
update_cache: true
|
update_cache: true
|
||||||
cache_valid_time: 3600
|
cache_valid_time: 3600
|
||||||
|
lock_timeout: "{{ common_packages_lock_timeout }}"
|
||||||
|
|
||||||
- name: Appliquer les mises à jour disponibles
|
- name: Appliquer les mises à jour disponibles
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
upgrade: full
|
upgrade: full
|
||||||
|
lock_timeout: "{{ common_packages_lock_timeout }}"
|
||||||
|
|
||||||
- name: Installer les paquets communs
|
- name: Installer les paquets communs
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
name: "{{ common_packages_list }}"
|
name: "{{ common_packages_list }}"
|
||||||
state: present
|
state: present
|
||||||
|
lock_timeout: "{{ common_packages_lock_timeout }}"
|
||||||
|
|
||||||
- name: Supprimer les dépendances devenues inutiles
|
- name: Supprimer les dépendances devenues inutiles
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
autoremove: true
|
autoremove: true
|
||||||
|
lock_timeout: "{{ common_packages_lock_timeout }}"
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,33 @@
|
||||||
ansible.builtin.include_vars:
|
ansible.builtin.include_vars:
|
||||||
file: "{{ setops_plan_dir }}/bases-donnees.yml"
|
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:
|
ansible.builtin.set_fact:
|
||||||
resoudre_base_entree: >-
|
resoudre_base_entree: >-
|
||||||
{{ ((bases_donnees | default({})) | dict2items
|
{{ ((bases_donnees | default({})) | dict2items
|
||||||
| selectattr('value.consommateur', 'defined')
|
| selectattr('value.consommateur', 'defined')
|
||||||
| selectattr('value.consommateur', 'equalto', resoudre_base_groupe)
|
| selectattr('value.consommateur', 'in', resoudre_base_noms)
|
||||||
| map(attribute='value') | list | first) | default({}) }}
|
| map(attribute='value') | list | first) | default({}) }}
|
||||||
|
|
||||||
- name: Exiger une entrée de registre et son secret
|
- name: Exiger une entrée de registre et son secret
|
||||||
|
|
@ -28,7 +49,9 @@
|
||||||
- resoudre_base_entree.base is defined
|
- resoudre_base_entree.base is defined
|
||||||
- resoudre_base_entree.proprietaire is defined
|
- resoudre_base_entree.proprietaire is defined
|
||||||
- resoudre_base_entree.secret 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)
|
- name: Résoudre le mot de passe de base (Vault)
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue