ajoute la supervision : règles d'alerte Prometheus, relais ntfy, Uptime Kuma

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-07 21:20:01 +02:00
parent d21c6ef544
commit aa31b6112f
12 changed files with 432 additions and 0 deletions

View file

@ -36,6 +36,19 @@ quit
"
logger -t sauvegarde-dedibackup "Quotidienne envoyée : $ARCHIVE_QUOT"
# Métrique pour l'alerte Prometheus "SauvegardeAncienne" (étape 7) : écrite
# seulement une fois la sauvegarde confirmée envoyée, de façon atomique
# (fichier temporaire puis renommage) pour que node_exporter ne lise jamais
# un fichier à moitié écrit.
DOSSIER_TEXTFILE="{{ sauvegardes_dossier_textfile | default('/var/lib/node_exporter/textfile') }}"
mkdir -p "$DOSSIER_TEXTFILE"
cat > "$DOSSIER_TEXTFILE/sauvegarde.prom.tmp" << METRIQUE
# HELP syllabus_derniere_sauvegarde_reussie_timestamp Horodatage Unix de la dernière sauvegarde Dedibackup réussie.
# TYPE syllabus_derniere_sauvegarde_reussie_timestamp gauge
syllabus_derniere_sauvegarde_reussie_timestamp $(date +%s)
METRIQUE
mv "$DOSSIER_TEXTFILE/sauvegarde.prom.tmp" "$DOSSIER_TEXTFILE/sauvegarde.prom"
if [ "$DOW" = "{{ sauvegardes_jour_hebdomadaire }}" ]; then
ARCHIVE_HEBDO="hebdomadaire-${SEMAINE}.tar.zst.age"
cp "$WORKDIR/$ARCHIVE_QUOT" "$WORKDIR/$ARCHIVE_HEBDO"

View file

@ -0,0 +1,5 @@
---
supervision_fichier_webhook: /root/.alertes-webhook
supervision_port_relais: 9099
supervision_port_uptime_kuma: 3001
supervision_dossier_textfile: /var/lib/node_exporter/textfile

View file

@ -0,0 +1,10 @@
---
- name: recharger systemd
ansible.builtin.systemd:
daemon_reload: true
- name: redémarrer le relais alertmanager-ntfy
ansible.builtin.systemd:
name: alertmanager-ntfy
state: restarted
enabled: true

View file

@ -0,0 +1,100 @@
---
- name: vérifier la présence du fichier webhook (jamais son contenu)
ansible.builtin.stat:
path: "{{ supervision_fichier_webhook }}"
register: supervision_webhook_stat
- name: échouer clairement si le webhook est absent
ansible.builtin.fail:
msg: >
{{ supervision_fichier_webhook }} est absent. Adam doit le déposer
lui-même (chmod 600) avant de continuer : c'est un secret, Claude ne le
demande jamais en clair.
when: not supervision_webhook_stat.stat.exists
- name: vérifier les permissions du fichier webhook
ansible.builtin.fail:
msg: "{{ supervision_fichier_webhook }} ne devrait pas être lisible par d'autres utilisateurs (mode actuel {{ supervision_webhook_stat.stat.mode }})."
when:
- supervision_webhook_stat.stat.exists
- supervision_webhook_stat.stat.mode != '0600'
- name: créer le dossier des métriques textfile (node_exporter)
ansible.builtin.file:
path: "{{ supervision_dossier_textfile }}"
state: directory
owner: root
group: root
mode: "0755"
- name: créer le dossier de configuration Prometheus personnalisée
ansible.builtin.file:
path: /etc/kolla/config/prometheus
state: directory
owner: root
group: root
mode: "0750"
- name: déployer les règles d'alerte Prometheus
ansible.builtin.copy:
src: "{{ playbook_dir }}/../kolla/config/prometheus/alertes.rules"
dest: /etc/kolla/config/prometheus/alertes.rules
owner: root
group: root
mode: "0640"
register: supervision_regles_alerte
- name: déployer la configuration Alertmanager
ansible.builtin.copy:
src: "{{ playbook_dir }}/../kolla/config/prometheus/prometheus-alertmanager.yml"
dest: /etc/kolla/config/prometheus/prometheus-alertmanager.yml
owner: root
group: root
mode: "0640"
register: supervision_config_alertmanager
- name: déployer le relais Alertmanager -> ntfy
ansible.builtin.template:
src: alertmanager-ntfy.py.j2
dest: /usr/local/sbin/alertmanager-ntfy.py
owner: root
group: root
mode: "0700"
notify: redémarrer le relais alertmanager-ntfy
- name: déployer le service systemd du relais
ansible.builtin.template:
src: alertmanager-ntfy.service.j2
dest: /etc/systemd/system/alertmanager-ntfy.service
mode: "0644"
notify:
- recharger systemd
- redémarrer le relais alertmanager-ntfy
- name: appliquer les changements du relais
ansible.builtin.meta: flush_handlers
- name: vérifier que le relais tourne
ansible.builtin.systemd:
name: alertmanager-ntfy
state: started
enabled: true
- name: déployer Uptime Kuma
community.docker.docker_container:
name: uptime-kuma
image: louislam/uptime-kuma:1
state: started
restart_policy: unless-stopped
ports:
- "127.0.0.1:{{ supervision_port_uptime_kuma }}:3001"
volumes:
- uptime-kuma-data:/app/data
- name: afficher s'il faut relancer Kolla pour les nouvelles règles
ansible.builtin.debug:
msg: >
Config Prometheus/Alertmanager modifiée : lancer
`kolla-ansible reconfigure -i /etc/kolla/all-in-one --tags prometheus`
sur le serveur (voir docs/runbooks/supervision.md).
when: supervision_regles_alerte.changed or supervision_config_alertmanager.changed

View file

@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Relais local : reçoit le webhook JSON d'Alertmanager, le transforme en
notification ntfy. Géré par Ansible (rôle supervision) — ne pas modifier à la
main. N'écoute que sur 127.0.0.1 : jamais exposé en dehors du serveur.
L'URL ntfy (avec le topic) est un secret : lue depuis {{ supervision_fichier_webhook }}
à chaque notification, jamais mise en cache, jamais journalisée.
"""
import http.server
import json
import urllib.request
FICHIER_WEBHOOK = "{{ supervision_fichier_webhook }}"
ADRESSE = ("127.0.0.1", {{ supervision_port_relais }})
def lire_url_ntfy():
with open(FICHIER_WEBHOOK, encoding="utf-8") as f:
return f.read().strip()
class Gestionnaire(http.server.BaseHTTPRequestHandler):
def do_POST(self):
longueur = int(self.headers.get("Content-Length", 0))
corps = self.rfile.read(longueur)
try:
charge = json.loads(corps)
except ValueError:
self.send_response(400)
self.end_headers()
return
try:
url_ntfy = lire_url_ntfy()
except OSError:
self.send_response(500)
self.end_headers()
return
for alerte in charge.get("alerts", []):
statut = alerte.get("status", "inconnu")
nom = alerte.get("labels", {}).get("alertname", "?")
resume = (
alerte.get("annotations", {}).get("description")
or alerte.get("annotations", {}).get("summary")
or nom
)
titre = f"[{statut.upper()}] {nom}"
requete = urllib.request.Request(
url_ntfy,
data=resume.encode("utf-8"),
method="POST",
headers={
"Title": titre,
"Priority": "high" if statut == "firing" else "default",
},
)
try:
urllib.request.urlopen(requete, timeout=5)
except Exception:
pass # ne bloque jamais Alertmanager ; vérifier manuellement en cas de doute
self.send_response(200)
self.end_headers()
def log_message(self, format, *args): # silencieux : rien de secret à tracer non plus
pass
if __name__ == "__main__":
serveur = http.server.HTTPServer(ADRESSE, Gestionnaire)
serveur.serve_forever()

View file

@ -0,0 +1,18 @@
[Unit]
Description=Relais Alertmanager -> ntfy (Syllabus)
# Géré par Ansible (rôle supervision) — ne pas modifier à la main.
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/sbin/alertmanager-ntfy.py
Restart=on-failure
# Durcissement minimal : ce service lit un secret (fichier webhook), pas de
# raison qu'il écrive où que ce soit ou touche au reste du système.
NoNewPrivileges=true
ProtectSystem=strict
PrivateTmp=true
ReadOnlyPaths=/root/.alertes-webhook
[Install]
WantedBy=multi-user.target

8
ansible/supervision.yml Normal file
View file

@ -0,0 +1,8 @@
---
# Étape 7 du déploiement Syllabus : supervision et alertes.
- name: Supervision et alertes
hosts: syllabus
become: true
gather_facts: true
roles:
- supervision

View file

@ -239,3 +239,36 @@ Une image de VM, ce n'est pas juste un système d'exploitation : c'est tout ce d
**Suite**
Étape 7 : supervision (Prometheus, Grafana, Uptime Kuma) et alertes, accessibles par tunnel SSH en attendant un nom de domaine.
## 2026-10-07 — Étape 7 : supervision et alertes
**Fait**
- Prometheus, Alertmanager, Grafana, node_exporter, cAdvisor, openstack-exporter, libvirt/mysqld/memcached-exporters étaient déjà déployés par Kolla depuis l'étape 5 (`enable_prometheus`/`enable_grafana: "yes"`), tous liés uniquement à `10.10.0.1` — jamais exposés sur `eno1` (déjà vérifié par nmap à l'étape 5, re-confirmé ici).
- Rôle Ansible `supervision` + playbook `ansible/supervision.yml` : vérifie la présence et les permissions (`600`) de `/root/.alertes-webhook` sans jamais afficher son contenu, déploie le relais Alertmanager → ntfy (`/usr/local/sbin/alertmanager-ntfy.py`, service systemd, `127.0.0.1:9099` uniquement), et Uptime Kuma (conteneur Docker, `127.0.0.1:3001`, volume nommé).
- `kolla/config/prometheus/alertes.rules` : 4 règles (`DisqueSature` > 80 %, `MemoireSaturee` > 90 %, `ServiceOpenStackArrete` via les métriques `openstack_nova_agent_state`/`openstack_neutron_agent_state`, `SauvegardeAncienne` > 26 h).
- `kolla/config/prometheus/prometheus-alertmanager.yml` : remplace le gabarit Kolla par défaut (receveur sans destination) par un webhook vers le relais local. Déposé via le mécanisme standard de Kolla (`node_custom_config/prometheus/...`), pas de template Jinja Kolla à casser.
- Collecteur "textfile" activé sur node_exporter (`prometheus_node_exporter_cmdline_extras`, `prometheus_node_exporter_extra_volumes` dans `globals.yml`) : `sauvegarde-dedibackup.sh` y écrit, de façon atomique, l'horodatage de la dernière sauvegarde réussie à chaque exécution.
**Problèmes rencontrés et solution**
- **Oubli répété (2 fois) : modifier un fichier du dépôt (`globals.yml`, le script de sauvegarde) sans le repousser sur le serveur avant de relancer Kolla.** Dans les deux cas, `kolla-ansible reconfigure` a tourné sans erreur mais sur l'ancienne configuration — aucun message ne le signale. Corrigé en relançant le playbook Ansible concerné avant `reconfigure`. Noté dans `docs/runbooks/supervision.md` comme piège à ne pas refaire.
- Mesure du mot de passe Alertmanager/Prometheus pour les tests `curl` : évité `python3-yaml` (absent du Python système) au profit d'un simple `awk` sur le format `clef: valeur` de `passwords.yml` ; jamais affiché, seulement sa longueur vérifiée une fois pour confirmer qu'il avait bien été lu.
**Critère vérifié**
- Alerte de test injectée via l'API d'Alertmanager (`POST /api/v2/alerts`), sans toucher à rien de réel sur le serveur. **Confirmé par Adam : notification ntfy bien reçue.** Testé aussi en contournant Alertmanager (requête directe au relais) : `HTTP 200`.
- Métrique de sauvegarde vérifiée de bout en bout : fichier textfile → `node_exporter` (`node_textfile_scrape_error 0`) → Prometheus (`/api/v1/query` renvoie la valeur).
- Règles d'alerte confirmées chargées dans Prometheus (`/api/v1/rules` : les 4 règles listées).
**Dette assumée, à trancher avec Adam**
Pas d'alerte « serveur injoignable » : toute la chaîne de supervision tourne sur le serveur qu'elle surveille, donc si le serveur tombe complètement, personne n'est prévenu par ce mécanisme. L'architecture d'origine (CLAUDE.md) prévoyait une sonde hors du serveur (Hetzner CX23) ; ce déploiement n'a qu'un seul serveur. Documenté dans `docs/runbooks/supervision.md`, pas construit : décision à prendre par Adam (qui commande et paie lui-même toute ressource supplémentaire).
**Comprendre**
La bonne nouvelle de cette étape, c'est que Kolla avait déjà fait le plus gros : Prometheus, Grafana et une demi-douzaine d'exportateurs tournaient depuis l'étape 5 sans qu'on ait rien demandé de spécial, juste en activant deux options. Le travail réel a surtout été de les relier à une vraie notification (Alertmanager ne sait parler qu'un format webhook générique, pas le format de ntfy, d'où le petit relais) et d'ajouter une alerte qui n'existe dans aucun exportateur standard : l'âge de la dernière sauvegarde. Le collecteur « textfile » de node_exporter est fait exactement pour ça — transformer n'importe quel script maison en métrique Prometheus, juste en écrivant un fichier texte à un endroit précis. La vraie leçon de l'étape, à retenir pour la suite : modifier un fichier dans le dépôt ne change rien tant qu'il n'est pas repoussé sur le serveur — une évidence une fois dite, mais qui s'oublie vite quand on va vite.
**Suite**
Étape 8 : portail Laravel (Blade, Vite, MariaDB/Redis pour l'app, modèles, connexion prof/élève, salle d'attente). Le dossier `design/` doit exister : à vérifier avant de commencer.

View file

@ -0,0 +1,111 @@
# Runbook — Supervision et alertes
## Ce qui tourne
| Service | Adresse (interne, `10.10.0.1`) | Déployé par |
|---|---|---|
| Prometheus | `:9091` | Kolla (`enable_prometheus: "yes"`) |
| Alertmanager | `:9093` | Kolla |
| Grafana | `:3000` | Kolla |
| node_exporter | `:9100` | Kolla (+ collecteur textfile, rôle `supervision`) |
| openstack-exporter | `:9198` | Kolla |
| cAdvisor, libvirt/mysqld/memcached exporters | ports divers | Kolla |
| Relais Alertmanager → ntfy | `127.0.0.1:9099` | rôle Ansible `supervision` |
| Uptime Kuma | `127.0.0.1:3001` | rôle Ansible `supervision` (conteneur Docker) |
Rien de tout cela n'est exposé sur `eno1` (vérifié : seuls 22/80/443 répondent
depuis Internet). **Tant qu'il n'y a pas de domaine, l'accès se fait par
tunnel SSH.**
## Accéder aux interfaces (tunnel SSH depuis le Mac)
```
ssh -L 3000:10.10.0.1:3000 -L 9091:10.10.0.1:9091 -L 9093:10.10.0.1:9093 -L 3001:127.0.0.1:3001 paillasse
```
Puis dans le navigateur : `http://localhost:3000` (Grafana), `:9091`
(Prometheus), `:9093` (Alertmanager), `:3001` (Uptime Kuma — premier accès :
créer le compte admin toi-même, c'est une étape interactive).
Identifiants Grafana/Prometheus/Alertmanager : utilisateur `admin`, mot de
passe dans `/etc/kolla/passwords.yml` sur le serveur (`grafana_admin_password`,
`prometheus_password`, `prometheus_alertmanager_password`) — jamais affichés
ici, à lire toi-même :
```
sudo grep '^grafana_admin_password:' /etc/kolla/passwords.yml
```
## Alertes en place
Fichier `kolla/config/prometheus/alertes.rules` (poussé par le rôle
`supervision`, chargé par Prometheus) :
| Alerte | Condition | Sévérité |
|---|---|---|
| `DisqueSature` | occupation de `/` > 80 % pendant 5 min | avertissement |
| `MemoireSaturee` | RAM utilisée > 90 % pendant 5 min | avertissement |
| `ServiceOpenStackArrete` | un agent Nova ou Neutron à l'état différent de `up` pendant 2 min | critique |
| `SauvegardeAncienne` | dernière sauvegarde Dedibackup réussie il y a plus de 26 h | critique |
`SauvegardeAncienne` dépend d'une métrique maison
(`syllabus_derniere_sauvegarde_reussie_timestamp`), écrite par
`sauvegarde-dedibackup.sh` dans `/var/lib/node_exporter/textfile/sauvegarde.prom`
à chaque sauvegarde réussie (collecteur "textfile" de node_exporter).
**Limite connue, assumée pour l'instant : pas d'alerte « serveur
injoignable ».** Toute la chaîne (Prometheus, Alertmanager, le relais, ntfy)
tourne sur le même serveur que ce qu'elle surveille : si le serveur tombe
complètement, rien ne peut prévenir personne. C'est exactement pour ça que
CLAUDE.md prévoyait à l'origine une sonde **hors du serveur** (un petit
Hetzner CX23). Dans ce déploiement (un seul serveur Scaleway), ce n'est pas
fait — à décider avec Adam s'il veut un jour un point de supervision externe.
## Le relais Alertmanager → ntfy
Alertmanager ne sait pas parler nativement le format ntfy. Un petit service
Python (`/usr/local/sbin/alertmanager-ntfy.py`, service systemd
`alertmanager-ntfy`, écoute uniquement sur `127.0.0.1:9099`) reçoit le webhook
JSON standard d'Alertmanager et le retraduit en notification ntfy.
- L'URL ntfy (avec le topic) est lue à chaque notification depuis
`/root/.alertes-webhook` (`600 root:root`) — jamais mise en cache, jamais
journalisée, jamais dans Git.
- Configuration Alertmanager : `kolla/config/prometheus/prometheus-alertmanager.yml`
(receveur unique `ntfy`, webhook vers le relais local).
### Déclencher une alerte de test
Sans toucher à rien de réel sur le serveur, via l'API d'Alertmanager :
```
ssh paillasse
sudo bash -c '
PASS=$(awk -F": " "/^prometheus_alertmanager_password:/{print \$2}" /etc/kolla/passwords.yml)
MAINTENANT=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
FIN=$(date -u -d "+5 minutes" +%Y-%m-%dT%H:%M:%S.000Z)
curl -s --user "admin:$PASS" -XPOST http://10.10.0.1:9093/api/v2/alerts -H "Content-Type: application/json" -d "[{
\"labels\": {\"alertname\": \"TestManuel\"},
\"annotations\": {\"description\": \"Test manuel.\"},
\"startsAt\": \"$MAINTENANT\", \"endsAt\": \"$FIN\"
}]"
unset PASS
'
```
**Vérifié le 2026-10-07 : alerte de test reçue par Adam sur ntfy**, par ce
mécanisme et en testant aussi le relais directement
(`curl -XPOST http://127.0.0.1:9099/alertmanager ...`).
## Après tout changement à `kolla/config/prometheus/*` ou `kolla/globals.yml`
```
cd ansible && script -q /dev/null ansible-playbook kolla.yml -i inventory.ini
cd ansible && script -q /dev/null ansible-playbook supervision.yml -i inventory.ini
ssh paillasse "sudo -i bash -c 'export PATH=/opt/kolla-venv/bin:\$PATH; kolla-ansible reconfigure -i /etc/kolla/all-in-one --tags prometheus'"
```
**Piège vécu le 2026-10-07 :** modifier `kolla/globals.yml` ou un fichier de
`kolla/config/` dans le dépôt ne suffit pas : il faut d'abord rejouer
`kolla.yml`/`supervision.yml` pour le copier sur le serveur, **puis**
`kolla-ansible reconfigure`. Oublier la première étape fait tourner
`reconfigure` sur l'ancienne config, sans erreur visible.

View file

@ -0,0 +1,38 @@
groups:
- name: syllabus
rules:
- alert: DisqueSature
expr: 100 - (node_filesystem_avail_bytes{mountpoint="/",fstype!="tmpfs"} / node_filesystem_size_bytes{mountpoint="/",fstype!="tmpfs"} * 100) > 80
for: 5m
labels:
severite: avertissement
annotations:
summary: "Disque / rempli a plus de 80%"
description: "Occupation du disque racine superieure a 80% depuis plus de 5 minutes."
- alert: MemoireSaturee
expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) * 100 > 90
for: 5m
labels:
severite: avertissement
annotations:
summary: "RAM utilisee a plus de 90%"
description: "Memoire disponible inferieure a 10% depuis plus de 5 minutes."
- alert: ServiceOpenStackArrete
expr: openstack_nova_agent_state == 0 or openstack_neutron_agent_state == 0
for: 2m
labels:
severite: critique
annotations:
summary: "Un agent OpenStack est arrete"
description: "Un agent Nova ou Neutron signale un etat different de UP depuis plus de 2 minutes."
- alert: SauvegardeAncienne
expr: (time() - syllabus_derniere_sauvegarde_reussie_timestamp) > 26 * 3600
for: 10m
labels:
severite: critique
annotations:
summary: "Derniere sauvegarde reussie il y a plus de 26h"
description: "Le timer de sauvegarde Dedibackup n'a pas reussi depuis plus de 26 heures : verifier /root/.dedibackup et le journal du service."

View file

@ -0,0 +1,18 @@
# Remplace le gabarit Kolla par défaut (receveur sans destination). Le relais
# local transforme ce webhook JSON en notification ntfy (voir
# ansible/roles/supervision/templates/alertmanager-ntfy.py.j2). L'URL ntfy
# elle-même reste un secret, lue depuis /root/.alertes-webhook, jamais ici.
global:
resolve_timeout: 5m
route:
receiver: ntfy
group_wait: 10s
group_interval: 5m
repeat_interval: 3h
receivers:
- name: ntfy
webhook_configs:
- url: http://127.0.0.1:9099/alertmanager
send_resolved: true
templates:
- '/etc/prometheus/*.tmpl'

View file

@ -34,6 +34,12 @@ enable_nova_serialconsole_proxy: "yes" # console élève (nova-serialproxy)
nova_compute_virt_type: "kvm" # VT-x vérifié à l'étape 1
enable_prometheus: "yes" # supervision (étape 7)
enable_grafana: "yes"
# Collecteur "textfile" de node_exporter : permet d'exposer en métrique
# Prometheus la date de la dernière sauvegarde réussie (alerte SauvegardeAncienne),
# écrite par le service sauvegarde-dedibackup dans ce dossier.
prometheus_node_exporter_cmdline_extras: "--collector.textfile.directory=/var/lib/node_exporter/textfile"
prometheus_node_exporter_extra_volumes:
- "/var/lib/node_exporter/textfile:/var/lib/node_exporter/textfile:ro"
# Cinder désactivé pour l'instant : les deux disques sont entièrement pris par
# le RAID 1 (/), il n'y a pas de place pour un groupe de volumes LVM dédié.