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:
parent
6173d5bfe9
commit
19871894ed
5 changed files with 141 additions and 0 deletions
54
CHANGELOG.md
54
CHANGELOG.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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 }}"
|
||||||
|
|
|
||||||
|
|
@ -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 }}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
32
roles/serveur_keycloak/tasks/jeton-admin.yml
Normal file
32
roles/serveur_keycloak/tasks/jeton-admin.yml
Normal 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
|
||||||
Loading…
Reference in a new issue