keycloak : un jeton frais la ou on s'en sert ; grafana : un echec lisible

Technolibre remonte depuis zero une seconde fois — 14 hotes, 2588 taches ok,
0 failed. Deux defauts trouves en chemin.

LE JETON KEYCLOAK VIVAIT 60 SECONDES. Il etait pris dans politique-mdp.yml —
le 2e des neuf fichiers du role — et reutilise jusqu'au 8e. Entre les deux,
six fichiers de travail dont groupes-ldap.yml et ses reprises espacees de
15 s. Sur une construction NEUVE le temps depasse la minute : 401. Sur un
REJEU tout est converge, ca va vite, ca passe.

D'ou les deux echecs du matin, chaque fois suivis d'un succes au rejeu, qui
donnaient l'illusion d'une course au demarrage de Keycloak. J'avais ecrit
alors ne pas avoir de mesure qui le prouve — c'etait juste, et la cause etait
l'AGE du jeton. jeton-admin.yml en prend un frais la ou on s'en sert.

GRAFANA : UN ECHEC TRANSITOIRE RENDU ILLISIBLE. Premier demarrage, apres 67 s
de migrations : « failed to create admin user: no such column: uid », alors
que la migration qui ajoute cette colonne etait journalisee comme reussie.
Base neuve : tout remigre, service actif, colonne presente. L'incident ne
s'est pas reproduit et Chezlepro ne l'a jamais eu — je n'ai donc PAS corrige
la cause, faute de l'avoir reproduite. J'ai corrige ce qui la rendait
indechiffrable :

- Restart=on-failure venait du paquet SANS RestartSec, donc 100 ms : six
  relances en une seconde, chacune rejouant les migrations sur la meme base
  SQLite. Un echec unique se presentait comme un desastre. RestartSec=10 ;
- la rotation du compte de secours echouait cinq fois sous no_log en
  annoncant « the output has been hidden », alors que la vraie cause etait
  ailleurs et lisible : le serveur ne demarrait pas. Une attente explicite sur
  le port precede desormais la CLI, avec un message qui renvoie a la PREMIERE
  erreur du journal.

Troisieme fois dans la journee que no_log masque la cause au moment ou elle
sert : une garde qui protege un secret ne doit pas emporter le diagnostic.

Verifie : 7 devis sur Technolibre — MTU, identite, certificats, PostgreSQL,
courriel, frontiere CONFORME ; prouver.py 35 OK ; ansible-lint production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-10 20:19:50 -04:00
parent 6173d5bfe9
commit 19871894ed
5 changed files with 141 additions and 0 deletions

View file

@ -1,5 +1,59 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-08-10 — Le jeton Keycloak vivait 60 s, et Grafana rendait son échec illisible
Technolibre remonté depuis zéro une seconde fois — `14 hôtes · 2 588 tâches ok · 0 failed`.
Deux défauts trouvés en chemin, dont un dont la cause était **restée non établie** le matin
même.
### Le jeton d'administration Keycloak était pris une fois pour toutes
Il était obtenu dans `politique-mdp.yml` — le **2ᵉ** des neuf fichiers du rôle — et réutilisé
jusqu'au **8ᵉ**. Or le jeton `admin-cli` du realm `master` vit **60 secondes**. Entre les
deux : six fichiers de travail, dont `groupes-ldap.yml` et ses reprises espacées de 15 s.
Sur une construction **neuve**, le temps écoulé dépasse la minute et l'appel suivant se prend
un 401. Sur un **rejeu**, tout est convergé, ça va vite, ça passe. D'où deux échecs le matin
même, suivis chaque fois d'un succès au rejeu — ce qui donnait l'illusion d'une course au
démarrage de Keycloak. J'avais écrit alors ne pas avoir de mesure qui le prouve ; c'était la
bonne prudence, et la cause était l'**âge du jeton**.
`jeton-admin.yml` prend désormais un jeton frais **là où on s'en sert**. C'est gratuit :
Keycloak répond en quelques millisecondes en local.
### Grafana : un échec transitoire, rendu illisible par systemd
Au premier démarrage, après **67 secondes** de migrations, `grafana-server` a échoué sur
`failed to create admin user: SQL logic error: no such column: uid` — alors que la migration
qui ajoute cette colonne était journalisée comme **réussie**. Base neuve : tout remigre
correctement, service actif, colonne présente. **L'incident ne s'est pas reproduit, et
Chezlepro ne l'a jamais eu.**
Je n'ai donc pas corrigé la cause — je ne l'ai pas reproduite. J'ai corrigé ce qui la rendait
indéchiffrable :
- `Restart=on-failure` venait du paquet **sans `RestartSec`**, donc 100 ms : systemd a relancé
**six fois en une seconde**, chaque relance rejouant les migrations sur la même base SQLite.
Un échec unique se présentait comme un désastre, et il a fallu remonter tout le journal pour
retrouver la **première** erreur — la seule qui disait quelque chose. `RestartSec=10` ;
- la rotation du compte de secours échouait **cinq fois sous `no_log`** en annonçant « the
output has been hidden », alors que la vraie cause était ailleurs et parfaitement lisible :
le serveur ne démarrait pas. Une attente explicite sur le port précède maintenant la CLI,
avec un message qui dit d'aller chercher la **première** erreur du journal.
**Troisième fois dans la journée que `no_log` masque la cause au moment où elle sert.** Le
motif est constant, et il mérite d'être retenu : *une garde qui protège un secret ne doit pas
emporter le diagnostic avec lui.*
### Sept devis sur Technolibre
MTU, identité, certificats, PostgreSQL, courriel et frontière : **CONFORME**. Les expositions
répondent depuis l'edge ; seul le plancher `/etc/hosts` du poste manquait.
**Le devis du MTU mérite une mention** : c'est la première flotte du dépôt à naître au bon
MTU sans une seule intervention — le gabarit porte `mtu=1`, les quatorze invités sont à 1450
dès leur premier démarrage.
## 2026-08-10 — Le MTU de la zone n'atteignait pas les invités ## 2026-08-10 — Le MTU de la zone n'atteignait pas les invités
Question de l'exploitant : « je ne vois nulle part un MTU à 1450 ». Elle était fondée. Question de l'exploitant : « je ne vois nulle part un MTU à 1450 ». Elle était fondée.

View file

@ -127,6 +127,32 @@
# Idempotence par EMPREINTE : on garde le sha256 du secret applique. Tant qu'il ne # Idempotence par EMPREINTE : on garde le sha256 du secret applique. Tant qu'il ne
# change pas, on ne touche a rien ; des qu'il change, on reapplique. On ne peut pas # change pas, on ne touche a rien ; des qu'il change, on reapplique. On ne peut pas
# lire le mot de passe en place, donc on memorise ce qu'on a pose. # lire le mot de passe en place, donc on memorise ce qu'on a pose.
# EXIGER QUE LE SERVEUR REPONDE AVANT DE TOUCHER A SA BASE. La CLI qui suit echouait
# sinon cinq fois de suite sous `no_log`, en annoncant « the output has been hidden » —
# alors que la vraie cause etait ailleurs et parfaitement lisible : `grafana-server` ne
# demarrait pas, sa base ayant echoue une migration au premier lancement (2026-08-10,
# `no such column: uid`). Cinq reprises censurees contre un message clair : on prefere
# le message.
- name: Attendre que Grafana réponde avant de toucher au compte de secours
ansible.builtin.wait_for:
host: 127.0.0.1
port: "{{ serveur_grafana_port | default(3000) }}"
timeout: 120
register: serveur_grafana_pret
failed_when: false
when: not ansible_check_mode
- name: Dire que Grafana ne répond pas, plutôt que d'échouer sur sa CLI
ansible.builtin.fail:
msg: >-
Grafana n'écoute pas sur le port {{ serveur_grafana_port | default(3000) }} après
120 s : la rotation du compte de secours ne peut pas aboutir, et sa CLI ne dirait
rien d'utile. Regarder `journalctl -u grafana-server` — et y chercher la PREMIÈRE
erreur, pas la dernière : `Restart=on-failure` en empile plusieurs.
when:
- not ansible_check_mode
- serveur_grafana_pret is failed
- name: Empreinte du mot de passe admin en place - name: Empreinte du mot de passe admin en place
ansible.builtin.slurp: ansible.builtin.slurp:
src: "{{ serveur_grafana_marqueur_admin }}" src: "{{ serveur_grafana_marqueur_admin }}"

View file

@ -1,4 +1,12 @@
[Service] [Service]
# `Restart=on-failure` vient du paquet, SANS `RestartSec` — donc 100 ms par defaut.
# Le 2026-08-10, une migration de base a echoue au premier demarrage : systemd a relance
# six fois en une seconde, chaque relance rejouant les migrations sur la meme base SQLite.
# Un echec unique s'est ainsi presente comme un desastre, et il a fallu remonter tout le
# journal pour retrouver la PREMIERE erreur — la seule qui disait quelque chose.
#
# Dix secondes laissent le temps de voir, de journaliser, et n'empilent pas les migrations.
RestartSec=10
Environment=GF_SERVER_DOMAIN={{ serveur_grafana_hostname }} Environment=GF_SERVER_DOMAIN={{ serveur_grafana_hostname }}
Environment=GF_SERVER_ROOT_URL={{ serveur_grafana_root_url }} Environment=GF_SERVER_ROOT_URL={{ serveur_grafana_root_url }}
Environment=GF_SECURITY_ADMIN_PASSWORD={{ serveur_grafana_admin_password }} Environment=GF_SECURITY_ADMIN_PASSWORD={{ serveur_grafana_admin_password }}

View file

@ -15,13 +15,34 @@
# la commande, sort en succes et n'ecrit rien (mesure du meme jour sur `smtpServer`). # la commande, sort en succes et n'ecrit rien (mesure du meme jour sur `smtpServer`).
# On relit systematiquement apres avoir pose. # On relit systematiquement apres avoir pose.
# Jeton FRAIS : celui de `politique-mdp.yml` a plus de six fichiers d'age, et il vit
# 60 s. C'est ce qui faisait echouer cette lecture sur toute construction NEUVE — et
# passer au rejeu, une fois la flotte convergee. Voir `jeton-admin.yml`.
- name: Reprendre un jeton d'administration avant de lire les clients
ansible.builtin.include_tasks: jeton-admin.yml
- name: Lire les clients du realm - name: Lire les clients du realm
ansible.builtin.uri: ansible.builtin.uri:
url: "http://localhost:8080/admin/realms/{{ serveur_keycloak_realm }}/clients" url: "http://localhost:8080/admin/realms/{{ serveur_keycloak_realm }}/clients"
headers: headers:
Authorization: "Bearer {{ serveur_keycloak_pol_jeton.json.access_token }}" Authorization: "Bearer {{ serveur_keycloak_pol_jeton.json.access_token }}"
status_code: [200]
register: serveur_keycloak_clients_actuels register: serveur_keycloak_clients_actuels
# `no_log` protege le jeton porte par l'en-tete. Il masquait aussi la CAUSE : le
# 2026-08-10, cet appel a echoue deux fois sur « the output has been hidden », et il a
# fallu remonter jusqu'a l'age du jeton pour comprendre. On extrait donc le verdict.
no_log: true no_log: true
failed_when: false
- name: Dire pourquoi la lecture des clients a échoué, si elle a échoué
ansible.builtin.fail:
msg: >-
Keycloak a refuse la lecture des clients du realme
« {{ serveur_keycloak_realm }} » (HTTP
{{ serveur_keycloak_clients_actuels.status | default('?') }}).
Un 401 signifie un jeton perime — verifier que `jeton-admin.yml` est bien inclus
juste avant cet appel.
when: serveur_keycloak_clients_actuels.status | default(0) != 200
- name: Composer les URI de déconnexion attendues - name: Composer les URI de déconnexion attendues
ansible.builtin.set_fact: ansible.builtin.set_fact:

View file

@ -0,0 +1,32 @@
---
# Un jeton d'administration FRAIS, a inclure juste avant de s'en servir.
#
# POURQUOI CE FICHIER EXISTE. Le jeton etait obtenu une seule fois, dans
# `politique-mdp.yml` — le 2e des neuf fichiers du role — puis reutilise jusqu'au 8e.
# Or le jeton `admin-cli` du realm `master` vit **60 secondes** par defaut. Entre les
# deux, six fichiers de travail, dont `groupes-ldap.yml` et ses reprises espacees de 15 s.
#
# Sur un deploiement NEUF, le temps ecoule depasse la minute et l'appel suivant se prend
# un 401. Sur un REJEU, tout est deja converge, ca va vite, et ca passe. C'est exactement
# ce qu'on a observe le 2026-08-10 : echec sur la construction, succes au rejeu — deux
# fois, ce qui donnait l'illusion d'une course aleatoire au demarrage de Keycloak.
#
# Un jeton se prend donc la ou on l'utilise, jamais « une fois pour toutes ». C'est
# gratuit : Keycloak repond en quelques millisecondes en local.
- name: Obtenir un jeton d'administration frais
ansible.builtin.uri:
url: "http://localhost:8080/realms/master/protocol/openid-connect/token"
method: POST
body_format: form-urlencoded
body:
grant_type: password
client_id: admin-cli
username: "{{ serveur_keycloak_admin_user }}"
password: "{{ serveur_keycloak_admin_password }}"
status_code: [200]
register: serveur_keycloak_pol_jeton
no_log: true
# Keycloak peut encore etre en train de recharger apres une tache precedente.
retries: 5
delay: 4
until: serveur_keycloak_pol_jeton is succeeded