
🌐 Aussi en: English · Deutsch · Español
Partie 6 de la visite Ansible, et la dernière. La partie 1 a expliqué le cadre, la partie 2 le serveur et la plateforme, les parties 3 et 4 les deux applications, la partie 5 la sécurité. Cette partie parle de savoir ce qui se passe. Elle décrit la version v3.3.0.
Un serveur que personne ne surveille tombe en panne en silence. L’installation ici surveille de deux manières, et il aide de bien les distinguer :
| Type | Question à laquelle il répond | Rôles |
|---|---|---|
| Métriques | Comment va le serveur dans le temps ? Mémoire, disque, CPU, base de données, PHP – sous forme de courbes | common_node_exporter, common_mysqld_exporter, common_kube_metrics, les exporteurs en sidecar (parties 3 et 4), common_prometheus, common_grafana |
| Chien de garde | Quelque chose est-il cassé en ce moment ? Un service arrêté, un pod qui plante en boucle, une sauvegarde manquante, un certificat sur le point d’expirer | common_monit |
Les deux envoient des mails, mais différemment : Grafana quand une métrique franchit un seuil, Monit quand un état change. Et les deux suivent la règle de la partie 5 – silence tant que tout va bien, pour que chaque mail ait un sens.
📈 Comment circulent les métriques
Le modèle est le pull : des programmes appelés exporteurs exposent leurs chiffres sous forme de page texte en HTTP, et Prometheus récupère cette page toutes les 15 secondes, stocke chaque valeur avec un horodatage et la garde 30 jours. Grafana interroge Prometheus et dessine les courbes. Rien ne pousse de données nulle part, et rien ne quitte le serveur.
| Exporteur | Tourne | Rapporte | Prometheus le joint sur |
|---|---|---|---|
| Node Exporter | sur l’hôte, les deux piles | CPU, mémoire, disques, systèmes de fichiers, réseau, systemd | 127.0.0.1:9100 |
| mysqld_exporter | sur l’hôte, Nextcloud seulement | le MariaDB de l’hôte : connexions, requêtes, InnoDB | 127.0.0.1:9104 |
| mysqld_exporter | sidecar dans le pod MariaDB, blog | le MariaDB du blog | un NodePort sur 127.0.0.1 |
| php-fpm_exporter | sidecar dans le pod applicatif, les deux | workers PHP-FPM : occupés, inactifs, file d’attente, requêtes lentes | un NodePort sur 127.0.0.1 |
| cAdvisor | dans le kubelet, les deux stacks | CPU, mémoire, bridage, événements OOM par conteneur | https://127.0.0.1:10250 avec un jeton |
| kube-state-metrics | pod dans kube-system, les deux | limites, redémarrages, raisons d’arrêt depuis l’API | NodePort 30808 sur 127.0.0.1 |
Tout écoute sur localhost ou sur des NodePorts que le pare-feu rejette depuis l’extérieur (partie 5). Prometheus se lie aussi à localhost ; seul Grafana est joignable depuis l’extérieur, et seulement via l’Ingress sous /grafana/, avec sa propre connexion.
🧰 Les exporteurs : le motif du binaire épinglé, une fois de plus
common_node_exporter et common_mysqld_exporter sont petits et presque identiques, et ils sont l’exemple le plus clair d’un motif introduit par la partie 2 :
- Un utilisateur système sans shell de connexion et sans répertoire personnel – le processus ne doit pas tourner en root, et personne ne se connecte jamais sous cet utilisateur.
- Lire la version installée (
node_exporter --version | awk ..., avecchanged_when: false,check_mode: false,failed_when: false– sur un hôte neuf, il n’y a pas de binaire et la réponse vide signifie « installer »). - Télécharger, décompresser, installer seulement si elle diffère de l’épinglage dans
versions.yml, avec la garde de simulationnot ansible_check_modesur les étapes qui ont besoin du fichier téléchargé. - Une unité systemd plus un drop-in de bac à sable, écrits avec
copyetcontent:, et deux handlers dans un ordre fixe : recharger systemd, puis redémarrer le service.
Une petite histoire se cache dans l’étape 3. L’archive servait autrefois de marqueur « déjà installé » : ne télécharger que si /tmp/node_exporter.tar.gz manque. Mais /tmp est vidé à chaque reboot – donc chaque exécution après un reboot retéléchargeait et signalait un changement, alors que la bonne version était installée. Comparer la version installée à l’épinglage est le test honnête.
Node Exporter n’a pas de fichier de configuration ; tous les réglages sont des options en ligne de commande dans l’unité. Le rôle le signale, avec un avertissement valable pour tout rôle qui écrit un fichier avec content: : les lignes de commentaire du contenu font partie du fichier sur le serveur. Modifier un commentaire à cet endroit change l’unité et redémarre le service.
L’exporteur MariaDB ajoute une chose : des identifiants. Il les lit dans un fichier que lui seul peut lire (mode 0600, appartenant à son propre utilisateur), écrit avec no_log: true. L’utilisateur avec lequel il se connecte est le compte en lecture seule de la partie 4 – avec son propre mot de passe, plus celui de root. Son meta/argument_specs.yml est celui que les autres rôles citent comme exemple expliqué : au démarrage du rôle, Ansible vérifie chaque option listée, les options obligatoires doivent exister, chaque valeur doit correspondre à son type, et no_log sur une option la garde hors du message d’erreur de validation.
🗄️ common_prometheus : la base de séries temporelles
Prometheus s’installe de la même façon que les exporteurs – utilisateur système, répertoires pour la base, binaire épinglé, unité systemd, drop-in de bac à sable. Le commentaire d’en-tête du rôle est le plus accessible aux débutants de tout le dépôt : il explique register, loop, les handlers, le mode check et la différence entre template et copy depuis le début. Chaque tâche qui modifie quelque chose que lit Prometheus notifie le même handler, restart prometheus – un nouveau binaire et une nouvelle configuration ne font toujours qu’un seul redémarrage.
Une configuration de collecte, deux piles
scrape_configs:
- job_name: "node"
static_configs:
- targets: ["127.0.0.1:9100"]
labels:
instance: "{{ inventory_hostname }}"
{% if stack == 'nextcloud' %}
- job_name: "mysqld_nextcloud"
static_configs:
- targets: ["127.0.0.1:9104"]
- job_name: "php_fpm_nextcloud"
static_configs:
- targets: ["127.0.0.1:<nodeport>"]
{% endif %}
{% if stack == 'wordpress' %}
- job_name: "mysqld_wordpress"
...
- job_name: "php_fpm_wordpress"
...
{% endif %}Encore la variable stack de la partie 1. Jusqu’à ce que les jobs deviennent propres à chaque pile, les quatre jobs applicatifs étaient rendus sur chaque serveur, et chaque serveur affichait en permanence deux cibles « down » – les ports de l’autre pile. Sans danger, mais du bruit précisément dans la vue où l’on cherche les vrais problèmes.
Des règles d’alerte sans mail
Le template définit aussi des règles d’alerte : disque sous 20 et sous 10 pour cent libres, mémoire sous 10 pour cent, CPU au-dessus de 90 pour cent pendant dix minutes, Node Exporter injoignable. Prometheus les évalue et les affiche sur sa page /alerts – mais n’envoie pas de mail. Il faudrait pour cela un second composant, Alertmanager, qui n’est volontairement pas installé. Les mails viennent à la place de Grafana et de Monit, et le fichier le dit dès ses premières lignes, pour que personne n’attende un mail qui ne peut pas venir.
📊 common_grafana : des courbes, derrière un sous-chemin, et du mail
Grafana est la seule partie de la supervision avec une interface web, et le rôle qui le configure est un bon cas d’étude pour parler à une application qui n’a pas de module Ansible. Ses trois fichiers s’exécutent dans l’ordre, parce que le deuxième et le troisième ont besoin d’un Grafana en marche :
install.yml : le paquet et quelques lignes ini
Grafana vient de son dépôt officiel, versions bêta exclues. Ensuite, lineinfile définit des lignes individuelles dans grafana.ini : l’utilisateur et le mot de passe d’administration (depuis le vault, avec no_log), pas de changement de mot de passe forcé à la première connexion, écoute sur toutes les interfaces – parce que l’Ingress le joint depuis le réseau des pods via le Service externe des parties 3 et 4 – et les deux réglages pour tourner sous un sous-chemin, root_url et serve_from_sub_path. Sans eux, Grafana générerait des liens vers /login au lieu de /grafana/login.
- name: Grafana | Wait for API to become available
ansible.builtin.uri:
url: "http://127.0.0.1:3000/api/health"
status_code: 200
register: grafana_health
check_mode: false
until: grafana_health.status == 200
retries: 30
delay: 3Le module uri fait des requêtes HTTP depuis le serveur – ici vers Grafana lui-même, sur localhost. Avec until, il interroge le point de santé jusqu’à ce que Grafana réponde, 90 secondes au plus.
dashboards.yml : vérifier, puis créer
L’API de Grafana n’a pas d’idempotence propre : « créer une source de données » deux fois crée deux sources de données. Le rôle sécurise cela avec une paire de requêtes :
- name: Grafana | Check if Prometheus datasource already exists
ansible.builtin.uri:
url: "http://127.0.0.1:3000/api/datasources/name/Prometheus"
user: "..."
password: "..."
force_basic_auth: true
status_code: [200, 404]
register: grafana_ds_check
- name: Grafana | Create Prometheus datasource
ansible.builtin.uri:
url: "http://127.0.0.1:3000/api/datasources"
method: POST
body_format: json
body: { name: Prometheus, type: prometheus, url: "http://127.0.0.1:9090", ... }
when: grafana_ds_check.status == 404status_code: [200, 404] déclare les deux réponses attendues – « existe » et « n’existe pas » –, si bien que seule une vraie erreur (401, 500) fait échouer la tâche. La requête de création ne s’exécute que sur 404. C’est « demander d’abord, agir ensuite » pour une API HTTP.
Les trois tableaux de bord (métriques de l’hôte, MySQL, PHP-FPM) viennent de grafana.com et sont importés via l’API. Le JSON téléchargé est relu avec slurp – en Base64, d’où | b64decode | from_json – et fusionné avec un identifiant fixe grâce au filtre combine, si bien que chaque import écrase le même tableau de bord au lieu d’en ajouter une copie. Tout le bloc d’import est sauté en mode check. Un petit script Python ajuste ensuite les variables d’un tableau de bord aux noms de jobs de cette installation. Une limite est indiquée dans le rôle : uri ne signale jamais « changed » de lui-même, donc les écritures via l’API n’apparaissent pas dans le récapitulatif – les paires vérifier/créer empêchent seulement qu’elles se répètent. Un quatrième tableau de bord, Container-Ressourcen, n’est pas téléchargé : son JSON se trouve dans le dépôt (files/container-resources.json), porte lui-même son identifiant fixe et est envoyé tel quel.
alerting.yml : du mail par le même relais
Pour le mail, Grafana a besoin de réglages SMTP, et le rôle utilise le même relais que les mails du serveur lui-même (partie 2) ; il n’y a donc pas de second compte de messagerie. Les réglages sont écrits avec blockinfile : il gère tout un bloc de lignes entre deux commentaires marqueurs et, à l’exécution suivante, remplace exactement ce bloc. Puis meta: flush_handlers redémarre Grafana immédiatement, parce que les tâches suivantes configurent l’alerting via le Grafana en marche, qui doit déjà savoir envoyer des mails. Une seconde attente du point de santé suit le redémarrage.
Ensuite, toujours avec vérifier-puis-créer : un point de contact par e-mail, la politique de notification par défaut qui pointe vers lui, un dossier pour les règles, et les règles elles-mêmes – écrites par un script Python que le rôle dépose sur le serveur et exécute, avec les identifiants passés en variables d’environnement pour cette seule tâche. Les règles :
| Alerte | Se déclenche quand |
|---|---|
| Partition racine | plus de 80 % utilisés, pendant 5 minutes |
| Partition de données | plus de 80 % utilisés, pendant 5 minutes |
| Mémoire | plus de 90 % utilisés, pendant 5 minutes |
| CPU | charge moyenne au-dessus de 85 %, pendant 5 minutes |
« Pendant 5 minutes » compte : un pic isolé – une sauvegarde, une génération d’aperçus – ne réveille personne.
📦 common_kube_metrics : une courbe par conteneur
Node Exporter voit l’hôte dans son ensemble. Que le conteneur PHP soit bridé à sa limite CPU pendant que l’hôte ne fait rien, ou qu’un pod approche lentement de sa limite mémoire, reste invisible – le 16/10/2026, il n’existait pas une seule série par conteneur. Depuis la version v3.3.0, deux sources supplémentaires comblent ce manque :
- cAdvisor est intégré au kubelet, rien à installer. Il publie CPU, mémoire, bridage et événements OOM de chaque conteneur sur
https://127.0.0.1:10250/metrics/cadvisor– mais seulement pour un appelant muni d’un jeton. Le rôle crée un ServiceAccountprometheus-scraperdont le seul droit estgetsurnodes/metrics, ainsi qu’un Secret de typeservice-account-token.common_prometheusle lit aveck8s_info(avec des répétitions, jusqu’à ce que le contrôleur de jetons l’ait rempli) et l’écrit dans/etc/prometheus/kubelet-token. - kube-state-metrics transforme les objets du serveur d’API en chiffres : limites et requests, redémarrages, raison de la dernière fin d’un conteneur. Il tourne dans
kube-systemavec des droits en lecture seule, sans root et avec un système de fichiers racine en lecture seule, derrière le NodePort 30808 – en local uniquement, comme les autres exportateurs.
- job_name: "kubelet_cadvisor"
scheme: https
metrics_path: /metrics/cadvisor
authorization:
credentials_file: /etc/prometheus/kubelet-token
tls_config:
insecure_skip_verify: true # K3s's self-signed kubelet certificate, 127.0.0.1 only
static_configs:
- targets: ["127.0.0.1:10250"]
metric_relabel_configs:
- source_labels: [container]
regex: "|POD" # pause containers and pod-level sums
action: dropLa règle de relabel écarte environ la moitié des séries avant leur stockage. Le quatrième tableau de bord Grafana, Container-Ressourcen, rapporte chaque conteneur à sa propre limite – part de CPU, bridage, mémoire, redémarrages, OOM kills – et quatre alertes par mail surveillent la même chose : mémoire au-dessus de 90 % de la limite, bridage au-dessus de 50 %, un OOM kill, plus de deux redémarrages en 15 minutes.
🐕 common_monit : le chien de garde
Monit existe à cause d’un incident raconté dans son propre article : fail2ban était mort sur le blog et personne ne s’en est aperçu pendant deux jours. Les métriques n’ont pas aidé – un fail2ban mort n’apparaît pas sous forme de courbe. Monit vérifie des états : toutes les deux minutes, il exécute une liste de vérifications, et il n’envoie un mail que lorsqu’une vérification passe de ok à échec, ou inversement. Un serveur en bonne santé n’envoie rien.
Le rôle, pas à pas
assertsur les réglages de mail. Monit envoie ses propres mails par le relais ; sans identifiants, ce serait un chien de garde incapable d’aboyer. Mieux vaut échouer au début du rôle avec un message qui dit quelles valeurs du vault manquent.- Découvrir quels services existent ici. Le même rôle tourne sur les deux piles, mais tous les services n’existent pas sur chaque hôte (l’exporteur MariaDB seulement sur Nextcloud). Donc :
- name: Monit | Detect which candidate services exist on this host
ansible.builtin.command: "systemctl cat {{ item }}.service"
register: monit_unit_check
changed_when: false
failed_when: false
loop: "{{ monit_candidate_services }}"
- name: Monit | Build the list of present services
ansible.builtin.set_fact:
monit_present_services: >-
{{ monit_candidate_services
| zip(monit_unit_check.results)
| selectattr('1.rc', 'equalto', 0)
| map(attribute='0') | list }}systemctl cat affiche un fichier d’unité et sort avec un code non nul si l’unité n’existe pas – une manière en lecture seule de demander « est-ce installé ? ». La seconde tâche est un morceau compact de Jinja : zip associe chaque nom candidat à son résultat (élément 0 = nom, élément 1 = résultat), selectattr('1.rc', 'equalto', 0) garde les paires dont le résultat avait le code de sortie 0, et map(attribute='0') en ressort les noms. Le template de configuration n’écrit ensuite une vérification que pour ceux-là.
- Étape 3 – scripts de vérification et configuration depuis
files/et deux templates. Les scripts sont petits et chacun répond à une question par son code de sortie : ce service est-il actif ? La table du pare-feu est-elle chargée avec ses sets de bannissement ? Un pod plante-t-il en boucle ou n’arrive-t-il pas à tirer son image (les brefs états « creating » sont ignorés) ? Les jails de fail2ban lisent-elles le journal actuel de l’Ingress ? Un certificat est-il à moins de trois semaines de son expiration ? - Étape 4 – valider, puis démarrer.
monit -tvérifie la configuration ; le rôle démarre Monit et confirme qu’il est actif – avec unfailed_whenqui lit la sortie desystemctl is-active.
Ce que surveille Monit
| Vérification | Alerte quand | Pourquoi ce seuil |
|---|---|---|
| Système | charge ou mémoire trop élevée pendant trois cycles | les pics courts sont normaux |
| Système de fichiers racine | plus de 90 % d’espace ou d’inodes | la dernière ligne, après les 80 % de Grafana |
| fail2ban | ne tourne pas – Monit le redémarre ; abandonne et alerte après trois redémarrages en peu de temps | l’incident qui a tout déclenché |
| Services | SSH, K3s, Node Exporter, Prometheus, Grafana et – sur Nextcloud – l’exporteur MariaDB non actifs | seulement ceux qui existent sur cet hôte |
| Pare-feu | la table ou ses sets de bannissement manquent | un pare-feu sans sa table ne protège rien |
| Pods | plantage en boucle, erreur de récupération d’image, OOM | des pods en attente pendant un redémarrage ne sont pas une erreur |
| Jails de fail2ban | une jail lit un journal de l’Ingress périmé | le problème des jails aveugles de la partie 5 |
| Certificats | moins de 21 jours restants | le renouvellement commence à 30 jours ; 21 signifie qu’il échoue depuis plus d’une semaine – assez tôt pour réparer |
| Sauvegarde | la dernière sauvegarde réussie date de plus de 3 jours | la sauvegarde est récupérée depuis l’extérieur ; c’est ainsi que le serveur sait qu’elle a eu lieu |
Deux vérifications ont leur propre planning, en syntaxe cron : certificats et sauvegardes une fois par heure au lieu de chaque cycle. Ouvrir une connexion TLS vers chaque certificat toutes les deux minutes n’apporte rien. La vérification de sauvegarde est une jolie astuce : la tâche de sauvegarde externe touche un fichier sur le serveur après chaque exécution réussie, et Monit alerte quand l’horodatage de ce fichier devient trop ancien. Le serveur ne peut pas voir la sauvegarde, mais il peut voir que personne ne lui en a parlé.
Le seuil des certificats a une histoire. Un jour, une politique d’images (partie 3) a bloqué en silence le solveur de challenge de cert-manager, et aucun certificat n’a pu être renouvelé. Personne ne l’a remarqué avant qu’un certificat soit près d’expirer. Désormais, Monit le remarque trois semaines à l’avance.
Un autotest qui envoie de vrais mails
Comment savoir que le chien de garde aboie ? tasks/selftest.yml arrête brièvement Node Exporter, attend que Monit signale la panne, puis le redémarre – ce qui envoie un vrai mail d’alerte et un vrai mail de rétablissement. C’est perturbant, donc cela ne s’exécute qu’à la demande, depuis les rôles de vérification :
ansible-playbook blog.yml --limit <host> -e monit_selftest=trueAjout du 19/10/2026 : depuis la rédaction de cette partie, la supervision s’est étoffée. common_selinux_monitoring donne à Prometheus, aux exportateurs et à Grafana leurs propres domaines SELinux (d’abord en phase d’apprentissage permissive), Monit tourne dans un bac à sable systemd, et Grafana journalise en warn au lieu de info. Deux nouveaux rôles, d’abord sur le blog : common_loki (Loki + Grafana Alloy : journal, journaux des pods, journaux d’audit et de fail2ban, 14 jours, avec des e-mails pour les accès SELinux bloqués, les erreurs fail2ban, les OOM kills du noyau et l’absence de journaux) et common_trivy (un scan nocturne de toutes les images en cours ; un e-mail seulement pour les nouvelles vulnérabilités CRITICAL/HIGH corrigeables). Chacun aura son propre deep dive.
🛠️ En exploitation : qui envoie quoi
Tous les mails de toute la visite, au même endroit – c’est le tableau à garder à côté de la boîte de réception :
| De | Signification | Partie | |
|---|---|---|---|
| Rôle de vérification | résultat du déploiement | après chaque exécution du playbook : toutes les vérifications vertes, ou celles en échec | 1, 3, 4 |
| dnf-automatic | redémarrage après mise à jour de sécurité / redémarrage toujours nécessaire | mises à jour nocturnes | 2 |
| Grafana | disque, mémoire ou CPU au-dessus du seuil | une ressource s’épuise – planifier, pas paniquer | 6 |
| Monit | service, pod, pare-feu, jail, certificat, sauvegarde | quelque chose est cassé maintenant – regarder | 6 |
| Alerte SSH | nouvelle connexion interactive | si ce n’était pas vous : incident | 5 |
| auditd | k3s_manifests, token, tls, identity | quelque chose qui ne change jamais a changé : incident jusqu’à explication | 5 |
| AIDE | changements d’intégrité des fichiers | lire la liste ; attendu après des changements planifiés | 5 |
| rkhunter | avertissements | généralement un changement de paquet ; sinon enquêter | 5 |
| ClamAV | fichiers infectés trouvés | regarder le fichier et qui l’a envoyé | 5 |
Regarder
# Monit's view of everything it watches
monit summary
# Prometheus: are all targets up?
curl -s http://127.0.0.1:9090/api/v1/targets | python3 -m json.tool | grep -E '"job"|"health"'
# Prometheus: which of its own alert rules are firing?
curl -s http://127.0.0.1:9090/api/v1/alerts | python3 -m json.tool
# Grafana: https://<your host>/grafana/ – dashboards "Node Exporter Full", MySQL, PHP-FPM, Container-RessourcenLire les courbes
- Disque : regardez la pente, pas le niveau. Une ligne plate à 70 % convient pendant des mois ; une ligne qui monte de deux pour cent par semaine atteint l’alerte des 80 % à une date prévisible.
- Mémoire : Linux utilise la mémoire libre comme cache de fichiers, donc « utilisée » est toujours élevée. Ce qui compte, c’est « disponible ». Si elle diminue régulièrement, quelque chose grossit.
- PHP-FPM : si tous les workers sont occupés et que les requêtes font la queue, le site est lent, quelle que soit la vitesse du serveur. Plus de workers demandent plus de mémoire – voir le dimensionnement de la partie 2.
- MariaDB : les requêtes lentes apparaissent dans le journal des requêtes lentes, pas seulement dans Grafana. Mesurer d’abord, optimiser ensuite est un article justement là-dessus.
🏁 La fin de la visite
Voilà chaque rôle du dépôt – 36 en tout, quatre playbooks, l’inventaire qui les relie et les tests qui les entourent. Si vous avez lu les six parties, vous savez ce qui tourne sur ces serveurs et pourquoi, comment chaque pièce est arrivée là, ce que fait le code à chaque étape et quoi faire quand un mail arrive. C’était le but : non pas que vous copiiez cette installation, mais qu’elle devienne assez compréhensible pour que vous puissiez l’exploiter – ou construire la vôtre et éviter les erreurs consignées dans ses commentaires.
Les rôles continueront de changer. Les liens de cette série pointent vers la version v3.3.0, pour qu’ils continuent de montrer le code que décrit le texte. Certains sujets méritent plus qu’une section – nftables, fail2ban, SELinux et K3s auront chacun leur propre article, sur l’outil lui-même plutôt que sur ce dépôt. Ils seront liés ici.

