# Stack de monitoring homelab — Prometheus + Grafana + Cloudflare Tunnel

## Contexte et besoin

Gérer plusieurs serveurs — Pi local, serveur NAS, VPS chez différents hébergeurs — sans supervision centralisée revient à naviguer à l'aveugle. Disque plein, service tombé, pic CPU : on l'apprend par l'incident, pas avant.

Les besoins concrets :
- Visualiser CPU, RAM, disque et uptime de chaque machine en un coup d'œil
- Être alerté avant saturation plutôt qu'après
- Accéder au dashboard depuis n'importe où, sans VPN
- Ne pas exposer de ports entrants sur le routeur domestique

## Solution retenue

| Besoin | Outil |
|---|---|
| Collecte des métriques sur chaque serveur | **Node Exporter** (agent léger, port 9100) |
| Agrégation et stockage time-series | **Prometheus** (scrape toutes les 15s) |
| Visualisation et dashboards | **Grafana** |
| Exposition publique sécurisée sans port ouvert | **Cloudflare Tunnel** + **Cloudflare Access** |

Tout tourne sur un Raspberry Pi via Docker Compose. Les serveurs distants sont scrapés directement ou via tunnel SSH selon les contraintes réseau de chaque hébergeur.

---

## Architecture

```
                          INTERNET
                              │
                 ┌────────────▼────────────┐
                 │     Cloudflare Access    │
                 │  (auth avant d'arriver)  │
                 └────────────┬────────────┘
                              │ HTTPS
                 ┌────────────▼────────────┐
                 │       Raspberry Pi       │
                 │                          │
                 │  cloudflared ──────────► Cloudflare
                 │       │                  │
                 │  ┌────▼────┐             │
                 │  │ Grafana │:3000        │
                 │  └────┬────┘             │
                 │  ┌────▼──────┐           │
                 │  │Prometheus │:9090      │
                 │  └────┬──────┘           │
                 │  ┌────▼──────┐           │
                 │  │node-exp.  │:9100      │
                 └──────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              │               │               │
       ┌──────▼─────┐  ┌──────▼─────┐  ┌─────▼──────┐
       │Serveur local│  │  VPS :9100 │  │VPS SSH only│
       │   :9100    │  │ou proxy :80│  │ SSH tunnel │
       └────────────┘  └────────────┘  └────────────┘
```

**Rôles :**
- **Node Exporter** — agent sur chaque serveur, expose CPU/RAM/disque/réseau sur `:9100/metrics`
- **Prometheus** — scrape les métriques toutes les 15s et les stocke (time-series DB)
- **Grafana** — visualise les données de Prometheus via dashboards
- **Cloudflare Tunnel** — expose Grafana publiquement sans ouvrir de port entrant sur le routeur

---

## 1. Node Exporter — installation sur chaque serveur

### Avec sudo (Debian/Ubuntu)

```bash
sudo apt-get install -y prometheus-node-exporter
sudo systemctl enable --now prometheus-node-exporter
```

Écoute sur `0.0.0.0:9100` par défaut. Vérification : `curl http://localhost:9100/metrics | head -5`

### Sans sudo (service utilisateur)

```bash
# Téléchargement
VERSION=1.8.2
ARCH=$(dpkg --print-architecture)  # amd64 ou arm64
wget -q https://github.com/prometheus/node_exporter/releases/download/v${VERSION}/node_exporter-${VERSION}.linux-${ARCH}.tar.gz -O /tmp/ne.tar.gz
tar xzf /tmp/ne.tar.gz -C /tmp
mkdir -p ~/.local/bin
cp /tmp/node_exporter-${VERSION}.linux-${ARCH}/node_exporter ~/.local/bin/

# Service systemd utilisateur
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/node-exporter.service << 'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network.target

[Service]
ExecStart=%h/.local/bin/node_exporter --web.listen-address=0.0.0.0:9100
Restart=always

[Install]
WantedBy=default.target
EOF

loginctl enable-linger $USER   # démarre au boot sans session active
systemctl --user daemon-reload
systemctl --user enable --now node-exporter
```

---

## 2. Stack monitoring sur le Pi — Docker Compose

### Arborescence

```
~/monitoring/
├── docker-compose.yml
├── prometheus.yml
├── tunnel-entrypoint.sh      # si tunnel SSH nécessaire
└── provisioning/
    └── datasources/
        └── prometheus.yml
```

### docker-compose.yml

```yaml
services:

  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    networks:
      - monitoring_default
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
    restart: unless-stopped

  node-exporter-pi:
    image: prom/node-exporter:latest
    container_name: node-exporter-pi
    networks:
      - monitoring_default
    pid: host
    volumes:
      - /:/host:ro,rslave
    command:
      - '--path.rootfs=/host'
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    networks:
      - monitoring_default
      - portainer_default    # si cloudflared est dans ce réseau
    volumes:
      - grafana_data:/var/lib/grafana
      - ./provisioning:/etc/grafana/provisioning
    environment:
      - GF_SERVER_HTTP_PORT=3000
      - GF_SERVER_ROOT_URL=https://grafana.mondomaine.com
      - GF_USERS_ALLOW_SIGN_UP=false
      - GF_SECURITY_COOKIE_SECURE=true
      - GF_SECURITY_STRICT_TRANSPORT_SECURITY=true
    restart: unless-stopped

networks:
  monitoring_default:
    driver: bridge
  portainer_default:
    external: true

volumes:
  prometheus_data:
  grafana_data:
```

> **Note réseau :** Grafana doit être dans le même réseau Docker que cloudflared pour que le tunnel puisse l'atteindre via `http://grafana:3000`.

### prometheus.yml

```yaml
global:
  scrape_interval: 15s

scrape_configs:

  - job_name: 'pi-home'
    static_configs:
      - targets: ['node-exporter-pi:9100']   # container dans le même réseau Docker

  - job_name: 'serveur-local'
    static_configs:
      - targets: ['192.168.1.X:9100']         # réseau local, port direct

  - job_name: 'vps-standard'
    static_configs:
      - targets: ['IP_VPS:9100']              # VPS avec port 9100 accessible

  - job_name: 'vps-proxy'
    metrics_path: /metrics
    static_configs:
      - targets: ['IP_VPS:80']               # VPS avec proxy nginx sur port 80

  - job_name: 'vps-tunnel'
    static_configs:
      - targets: ['tunnel-container:9100']   # VPS via tunnel SSH
```

### provisioning/datasources/prometheus.yml

Grafana charge automatiquement cette datasource au démarrage — pas besoin de configurer manuellement dans l'UI.

```yaml
apiVersion: 1
datasources:
  - name: Prometheus
    type: prometheus
    url: http://prometheus:9090   # nom du container, même réseau Docker
    isDefault: true
    editable: true
```

### Démarrage

```bash
cd ~/monitoring
docker compose up -d
docker compose ps   # vérifier que tout est "Up"
```

---

## 3. Firewall — trois cas de figure

### Cas 1 — Port 9100 accessible

Ouvrir uniquement depuis l'IP du Pi (ne jamais ouvrir à `0.0.0.0`) :

```bash
sudo ufw allow from IP_PI to any port 9100 proto tcp comment 'Prometheus'
sudo ufw reload
```

**Diagnostic depuis le Pi :**
```bash
nc -zv -w 5 IP_VPS 9100
# "succeeded"     → OK
# "timed out"     → bloqué par le provider (cas 2)
# "refused"       → port fermé côté serveur
```

### Cas 2 — Port 9100 bloqué par le provider, mais 80/443 accessible

Proxy Nginx sur le VPS qui relaie `/metrics` vers node-exporter local :

```nginx
# /etc/nginx/sites-available/node-exporter
server {
    listen 80;
    server_name IP_VPS;

    location /metrics {
        allow IP_PI;
        deny all;
        proxy_pass http://127.0.0.1:9100/metrics;
    }

    location / { return 404; }
}
```

```bash
sudo ln -s /etc/nginx/sites-available/node-exporter /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```

Dans `prometheus.yml` : `targets: ['IP_VPS:80']` + `metrics_path: /metrics`

### Cas 3 — Seul le port 22 (SSH) est accessible

Tunnel SSH dans un container Docker dédié. Le container crée un tunnel persistant et expose le port 9100 du VPS dans le réseau Docker du Pi. Prometheus scrape le container comme s'il était local.

```
Pi                              VPS distant
tunnel-container:9100 ──SSH──► localhost:9100
        ▲
Prometheus scrape
```

**tunnel-entrypoint.sh :**

```bash
#!/bin/sh
apk add --no-cache openssh-client > /dev/null 2>&1
while true; do
  ssh \
    -o StrictHostKeyChecking=no \
    -o ServerAliveInterval=30 \
    -o ExitOnForwardFailure=yes \
    -i /root/.ssh/id_rsa \
    -N -L 0.0.0.0:9100:localhost:9100 \
    user@IP_VPS
  echo "Tunnel coupé, reconnexion dans 5s..."
  sleep 5
done
```

**Service dans docker-compose.yml :**

```yaml
  tunnel-vps:
    image: alpine:latest
    container_name: tunnel-vps
    networks:
      - monitoring_default
    volumes:
      - /home/user/.ssh/id_rsa:/root/.ssh/id_rsa:ro
      - ./tunnel-entrypoint.sh:/entrypoint.sh:ro
    entrypoint: ["/entrypoint.sh"]
    restart: unless-stopped
```

La clé SSH doit être copiée sur le Pi au préalable et avoir les bons droits :
```bash
chmod 600 ~/.ssh/id_rsa
```

---

## 4. Cloudflare Tunnel — exposition sécurisée

Cloudflare Tunnel crée une connexion **sortante** chiffrée du Pi vers Cloudflare. Aucun port entrant n'est ouvert sur le routeur. Le trafic entre par Cloudflare et ressort vers Grafana via le tunnel.

### Déploiement cloudflared

```bash
docker run -d \
  --name cloudflared \
  --restart unless-stopped \
  --network portainer_default \   # même réseau que Grafana
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run --token TOKEN
```

Le token se génère dans **Cloudflare Zero Trust → Networks → Tunnels → Create a tunnel**.

### Configuration du hostname public

**Zero Trust → Networks → Tunnels → votre tunnel → Public Hostnames → Add :**

| Champ | Valeur |
|---|---|
| Subdomain | `grafana` |
| Domain | `mondomaine.com` |
| Service type | HTTP |
| URL | `grafana:3000` (nom container) |

> **Piège fréquent :** Si cloudflared et Grafana ne sont pas dans le même réseau Docker, utiliser le nom de container ne fonctionnera pas. Vérifier avec `docker network inspect NOM_RESEAU`.

### Cloudflare Access — authentification avant Grafana

Sans Access, l'URL est publique. Access ajoute une page d'auth Cloudflare devant l'application.

**Zero Trust → Access → Applications → Add → Self-hosted :**

```
Application domain : grafana.mondomaine.com
Policy             : Allow
Condition          : Emails → liste d'emails autorisés
MFA                : Recommandé (TOTP ou hardware key)
```

Flux résultant :
```
Navigateur → grafana.mondomaine.com
           → Cloudflare Access (login email + MFA)
           → cloudflared → Grafana (login Grafana)
```

---

## 5. Requêtes PromQL — référence

```promql
# CPU en %
100 - (avg(rate(node_cpu_seconds_total{mode="idle", job="NOM"}[2m])) * 100)

# RAM utilisée en %
100 - (node_memory_MemAvailable_bytes{job="NOM"} / node_memory_MemTotal_bytes{job="NOM"} * 100)

# Disque / utilisé en %
100 - (
  node_filesystem_avail_bytes{job="NOM", mountpoint="/", fstype!="tmpfs"} /
  node_filesystem_size_bytes{job="NOM", mountpoint="/", fstype!="tmpfs"} * 100
)

# Uptime en secondes (afficher avec unité "s" dans Grafana)
node_time_seconds{job="NOM"} - node_boot_time_seconds{job="NOM"}

# Statut up/down
up{job="NOM"}   # 1 = up, 0 = down
```

---

## 6. Récapitulatif sécurité

| Surface | Accès | Protection |
|---|---|---|
| Grafana (`:3000`) | Interne Docker uniquement | Cloudflare Access + auth Grafana |
| Prometheus (`:9090`) | Interne Docker uniquement | Pas exposé |
| Node Exporter Pi (`:9100`) | Interne Docker uniquement | Pas exposé |
| Node Exporter VPS | Port 9100 restreint ou proxy nginx | `ufw allow from IP_PI` |
| VPS backup | SSH tunnel | Aucun port supplémentaire ouvert |
| URL publique | HTTPS uniquement via Cloudflare | Cloudflare Access + MFA |

**Point de vigilance :** Les règles ufw `allow from IP_PI` sont liées à l'IP externe du Pi. Si le FAI change cette IP, les VPS ne seront plus scrapés. Vérifier périodiquement avec `curl -4 https://ifconfig.me` depuis le Pi.

---

## 7. Commandes utiles

```bash
# Vérifier l'état de tous les targets Prometheus
docker exec prometheus wget -qO- 'http://localhost:9090/api/v1/targets' | \
  python3 -c 'import sys,json; d=json.load(sys.stdin); \
  [print(t["labels"]["job"], t["health"]) for t in d["data"]["activeTargets"]]'

# Recharger la config Prometheus sans redémarrage
docker exec prometheus kill -HUP 1

# Mettre à jour la datasource Grafana via API
curl -X PUT -H 'Content-Type: application/json' \
  -u 'admin:PASSWORD' \
  http://GRAFANA_IP:3000/api/datasources/uid/UID \
  -d '{"name":"Prometheus","type":"prometheus","url":"http://prometheus:9090","isDefault":true,"access":"proxy"}'

# Logs du tunnel SSH
docker logs tunnel-vps --tail 20 -f

# Redémarrer uniquement Prometheus
docker compose restart prometheus
```