Set-OPS-Public/roles/hosts_statiques/tasks/main.yml
Daniel Allaire a74e35bf21
Some checks are pending
verifier / verifier (push) Waiting to run
site : le plancher survit au redemarrage, et la zone dit les vraies adresses
Le decoupage du site en quatre zones a deplace cinq machines. Ni le plancher
/etc/hosts ni la zone DNS n'avaient suivi. Quatre defauts, tous dans le moteur.

LE PLANCHER ETAIT EFFACE A CHAQUE DEMARRAGE — ET LE PREMIER CORRECTIF N'EN
ETAIT PAS UN.

`hosts_statiques` posait `99-setops-hosts.cfg` avec `manage_etc_hosts: false`,
pendant que `cloud_init` posait `99_setops.cfg` avec `true`. Dans `cloud.cfg.d`
l'ordre est LEXICAL et le dernier gagne : `-` vaut 0x2D, `_` vaut 0x5F. On a
donc retire la cle de `cloud_init` — le role qui POSSEDE le fichier decide —
puis renomme notre fragment `zz-` pour passer apres le `99_chezlepro.cfg` du
gabarit dore.

Et ca ne suffisait toujours pas. Redemarrage d'epreuve : plancher encore efface.
La cause reelle est ailleurs — Proxmox inscrit `manage_etc_hosts: true` dans la
USER-DATA de son lecteur cloud-init, et la user-data prime sur `cloud.cfg.d`
tout entier. Aucun fragment ne pouvait gagner ; renommer pour parler en dernier
ne servait a rien, le dernier mot n'appartenant pas a ce repertoire.

Ce que cloud-init regenere, il le regenere depuis `hosts.debian.tmpl` — le
gabarit le documente lui-meme. `hosts_statiques` le pose desormais avec le MEME
contenu que /etc/hosts, et une garde compare les deux a chaque passage.
Redemarrage d'epreuve : les neuf entrees sont la.

La garde precedente affirmait « conforme » en mesurant l'ordre lexical — vrai,
et sans rapport avec ce qui se passait. Une garde qui mesure la mauvaise chose
est pire qu'aucune.

LA ZONE DNS NE PUBLIAIT PAS LES NOMS DE SERVICE.

`forge.genese.internal` et `pki.genese.internal` — des noms que les certificats
portent et que les clients appellent — n'avaient aucun enregistrement. Seul le
plancher savait les resoudre. Trois causes empilees :

  - le plan du site coupait `serveur_powerdns_publier_expositions`, au motif que
    « le site n'a pas d'edge » : ca confondait PUBLIC et EXPOSE ;
  - `expositions_des_applications` rendait `domaine: None` faute de
    `domaines.yml`, et le modele de zone ecarte les expositions dont le domaine
    n'est pas la zone. Repli ajoute, symetrique de celui deja ecrit pour `edge` :
    sans domaine public declare, le domaine est celui que porte le FQDN ;
  - `serveur_powerdns` exigeait les deux registres et echouait si `domaines.yml`
    manquait — le meme tout-ou-rien que `hosts_statiques` a corrige le meme jour.

Puis `named-checkzone` a refuse la zone : `dns.genese.internal` heritait d'un
CNAME par defaut du role ET d'un A par exposition. La garde a bien joue son role
— elle a arrete une zone cassee avant qu'elle soit servie. Le plan l'emporte
desormais sur le defaut du role.

Un service ne doit pas dependre d'un plancher pour etre joignable : le plancher
est un filet, pas le sol.

VERIFICATION. Les huit noms — cinq machines, trois services — resolvent vers les
bonnes adresses depuis les cinq hotes, par le plancher ET par le DNS, et le
plancher survit au redemarrage.

P46 refuse desormais deux choses : plus d'un role ecrivant `manage_etc_hosts`,
et l'absence du gabarit maitre. Controle negatif verifie.

RESTE NOMME, PAS CORRIGE : la zone INVERSE. `serveur_powerdns_zone_inverse`
derive d'un supernet /16 — la forme d'un tenant. Un site declare plusieurs /24
et n'a pas de supernet unique : aucune zone inverse n'est generee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 20:31:01 -04:00

198 lines
9.4 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 ».
- 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'
# 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
when:
- hosts_statiques_actif | bool
- ansible_facts.os_family | default('') == 'Debian'
- 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'