playbooks/site.yml ordonne les couches pour n importe quelle instance. Mais deployer-tout ne vise que l inventaire d un locataire, et le site n avait que site-appliquer GROUPE=<un seul>. Un hebergeur qu on ne peut remonter qu en enchainant onze groupes de memoire n est pas reconstructible : il est reparable par quelqu un qui se souvient. Et quatre gardes que --check rendait folles, toutes de la meme famille : une garde qui compare contre ce qu une tache du meme play vient de produire n a rien a dire tant que rien n a ete ecrit. Sept machines en echec sur un site sain, en suivant le conseil du Makefile lui-meme. Apres correction : 7/7, 0 echec, de 174 a 309 taches par machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
260 lines
13 KiB
YAML
260 lines
13 KiB
YAML
---
|
|
# CLOUD-INIT REECRIT /etc/hosts A CHAQUE DEMARRAGE (2026-08-24).
|
|
#
|
|
# Le gabarit dore laisse `manage_etc_hosts: true`. A chaque boot, cloud-init regenere
|
|
# /etc/hosts depuis son propre modele — et efface le plancher de resolution que ce role
|
|
# vient de poser. La machine perd alors la seule facon qu'elle a de nommer ses voisines
|
|
# sans DNS, et personne ne le voit : rien n'echoue tant qu'on ne demande pas un nom.
|
|
#
|
|
# CONSTATE SUR infra-dns-01 : redemarre a 18h53 lors d'un deplacement de disque, il a
|
|
# perdu ses six entrees de flotte. Le defaut est apparu deux jours plus tard, sous la
|
|
# forme d'un `apt update` qui n'arrivait plus a resoudre le cache d'artefacts — un
|
|
# message qui ne parlait pas du tout du vrai probleme.
|
|
#
|
|
# On le desactive par un fragment plutot qu'en editant cloud.cfg : le gabarit reste
|
|
# intact, et `apt purge cloud-init` ou une remise a zero rend la machine a son etat
|
|
# d'origine.
|
|
#
|
|
# `zz-` ET NON `99-` (2026-08-25). Cloud-init lit `cloud.cfg.d` dans l'ordre LEXICAL, et
|
|
# le DERNIER a parler gagne. Notre fragment s'appelait `99-setops-hosts.cfg` ; le gabarit
|
|
# dore en porte un autre, `99_chezlepro.cfg`, qui pose `manage_etc_hosts: true`. Or `-`
|
|
# vaut 0x2D et `_` vaut 0x5F : le fragment du gabarit etait lu APRES le notre, et gagnait.
|
|
#
|
|
# Mesure du 2026-08-25 : les cinq machines du site redemarrees avaient perdu tout leur
|
|
# plancher malgre notre fragment — il etait bien la, simplement sans effet. Un fichier
|
|
# present et ignore est le pire des deux mondes : il rassure sans rien garantir.
|
|
#
|
|
# `zz-` passe apres tout prefixe numerique ET apres `_`. On ne cherche donc plus a
|
|
# connaitre les fragments des autres : on parle en dernier, quels qu'ils soient. La garde
|
|
# plus bas verifie que c'est encore vrai sur la machine.
|
|
- name: Empêcher cloud-init de réécrire le plancher à chaque démarrage
|
|
ansible.builtin.copy:
|
|
dest: /etc/cloud/cloud.cfg.d/zz-setops-hosts.cfg
|
|
content: |
|
|
# GÉNÉRÉ par Set-OPS (rôle hosts_statiques). NE PAS éditer à la main.
|
|
# /etc/hosts est le PLANCHER DE RÉSOLUTION de l'écosystème, dérivé de l'inventaire.
|
|
# Laisser cloud-init le régénérer au démarrage l'effacerait en silence.
|
|
manage_etc_hosts: false
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
when: hosts_statiques_actif | bool
|
|
|
|
# CE FRAGMENT EST DESORMAIS LE SEUL A DECIDER (2026-08-25).
|
|
#
|
|
# `cloud_init` en ecrivait un second (`99_setops.cfg`) qui posait `manage_etc_hosts: true`.
|
|
# Dans `cloud.cfg.d` l'ordre est LEXICAL : `-` (0x2D) passe avant `_` (0x5F), donc le
|
|
# fragment de `cloud_init` etait lu EN DERNIER et gagnait. Le plancher etait efface a
|
|
# chaque demarrage, et ca ne se voyait qu'au redemarrage suivant — sous la forme d'un
|
|
# `apt update` qui ne resolvait plus, ou d'un nom de service qui ne pointait nulle part.
|
|
#
|
|
# LE RETRAIT DOIT ETRE AUSSI EXPLICITE QUE LA POSE. Sans la tache ci-dessous, desactiver
|
|
# `hosts_statiques` laissait le fragment en place : la machine gardait un /etc/hosts fige
|
|
# que plus personne ne tenait a jour, et cloud-init n'avait plus le droit de le reprendre.
|
|
# Un etat que ni l'un ni l'autre n'assume.
|
|
- name: Rendre /etc/hosts à cloud-init quand le plancher est désactivé
|
|
ansible.builtin.file:
|
|
path: /etc/cloud/cloud.cfg.d/zz-setops-hosts.cfg
|
|
state: absent
|
|
when: not (hosts_statiques_actif | bool)
|
|
|
|
# L'ancien nom, qui ne parlait pas en dernier. Le laisser en place n'aurait rien casse,
|
|
# mais deux fichiers disant la meme chose sont un desaccord qui attend son heure.
|
|
- name: Retirer l'ancien fragment, qui ne parlait pas en dernier
|
|
ansible.builtin.file:
|
|
path: /etc/cloud/cloud.cfg.d/99-setops-hosts.cfg
|
|
state: absent
|
|
|
|
# Alias d'exposition (best-effort) : dérivés du plan si celui-ci est disponible.
|
|
- name: Vérifier la présence des registres du plan (sur le nœud de contrôle)
|
|
ansible.builtin.stat:
|
|
path: "{{ item }}"
|
|
register: hosts_statiques_plan
|
|
delegate_to: localhost # les registres du plan vivent sur le nœud de contrôle
|
|
become: false # lire un fichier ne nécessite pas de privilèges
|
|
loop:
|
|
- "{{ setops_plan_dir }}/applications.yml"
|
|
- "{{ setops_plan_dir }}/domaines.yml"
|
|
loop_control:
|
|
label: "{{ item | basename }}"
|
|
when:
|
|
- hosts_statiques_actif | bool
|
|
- hosts_statiques_publier_expositions | bool
|
|
- setops_plan_dir is defined
|
|
|
|
# CHAQUE REGISTRE EST CHARGÉ POUR LUI-MÊME (2026-08-25).
|
|
#
|
|
# La condition exigeait que les DEUX fichiers existent (`length == 2`). Un tout-ou-rien
|
|
# sans raison : `domaines.yml` déclare les domaines PUBLICS, et un écosystème peut
|
|
# légitimement n'en avoir aucun — c'est le cas du SITE, dont tous les services sont
|
|
# internes.
|
|
#
|
|
# Conséquence : `applications.yml` n'était pas chargé non plus, la dérivation rendait une
|
|
# liste vide, et le plancher s'écrivait SANS AUCUN ALIAS — sans erreur. Le certificat de
|
|
# la forge portait `forge.genese.internal` et ce nom ne résolvait nulle part.
|
|
#
|
|
# On charge donc ce qui existe, fichier par fichier. Un registre absent n'est pas une
|
|
# faute ; il est simplement absent.
|
|
- name: Charger les registres applications et domaines (si présents)
|
|
ansible.builtin.include_vars:
|
|
file: "{{ item.item }}"
|
|
loop: "{{ hosts_statiques_plan.results | default([]) }}"
|
|
loop_control:
|
|
label: "{{ item.item | basename }}"
|
|
when:
|
|
- hosts_statiques_actif | bool
|
|
- hosts_statiques_publier_expositions | bool
|
|
- item.stat.exists | default(false)
|
|
|
|
- name: Dériver les alias d'exposition (FQDN exposés → edge qui les sert)
|
|
ansible.builtin.set_fact:
|
|
hosts_statiques_expositions: >-
|
|
{{ {'applications': applications | default({})}
|
|
| expositions_des_applications({'domaines_publics': domaines_publics | default({})}) }}
|
|
when:
|
|
- hosts_statiques_actif | bool
|
|
- hosts_statiques_publier_expositions | bool
|
|
|
|
- name: Générer /etc/hosts depuis l'inventaire (plancher de résolution, indépendant du DNS)
|
|
ansible.builtin.template:
|
|
src: hosts.j2
|
|
dest: /etc/hosts
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
when: hosts_statiques_actif | bool
|
|
|
|
# LE SEUL ENDROIT QUI TIENNE AU REDEMARRAGE (2026-08-25).
|
|
#
|
|
# Le fragment `cloud.cfg.d` ne suffit pas, et c'est mesure : Proxmox inscrit
|
|
# `manage_etc_hosts: true` dans la USER-DATA de son lecteur cloud-init, et la user-data
|
|
# prime sur tout fragment de `/etc/cloud/cloud.cfg.d`. Le plancher etait donc efface a
|
|
# chaque demarrage malgre le fragment — d'abord `99-setops-hosts.cfg`, puis son
|
|
# remplacant `zz-` : renommer pour parler en dernier ne changeait rien, puisque le dernier
|
|
# mot n'appartenait pas a ce repertoire.
|
|
#
|
|
# Ce que cloud-init regenere, il le regenere DEPUIS CE GABARIT. On le pose donc avec le
|
|
# meme contenu que /etc/hosts : le fichier de la machine et le gabarit qui le reecrira
|
|
# disent la meme chose, quelle que soit la valeur de `manage_etc_hosts`.
|
|
#
|
|
# Le gabarit d'origine le documente lui-meme : « make changes to the master file in
|
|
# /etc/cloud/templates/hosts.debian.tmpl ».
|
|
# ... MAIS SEULEMENT TANT QUE CLOUD-INIT EST LA (2026-09-10).
|
|
#
|
|
# D-85 fait RETIRER cloud-init par `serveur_durci`. Le repertoire des gabarits maitres
|
|
# part avec lui, et cette tache echoue alors sur :
|
|
#
|
|
# Destination directory /etc/cloud/templates does not exist
|
|
#
|
|
# Or l'echec n'a aucun sens : ce gabarit ne sert qu'a une chose, survivre a la reecriture
|
|
# de `/etc/hosts` par cloud-init au demarrage. Plus de cloud-init, plus de reecriture —
|
|
# le plancher tient tout seul, ce qui EST le resultat recherche par D-85.
|
|
#
|
|
# CE QUE CA A COUTE (reconstruction du 2026-09-10). `_amorcer-socle` monte `infra-pki-01`
|
|
# et `infra-dns-01` EN ENTIER d'abord, durcissement compris : cloud-init y est donc deja
|
|
# parti quand la flotte rejoue `hosts_statiques`. La tache echouait, l'hote sortait du
|
|
# play — et TOUT ce qui suivait dans ce play n'etait jamais pose, dont `icinga-ca.crt`.
|
|
# Leur porteur de sante tournait ensuite normalement et chaque rapport echouait sur
|
|
# `curl: (77)`, en silence. Deux machines ont supervise dans le vide sans que rien ne le
|
|
# dise : un echec bruyant au bon endroit avait produit une panne muette ailleurs.
|
|
- name: Le gabarit maître de cloud-init existe-t-il encore ?
|
|
ansible.builtin.stat:
|
|
path: /etc/cloud/templates
|
|
register: hosts_statiques_gabarits_cloud_init
|
|
when: hosts_statiques_actif | bool
|
|
|
|
- name: Poser le plancher dans le gabarit maître de cloud-init (il survit au démarrage)
|
|
ansible.builtin.template:
|
|
src: hosts.debian.tmpl.j2
|
|
dest: /etc/cloud/templates/hosts.debian.tmpl
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
when:
|
|
- hosts_statiques_actif | bool
|
|
- ansible_facts.os_family | default('') == 'Debian'
|
|
- hosts_statiques_gabarits_cloud_init.stat.isdir | default(false)
|
|
|
|
# LA GARDE MESURE LE FAIT QUI COMPTE (2026-08-25).
|
|
#
|
|
# La garde precedente verifiait que notre fragment `cloud.cfg.d` parlait en dernier. C'est
|
|
# vrai — et sans effet, puisque la user-data de la source de donnees prime sur ce
|
|
# repertoire tout entier. Une garde qui mesure la mauvaise chose est pire qu'aucune : elle
|
|
# affirme « conforme » pendant que le defaut opere.
|
|
#
|
|
# Ce qui compte est que le fichier ET le gabarit qui le reecrira portent les MEMES
|
|
# entrees. On les compare donc, ligne d'adresse par ligne d'adresse.
|
|
- name: Relever les entrées du plancher et celles du gabarit maître
|
|
ansible.builtin.shell:
|
|
cmd: >-
|
|
set -o pipefail;
|
|
for f in /etc/hosts /etc/cloud/templates/hosts.debian.tmpl; do
|
|
printf '%s=' "$f";
|
|
grep -cE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[[:space:]]' "$f" 2>/dev/null || printf '0';
|
|
printf '\n';
|
|
done
|
|
executable: /bin/bash
|
|
register: hosts_statiques_accord
|
|
changed_when: false
|
|
# LIRE N'EST PAS CHANGER, ET `--check` NE DOIT PAS RENDRE LA GARDE FOLLE (2026-09-14).
|
|
#
|
|
# Sans `check_mode: false`, cette commande est SAUTEE en mode idempotent. La garde qui
|
|
# suit compare alors du VIDE et refuse, sur les sept machines a la fois :
|
|
#
|
|
# ne portent pas le meme nombre d'entrees ()
|
|
#
|
|
# Les parentheses vides sont tout le diagnostic — il n'y avait aucun compte a comparer.
|
|
# Le Makefile CONSEILLE pourtant ce mode (« tester d abord en idempotent »), donc un
|
|
# essai a blanc rendait sept faux echecs a qui suivait le conseil.
|
|
#
|
|
# Cette tache ne fait que LIRE deux fichiers et compter des lignes. La faire tourner en
|
|
# `--check` ne modifie rien et rend au mode idempotent ce qu'il promet : dire ce qui
|
|
# changerait, pas inventer des pannes.
|
|
check_mode: false
|
|
when:
|
|
- hosts_statiques_actif | bool
|
|
- ansible_facts.os_family | default('') == 'Debian'
|
|
# Rien a relever quand le gabarit maitre n'existe plus (cloud-init retire, D-85).
|
|
- hosts_statiques_gabarits_cloud_init.stat.isdir | default(false)
|
|
|
|
- name: Refuser que le plancher et son gabarit maître divergent
|
|
ansible.builtin.assert:
|
|
that:
|
|
- (hosts_statiques_accord.stdout_lines | select('search', '^/etc/hosts=')
|
|
| first | default('=0')).split('=')[1] | int > 1
|
|
- (hosts_statiques_accord.stdout_lines | select('search', '^/etc/hosts=')
|
|
| first | default('=0')).split('=')[1]
|
|
== (hosts_statiques_accord.stdout_lines | select('search', 'hosts.debian.tmpl=')
|
|
| first | default('=-1')).split('=')[1]
|
|
fail_msg: >-
|
|
Le plancher et le gabarit maître de cloud-init ne portent pas le même nombre
|
|
d'entrées ({{ hosts_statiques_accord.stdout_lines | default([]) | join(' | ') }}).
|
|
Cloud-init régénère /etc/hosts depuis le gabarit à chaque démarrage : la divergence
|
|
ne se verrait qu'au prochain redémarrage, et se manifesterait ailleurs — un `apt`
|
|
qui ne résout plus, un certificat dont le nom ne pointe nulle part.
|
|
success_msg: >-
|
|
Plancher et gabarit maître d'accord
|
|
({{ hosts_statiques_accord.stdout_lines | default([]) | join(' | ') }}).
|
|
when:
|
|
- hosts_statiques_actif | bool
|
|
- ansible_facts.os_family | default('') == 'Debian'
|
|
# LA GARDE SUIT SON OBJET. Elle protege le plancher d'une REECRITURE par cloud-init ;
|
|
# sans cloud-init, il n'y a plus de reecriture et plus de gabarit a comparer. La
|
|
# laisser tourner ferait echouer sur « 34 contre 0 » — c'est-a-dire accuser une
|
|
# divergence la ou il n'y a plus qu'un seul fichier. Une garde qui survit a ce qu'elle
|
|
# gardait ne mesure plus rien : elle invente.
|
|
- hosts_statiques_gabarits_cloud_init.stat.isdir | default(false)
|
|
# NI EN MODE IDEMPOTENT, POUR LA MEME RAISON (2026-09-14). Cette garde compare l'etat
|
|
# que la tache « Poser le plancher dans le gabarit maitre » vient d'etablir vingt
|
|
# lignes plus haut. En `--check`, cette ecriture est SIMULEE : le gabarit sur disque
|
|
# porte encore l'etat d'AVANT, et la garde compare donc l'avant a l'apres.
|
|
#
|
|
# /etc/hosts=13 | /etc/cloud/templates/hosts.debian.tmpl=2
|
|
#
|
|
# Sept machines en echec sur un essai a blanc, alors que le deploiement reel passe.
|
|
# Le Makefile CONSEILLE ce mode — « tester d abord en idempotent » — donc suivre le
|
|
# conseil fabriquait sept fausses pannes.
|
|
#
|
|
# Une assertion qui porte sur une post-condition de son propre play n'a rien a dire
|
|
# tant que ce play n'a rien ecrit.
|
|
- not ansible_check_mode
|