Stack Observabilité - edu-kit

Table des matières

Introduction, contexte, définitions et objectifs

La stack d’observabilité d’edu-kit permet de superviser l’ensemble de l’infrastructure : VMs, containers Docker, cluster k3s, firewalls MikroTik et hyperviseurs Proxmox.

Elle repose sur le modèle LGTM (Loki · Grafana · Tempo · Mimir/Prometheus), porté par l’écosystème Grafana :

Composant Rôle
Prometheus Collecte et stockage des métriques (séries temporelles)
Loki Agrégation et stockage des logs
Tempo Stockage des traces distribuées (OpenTelemetry)
Grafana Visualisation, dashboards, alerting
Grafana Alloy Agent de collecte universel déployé sur chaque VM (métriques + logs)
cAdvisor Métriques des containers Docker (CPU, RAM, réseau, I/O)
mktxp Exporter Prometheus pour les firewalls MikroTik (RouterOS)
pve_exporter Exporter Prometheus pour les hyperviseurs Proxmox

Tous les services de la stack tournent en containers Docker (Compose) dans le segment réseau dédié obsnet (192.168.50.0/24), isolé du reste de l’infrastructure.

Prérequis

Environnement

  • OS : Debian 12 (Bookworm) sur toutes les VMs obsnet
  • Docker : version 24+ avec le plugin docker-compose-plugin
  • Réseau : segment obsnet (192.168.50.0/24), accès inter-VMs autorisé
  • Firewall : règles MikroTik configurées (voir section Règles firewall)

VMs obsnet

VM IP Services
grafana 192.168.50.5 Grafana, cAdvisor, Alloy
prometheus 192.168.50.10 Prometheus, mktxp, pve_exporter, cAdvisor, Alloy
loki 192.168.50.15 Loki, cAdvisor, Alloy
tempo 192.168.50.20 Tempo, cAdvisor, Alloy
uptime-kuma 192.168.50.25 Uptime Kuma, Alloy

VMs avec Grafana Alloy (agents)

Alloy est installé sur 15 VMs (toutes sauf haproxy-02, suricata, netbird, docusaurus) :

  • config-base.alloy : métriques système + logs systemd (VMs sans Docker)
  • config-docker.alloy : base + cAdvisor + logs Docker (VMs obsnet avec Docker)
  • config-prometheus-vm.alloy : config Docker + scrape mktxp + scrape pve_exporter (VM prometheus uniquement)

Installation et configuration

Prometheus

Prometheus est déployé sur la VM prometheus (192.168.50.10) dans /opt/docker/prometheus/.

Il reçoit les métriques en remote write depuis les agents Alloy (push) et scrape également mktxp et pve_exporter (pull).

mkdir -p /opt/docker/prometheus
cd /opt/docker/prometheus
# Copier prometheus.yml et docker-compose.yml
docker compose up -d

docker-compose.yml :

services:
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    restart: unless-stopped
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    command:
      - "--config.file=/etc/prometheus/prometheus.yml"
      - "--storage.tsdb.path=/prometheus"
      - "--storage.tsdb.retention.time=30d"
      - "--web.enable-lifecycle"
      - "--web.enable-remote-write-receiver"  # nécessaire pour que les agents Alloy puissent pousser leurs métriques

volumes:
  prometheus_data:

prometheus.yml (configuration de base) :

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["localhost:9090"]

Les autres jobs de scrape (mktxp, pve) sont gérés directement par Alloy sur cette VM via config-prometheus-vm.alloy.

Vérification :

docker compose ps            # le container doit être "Up"
curl http://localhost:9090   # l'interface web doit répondre

Loki

Loki est déployé sur la VM loki (192.168.50.15) dans /opt/docker/loki/.

Il reçoit les logs en push depuis les agents Alloy.

mkdir -p /opt/docker/loki
cd /opt/docker/loki
docker compose up -d

docker-compose.yml :

services:
  loki:
    image: grafana/loki:latest
    container_name: loki
    restart: unless-stopped
    ports:
      - "3100:3100"
    volumes:
      - ./loki.yml:/etc/loki/loki.yml:ro
      - loki_data:/loki
    command: -config.file=/etc/loki/loki.yml

volumes:
  loki_data:

loki.yml :

auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

query_range:
  results_cache:
    cache:
      embedded_cache:
        enabled: true
        max_size_mb: 100

schema_config:
  configs:
    - from: 2020-10-24
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 30d   # conservation des logs sur 30 jours

compactor:
  working_directory: /loki/compactor
  retention_enabled: true
  delete_request_store: filesystem

Vérification :

curl http://localhost:3100/ready   # doit répondre "ready"

Tempo

Tempo est déployé sur la VM tempo (192.168.50.20) dans /opt/docker/tempo/.

Il reçoit les traces distribuées via le protocole OTLP (OpenTelemetry Protocol).

mkdir -p /opt/docker/tempo
cd /opt/docker/tempo
docker compose up -d

docker-compose.yml :

services:
  tempo:
    image: grafana/tempo:latest
    container_name: tempo
    restart: unless-stopped
    ports:
      - "3200:3200"
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
    volumes:
      - ./tempo.yml:/etc/tempo/tempo.yml:ro
      - tempo_data:/var/tempo
    command: -config.file=/etc/tempo/tempo.yml

volumes:
  tempo_data:

tempo.yml :

server:
  http_listen_port: 3200

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:4318

storage:
  trace:
    backend: local
    local:
      path: /var/tempo/blocks
    wal:
      path: /var/tempo/wal

Vérification :

curl http://localhost:3200/ready   # doit répondre "ready"

Grafana

Grafana est déployé sur la VM grafana (192.168.50.5) dans /opt/docker/grafana/.

L’interface est exposée en HTTPS via NPM (Nginx Proxy Manager) à l’adresse https://grafana.int.edu-kit.fr.

mkdir -p /opt/docker/grafana/provisioning
cd /opt/docker/grafana
# Copier docker-compose.yml, .env, et le dossier provisioning/
docker compose up -d

docker-compose.yml :

services:
  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    ports:
      - "3000:3000"
    env_file:
      - .env
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
      - GF_SERVER_ROOT_URL=https://grafana.int.edu-kit.fr
      - GF_SMTP_ENABLED=${GF_SMTP_ENABLED:-false}
      - GF_SMTP_HOST=${GF_SMTP_HOST:-localhost:25}
      - GF_SMTP_USER=${GF_SMTP_USER:-}
      - GF_SMTP_PASSWORD=${GF_SMTP_PASSWORD:-}
      - GF_SMTP_FROM_ADDRESS=${GF_SMTP_FROM_ADDRESS:-grafana@edu-kit.fr}
    volumes:
      - grafana_data:/var/lib/grafana
      - ./provisioning:/etc/grafana/provisioning:ro  # dashboards, datasources, alerting

volumes:
  grafana_data:

Fichier .env (à créer depuis .env.example, ne jamais committer le .env réel) :

GRAFANA_ADMIN_PASSWORD=changeme

# Teams (Power Automate webhook)
TEAMS_WEBHOOK_URL=https://your-power-automate-url

# SMTP (optionnel)
GF_SMTP_ENABLED=false
GF_SMTP_HOST=smtp.example.com:587
GF_SMTP_USER=
GF_SMTP_PASSWORD=
GF_SMTP_FROM_ADDRESS=grafana@edu-kit.fr

Datasources (provisioning automatique)

Le fichier provisioning/datasources/datasources.yml déclare les trois sources de données :

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    uid: prometheus
    url: http://192.168.50.10:9090
    isDefault: true
    jsonData:
      timeInterval: 15s

  - name: Loki
    type: loki
    uid: loki
    url: http://192.168.50.15:3100

  - name: Tempo
    type: tempo
    uid: tempo
    url: http://192.168.50.20:3200
    jsonData:
      tracesToLogsV2:
        datasourceUid: loki
        filterByTraceID: true
      lokiSearch:
        datasourceUid: loki
      serviceMap:
        datasourceUid: prometheus

Dashboards importés

Dashboard Description
infra-overview Vue globale CPU/RAM/Disque/Logs
network-traffic Trafic réseau IN/OUT par VM
disk-io Lecture/écriture disque, IOPS, latence
system-load Load average, processus, uptime
security-logs Erreurs auth, logs filtrés
logs-explorer Exploration logs systemd + Docker
traces-explorer Traces Tempo (actif quand apps instrumentées)
k3s-cluster Métriques nœuds k3s
docker-containers CPU/RAM/réseau par container + logs
mikrotik-chr (ID 13679) Métriques firewalls MikroTik

Vérification :

curl http://localhost:3000   # l'interface web doit répondre

Grafana Alloy

Alloy est l’agent de collecte universel installé sur chaque VM. Il remplace Prometheus Node Exporter + Promtail en un seul binaire.

Installation

Copier le fichier de config correspondant à la VM en config.alloy, puis exécuter install-alloy.sh en root :

# Depuis la machine locale, copier les fichiers nécessaires
scp config-base.alloy user@<ip-vm>:~/config.alloy
scp install-alloy.sh user@<ip-vm>:~/

# Sur la VM
sudo bash install-alloy.sh

Le script réalise les opérations suivantes :

  1. Ajoute le dépôt APT officiel de Grafana
  2. Installe le paquet alloy
  3. Copie config.alloy vers /etc/alloy/config.alloy
  4. Active et démarre le service systemd alloy

Attention CRLF : si le script a été édité sous Windows, il peut contenir des retours chariot \r qui provoquent une erreur à la ligne 9. Corriger avec sed -i 's/\r//' install-alloy.sh avant exécution.

Sur les VMs obsnet (grafana, loki, tempo, uptime-kuma, prometheus) : ajouter l’utilisateur alloy au groupe docker pour permettre la lecture du socket Docker :

sudo usermod -aG docker alloy
sudo systemctl restart alloy

Configuration - config-base.alloy (VMs sans Docker)

Collecte les métriques système via le node exporter intégré et les logs systemd via journald.

// Métriques système -> Prometheus
prometheus.exporter.unix "node" {}

prometheus.scrape "node" {
  targets    = prometheus.exporter.unix.node.targets
  forward_to = [prometheus.remote_write.default.receiver]
  job_name   = "node"
}

prometheus.remote_write "default" {
  endpoint {
    url = "http://192.168.50.10:9090/api/v1/write"
  }
  external_labels = {
    host = constants.hostname,
  }
}

// Logs systemd -> Loki
loki.relabel "journal" {
  forward_to = []
  rule {
    source_labels = ["__journal__systemd_unit"]
    target_label  = "unit"
  }
  rule {
    source_labels = ["__journal__priority_keyword"]
    target_label  = "level"
  }
}

loki.source.journal "default" {
  forward_to    = [loki.write.default.receiver]
  relabel_rules = loki.relabel.journal.rules
  labels = {
    job  = "systemd-journal",
    host = constants.hostname,
  }
}

loki.write "default" {
  endpoint {
    url = "http://192.168.50.15:3100/loki/api/v1/push"
  }
}

Configuration - config-docker.alloy (VMs avec Docker)

Étend la config de base avec le scrape cAdvisor et la collecte des logs Docker.

// ... (même config que config-base.alloy)

// Métriques containers Docker via cAdvisor
prometheus.scrape "cadvisor" {
  targets = [{
    __address__ = "localhost:8080",
    host        = constants.hostname,
  }]
  forward_to = [prometheus.remote_write.default.receiver]
  job_name   = "cadvisor"
}

// Logs Docker via socket
discovery.docker "containers" {
  host = "unix:///var/run/docker.sock"
}

loki.source.docker "containers" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.docker.containers.targets
  forward_to = [loki.write.default.receiver]
  labels = {
    job  = "docker",
    host = constants.hostname,
  }
}

Configuration - config-prometheus-vm.alloy (VM prometheus uniquement)

Étend config-docker.alloy avec le scrape de mktxp et pve_exporter.

// ... (même config que config-docker.alloy)

// Métriques MikroTik via mktxp
prometheus.scrape "mktxp" {
  targets = [{
    __address__ = "localhost:49090",
  }]
  forward_to      = [prometheus.remote_write.default.receiver]
  job_name        = "mktxp"
  scrape_interval = "60s"
}

// Métriques Proxmox VE via pve_exporter (multi-target)
prometheus.scrape "pve" {
  targets = [
    { __address__ = "localhost:9221", __param_target = "10.10.40.1", __param_module = "default", instance = "pve1" },
    { __address__ = "localhost:9221", __param_target = "10.10.40.2", __param_module = "default", instance = "pve2" },
    { __address__ = "localhost:9221", __param_target = "10.10.40.3", __param_module = "default", instance = "pve3" },
  ]
  forward_to      = [prometheus.remote_write.default.receiver]
  job_name        = "pve"
  metrics_path    = "/pve"
  scrape_interval = "60s"
}

Vérification Alloy :

systemctl status alloy            # le service doit être "active (running)"
curl http://localhost:12345       # interface de debug Alloy (disponible en local)
journalctl -u alloy -f            # logs en temps réel

cAdvisor

cAdvisor expose les métriques des containers Docker en cours d’exécution. Il est déployé sur les 5 VMs obsnet.

mkdir -p /opt/docker/cadvisor
cd /opt/docker/cadvisor
docker compose up -d

docker-compose.yml :

services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
    restart: unless-stopped
    privileged: true        # nécessaire pour accéder aux métriques kernel
    ports:
      - "8080:8080"
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:ro
      - /sys:/sys:ro
      - /var/lib/docker:/var/lib/docker:ro
      - /dev/disk:/dev/disk:ro

cAdvisor est ensuite scrapé par Alloy en local (localhost:8080) via config-docker.alloy.

Vérification :

curl http://localhost:8080/metrics | head   # doit retourner des métriques Prometheus

mktxp - Monitoring MikroTik

mktxp est un exporter Prometheus qui interroge les firewalls MikroTik via l’API RouterOS. Il est déployé sur la VM prometheus (192.168.50.10) dans /opt/docker/mktxp/.

Prérequis côté MikroTik

Créer un utilisateur dédié prometheus avec les droits minimaux sur les deux firewalls (fw-01 et fw-02) :

/user/group/add name=prometheus policy=api,read
/user/add name=prometheus password=CHANGE_ME group=prometheus
/ip/service/set api disabled=no

Déploiement

mkdir -p /opt/docker/mktxp
cd /opt/docker/mktxp
# Copier docker-compose.yml, mktxp.conf, _mktxp.conf
docker compose up -d

docker-compose.yml :

services:
  mktxp:
    image: ghcr.io/akpw/mktxp:latest
    container_name: mktxp
    restart: unless-stopped
    ports:
      - "49090:49090"
    volumes:
      - ./_mktxp.conf:/home/mktxp/mktxp/_mktxp.conf   # config globale (sans :ro - mktxp tente de la mettre à jour)
      - ./mktxp.conf:/home/mktxp/mktxp/mktxp.conf     # config des routeurs (sans :ro)

_mktxp.conf (paramètres globaux) :

[MKTXP]
    listen = '0.0.0.0:49090'      # doit inclure le port (sans port = waitress ne bind pas)
    socket_timeout = 30            # augmenté car les CHR sont lents (défaut 10 insuffisant)
    fetch_routers_in_parallel = False            # séquentiel plus stable sur CHR
    persistent_router_connection_pool = True     # réutilise la connexion API entre scrapes

mktxp.conf (déclaration des routeurs) :

[fw-01]
    hostname = 192.168.50.1

[fw-02]
    hostname = 192.168.50.2

[default]
    enabled = True
    port = 8728
    username = prometheus
    password = CHANGE_ME
    plaintext_login = True         # obligatoire sur RouterOS 7.x (MD5 challenge non supporté)

    # Collectors désactivés (timeout sur CHR RouterOS 7.x)
    interface = False
    firewall = False
    connections = False
    connection_stats = False
    monitor = False
    neighbor = False
    ipv6_firewall = False
    ipv6_neighbor = False
    health = False                 # pas de capteurs physiques sur CHR
    routing_stats = False          # non supporté sur 7.x

    # Collectors actifs
    system = True
    route = True
    dns = True
    user = True
    certificate = True
    pool = True
    public_ip = True
    installed_packages = True

Vérification :

curl http://localhost:49090/metrics | grep mktxp   # doit retourner des métriques
docker logs mktxp                                   # vérifier l'absence d'erreurs de connexion

pve_exporter - Monitoring Proxmox

pve_exporter expose les métriques des hyperviseurs Proxmox via l’API PVE. Il est déployé sur la VM prometheus dans /opt/docker/proxmox/.

Prérequis côté Proxmox

Créer un utilisateur et un token API sur le cluster Proxmox :

# Dans l'interface web Proxmox ou en CLI sur un nœud
pveum user add prometheus@pve
pveum aclmod / -user prometheus@pve -role PVEAuditor
pveum user token add prometheus@pve monitoring

Le token généré est à reporter dans pve.yml.

Réseau pvenet

Les hyperviseurs sont joignables depuis la VM prometheus via le VLAN 40 (pvenet, 10.10.40.0/24) :

  • pve1 : 10.10.40.1
  • pve2 : 10.10.40.2
  • pve3 : 10.10.40.3
  • VRRP VIP (fw-01) : 10.10.40.252

La route est configurée sur chaque nœud Proxmox dans l’interface vmbr1 :

# Dans /etc/network/interfaces sur chaque nœud pve
post-up ip route add 192.168.50.0/24 via 10.10.40.252

Déploiement

mkdir -p /opt/docker/proxmox
cd /opt/docker/proxmox
docker compose up -d

docker-compose.yml :

services:
  pve-exporter:
    image: prompve/prometheus-pve-exporter:latest
    container_name: pve-exporter
    restart: unless-stopped
    ports:
      - "9221:9221"
    volumes:
      - ./pve.yml:/etc/pve.yml:ro
    command:
      - "--config.file=/etc/pve.yml"

pve.yml :

default:
  user: prometheus@pve
  token_name: monitoring
  token_value: CHANGE_ME
  verify_ssl: false

Le scrape multi-target est géré par Alloy dans config-prometheus-vm.alloy avec l’endpoint /pve?target=<ip>.

Vérification :

curl "http://localhost:9221/pve?target=10.10.40.1&module=default" | head   # métriques pve1

Alerting Grafana

L’alerting est géré nativement par Grafana avec un provisioning via fichiers YAML dans provisioning/alerting/.

Important : toujours utiliser docker compose up -d --force-recreate pour recharger les variables d’environnement (notamment $TEAMS_WEBHOOK_URL). Un simple restart ne recharge pas le .env.

Contact point Teams

Les alertes sont envoyées via un webhook Power Automate vers Microsoft Teams.

provisioning/alerting/contact-points.yaml :

apiVersion: 1

contactPoints:
  - orgId: 1
    name: teams
    receivers:
      - uid: teams-webhook
        type: teams
        disableResolveMessage: false
        settings:
          url: $TEAMS_WEBHOOK_URL   # variable d'environnement injectée depuis .env

provisioning/alerting/notification-policy.yaml :

apiVersion: 1

policies:
  - orgId: 1
    receiver: teams
    group_by: [grafana_folder, alertname]
    group_wait: 30s
    group_interval: 5m
    repeat_interval: 4h

Règles d’alerte

Six règles provisionnées dans provisioning/alerting/rules.yaml :

Règle Seuil Durée Sévérité
CPU Warning > 80% 10 min warning
CPU Critique > 95% 5 min critical
Disque Warning > 70% 5 min warning
Disque Critique > 85% 5 min critical
RAM Warning > 80% 10 min warning
RAM Critique > 90% 5 min critical

Règles firewall MikroTik

Règles ajoutées sur fw-01 pour permettre la communication entre les composants :

Source Destination Port Objet
alloy-agents (address-list) prometheus (192.168.50.10) 9090 Remote write métriques
alloy-agents loki (192.168.50.15) 3100 Push logs
prometheus alloy-agents 12345 Debug interface Alloy
NPM (192.168.10.5) grafana (192.168.50.5) 3000 Accès interface web
prometheus (src-list) fw-01 / fw-02 8728 API RouterOS (mktxp)
prometheus pve1/2/3 8006, 8007 API Proxmox (pve_exporter)

L’address-list alloy-agents contient toutes les VMs où Alloy est installé, y compris les nœuds Proxmox (pve1/2/3).

DNS interne

Un enregistrement CNAME est configuré sur fw-01 pour l’accès à Grafana :

/ip/dns/static/add name=grafana.int.edu-kit.fr cname=npm.int.edu-kit.fr

Problèmes rencontrés

mktxp - HTTP 000 / waitress ne bind pas

Symptôme : curl http://localhost:49090 retourne une erreur de connexion refusée, les logs Docker ne montrent aucune erreur de démarrage.

Cause : la valeur listen = '0.0.0.0' dans _mktxp.conf sans le numéro de port empêche waitress (le serveur HTTP Python) de binder correctement.

Solution : spécifier explicitement le port dans le paramètre listen :

listen = '0.0.0.0:49090'

mktxp - Timeout des collectors sur CHR RouterOS 7.x

Symptôme : mktxp démarre mais Prometheus ne reçoit aucune métrique, les logs montrent des timeout récurrents.

Cause : plusieurs collectors (interface, firewall, connections, monitor, neighbor) effectuent des requêtes lourdes non supportées ou très lentes sur les CHR (Cloud Hosted Router) RouterOS 7.x.

Solution : désactiver les collectors problématiques dans mktxp.conf. Voir la section mktxp.conf pour la liste complète.


mktxp - Authentification échoue sur RouterOS 7.x

Symptôme : erreur d’authentification dans les logs mktxp malgré un user/password correct.

Cause : RouterOS 7.x ne supporte pas l’authentification MD5 challenge utilisée par défaut par mktxp.

Solution : ajouter plaintext_login = True dans la section [default] de mktxp.conf.


Grafana alerting - Variables .env non rechargées

Symptôme : $TEAMS_WEBHOOK_URL reste vide dans la configuration du contact point après modification du .env.

Cause : docker compose restart ne relit pas le fichier .env - il conserve les variables de l’environnement de création du container.

Solution : toujours utiliser --force-recreate pour forcer la relecture du .env :

docker compose up -d --force-recreate

pve_exporter - Incompatibilité PBS 4.2

Symptôme : le scrape de PBS (Proxmox Backup Server) via pbs_exporter v0.9.1 échoue sur l’endpoint /nodes.

Cause : incompatibilité de l’exporter avec l’API PBS 4.2.

Solution : monitoring PBS abandonné. La supervision de PBS devra être reprise avec une version compatible de l’exporter.

Sources


Retour en haut