Monitorización de contenedores Docker con cAdvisor, Prometheus y Grafana

Work in progress
Apuntes incompletos de monitorización de contenedores Docker

Monitorización

Después de conseguir instalar Jam Session en mi Docker y ver como montan la monitorización del sistema me han entrado ganas de probar a montar algo para todos los contenedores instalados en mi VPS.

Prometheus y cAdvisor

Inspirándonos en la configuración de monitorización que venía con Jam Session, vamos a intentar configurar un sistema propio. Vamos a usar también Grafana y Prometheus, pero añadiendo cAdvisor a la receta como nos proponen en la propia web de Prometheus (la combinación de cAdvisor, Prometheus y Grafana parece un estándar de facto en la industria)

cAdvisor es un software de Google, escrito en Go, y programado específicamente para la captura de métricas de contenedores; necesita acceso a varios directorios del host (que mapearemos con bind-mounts en Docker) para capturar los datos. cAdvisor expone el puerto 8080 (interfaz web y REST api) y por defecto permite a Prometheus acceder a varias métricas.

Prometheus es un toolkit de monitorización de métricas. Prometheus almacena las métricas como series temporales, a las que se pueden asignar pares etiqueta valor opcionales (parece un funcionamiento análogo al de InfluxDB) Soporta un lenguaje de consultas denominado PromQL. Puede capturar métricas por pulling o por pushing. Normalmente, en procesos de vida larga, Prometheus hace scrapping en los orígenes de métricas definidos, pero también implementa un push-gateway para procesos de vida muy corta.

Prometheus, igual que cAdvisor, expone un interfaz web a través del cual podemos hacer consultas con PromQL y ver los resultados en modo tabla o modo gráfico.

Pero esto es solo la punta del iceberg. Entre otras cosas:

  • Prometheus nos da la posibilidad de instalar otras dos aplicaciones: Alertmanager y Node Exporter
    • Alertmanager se encarga de gestionar las Alertas que Prometheus genere a partir de las métricas para canalizarlas a distintos sistemas de comunicación (nos da muchísimas opciones)
    • Node Explorer está especializado en monitorizar métricas de la máquina host.
  • Hay todo un ecosistema de bibliotecas y clientes para exportar métricas compatibles con Prometheus desde multitud de sistemas.

cAdvisor

Empezamos por configurar el contenedor de cAdvisor. Creamos un directorio mon para nuestros nuevos contenedores de monitorización, y en ese directorio nuestro fichero docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
version: '3.9'
services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
#    ports:
#    - 8080:8080
    volumes:
    - /:/rootfs:ro
    - /var/run:/var/run:rw
    - /sys:/sys:ro
    - /var/lib/docker/:/var/lib/docker:ro
    networks:
      backend:
        ipv4_address: 172.20.0.10
networks:
  backend:
    external: true

La configuración no tiene mucho misterio, hacemos bind-mounts de todos los recursos del host que necesita cAdvisor para recolectar métricas. Como queremos consumir todas las métricas en local comentamos el mapeo de puertos por que no queremos que el VPS deje acceso desde internet a las métricas. Por la misma razón Traefik no necesita saber nada de este contenedor (ya que no será accesible desde fuera)

En principio entiendo que cAdvisor captura las métricas directamente desde Docker así que de momento lo conectamos solo a la red backend.

Con esto tenemos todo listo para lanzar el servicio cAdvisor con dcupd (mi alias para docker-compose up -d). Para comprobar el contenedor desde fuera del VPS podemos hacerlo por un túnel ssh:

1
ssh -L 9081:172.20.0.10:8080 dockadmin@fomalhaut

Ahora podemos conectar nuestro navegador a http://localhost:9081 y comprobar que cAdvisor está funcionando y detecta nuestros contenedores en el VPS.

Web de cAdvisor

Aunque podemos ver métricas (e incluso gráficas) de todos los contenedores no es muy “amistoso” y está bastante limitado. Pero su función principal es recolectar las métricas de los contenedores y eso lo hace perfectamente.

Gráficas en la web de cAdvisor

Prometheus

Ya tenemos cAdvisor funcionando. Vamos a configurar Prometheus creando su fichero de configuración:

1
2
mkdir mon/prometheus
touch mon/prometheus/prometheus.yml

Con el contenido:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
global:
  # How frequently to scrape targets (by default 1m)
  scrape_interval: 15s
  # How long until a scrape request times out (by default 10s)
  # scrape_timeout: 10s
  # How frequently evaluate the rules (by default 1m)
  evaluation_interval: 15s
  
# Alerting section specifies settings related to Alertmanager
alerting:
  alertmanagers:
  - static_configs:
    - targets:
    # whatever you want

# Rule files specifies a list of globs. Rules and alerts are read from all matching files
# rule_files:
    
# List of scrape configurations
scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['prometheus:9090']
      labels:
        alias: 'prometheus'
  - job_name: 'cadvisor'
    static_configs:
    - targets: ['cadvisor:8080']
      labels:
        alias: 'cadvisor'

En la sección global configuramos los períodos de polling a los origenes de métricas y el período de evaluación de reglas de alarmas.

Dejamos preparada la sección alerting donde configuraremos las reglas de alarmas. Y la sección rule_files.

En la seccion scrape_configs configuramos orígenes de métricas que queremos capturar. De momento configuramos el propio contenedor prometheus, que publicará sus metricas en el puerto 9090, y configuramos también el contenedor cadvisor que como ya vimos expone sus métricas en el puerto 8080. Al definir los orígenes de las métricas usamos los nombres de red de los contenedores (dentro de la red Docker) y asignamos a cada uno una etiqueta que nos permita distinguir los datos más tarde.

Ahora nos toca levantar el contenedor de Prometheus, modificamos el fichero docker-compose.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
version: '3.9'

networks:
  frontend:
    external: true
  backend:
    external: true

volumes:
  prometheus_data:
    name: prometheus_data
  grafana_data:
    name: grafana_data

services:
  cadvisor:
    image: gcr.io/cadvisor/cadvisor:latest
    container_name: cadvisor
#    ports:
#      - 8080:8080
    volumes:
      - /:/rootfs:ro
      - /var/run:/var/run:rw
      - /sys:/sys:ro
      - /var/lib/docker/:/var/lib/docker:ro
    networks:
      backend:
        ipv4_address: 172.20.0.10

  nodeexporter:
    image: prom/node-exporter:v1.3.1
    container_name: nodeexporter
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
    restart: unless-stopped
    networks:
      backend:
        ipv4_address: 172.20.0.11

 prometheus:
    image: prom/prometheus:v2.32.1
    container_name: prometheus
#    ports:
#      - 9090:9090
    volumes:
#      - ${PWD}/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ${PWD}/prometheus/:/etc/prometheus/:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/usr/share/prometheus/console_libraries'
      - '--web.console.templates=/usr/share/prometheus/consoles'
    depends_on:
      - cadvisor
    networks:
      backend:
        ipv4_address: 172.20.0.12

Igual que con cAdvisor no queremos publicar el puerto de Prometheus en internet. En todo caso, si después nos interesa, lo expondremos via Traefik.

Igual que con cAdvisor podemos probar el acceso a Prometheus via túnel ssh. Una vez conectados al interfa web de Prometheus podemos comprobar el estado de los dos origenes de datos definidos para ver que los dos están activos (deberían).

Ejecutando:

1
ssh -L 9082:172.20.0.10:9090 dockadmin@fomalhaut

Podremos acceder a nuestro nuevo Prometheus en http://localhost:9082. Si queremos hacer pruebas de consultas en la documentación oficial tenemos unos cuantos ejemplos.

Orígenes de datos en la web de Prometheus

Grafana

Vamos a añadir Grafana a nuestra plataforma de monitorización. Grafana es un software especializado en la elaboración de cuadros de mando (_Dashboards) y representación gráfica de valores. Y lo hace francamente bien. Permite definir tanto los orígenes de datos como los propios Dashboards mediante ficheros de configuración o desde el interfaz web. Además los Dashboards pueden exportarse e importarse como ficheros .json. Incluso hay una página web donde se comparten los Dashboards entre usuarios.

La sección de Grafana en el fichero docker-compose.yml quedaría

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
grafana:
    image: grafana/grafana:8.3.3
    container_name: grafana
    user: "472"
    depends_on:
      - prometheus
#    ports:
#      - 3000:3000
    volumes:
      - grafana_data:/var/lib/grafana
      - ${PWD}/grafana/provisioning/:/etc/grafana/provisioning/
    env_file:
      - ${PWD}/grafana/config.env
    networks:
      backend:
        ipv4_address: 10.20.0.15
      frontend:
        ipv4_address: 10.21.0.15
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.docker.network=frontend"
      - "traefik.http.routers.grafana.rule=Host(`grafana.midominio.com`)"
      - "traefik.http.routers.grafana.entrypoints=https"
      - "traefik.http.routers.grafana.service=grafana"
      - "traefik.http.services.grafana.loadbalancer.server.port=3000"

Tenemos que preparar el fichero de entorno mon/grafana/grafana.env con los datos confidenciales de acceso a Grafana.

1
2
3
4
GF_SECURITY_ADMIN_USER=admin
GF_SECURITY_ADMIN_PASSWORD=unaBuenaPassword
GF_USERS_ALLOW_SIGN_UP=false
# GF_SERVER_ROOT_URL=https://grafana.midomino.com

Con esta configuración ya podemos levantar el contenedor de Grafana y comprobar que podemos acceder sin problemas. (Claro que antes tenemos que declarar la ruta en la DNS Zone)

Web de Grafana

Como ya hemos comentado podríamos configurar completamente Grafana desde el interfaz web, pero vamos a crear la fuente de datos asociada a nuestro Prometheus desde el fichero de configuración de Grafana.

Para eso creamos el fichero mon/grafana/provisioning/datasources/datasource.yml con el contenido:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    orgId: 1
    url: http://prometheus:9090
    basicAuth: false
    isDefault: true
    editable: true

Una vez tengamos el fichero podemos reiniciar el contenedor de Grafana con dco restart grafana (docker-compose restart grafana si no usas mis alias). Después del reinicio podremos ver la nueva fuente de datos en la web de Grafana.

Grafana, datasource de Prometheus

Nuestro primer Dashboard lo importaremos directamente desde la web. Vamos a usar como ejemplo este Dashboard de la página web de Grafana. Vemos que tiene el id=193. Podríamos descargarlo como fichero .json pero nos basta con el id para probarlo. Clickamos en el signo + a la izquierda en la página web de nuestro Grafana y escogemos la opción Import. Basta con teclear el id 193 para que tengamos el Dashboard definido en Grafana.

Grafana: Dashboard de Contenedores

Prometheus: Añadiendo Node Exporter

Si queremos que Prometheus capture también métricas de la maquina host, podemos instalar Node Exporter. La sección del fichero docker-compose.yml para el Node Exporter sería:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  nodeexporter:
    image: prom/node-exporter:v0.18.1
    container_name: nodeexporter
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.rootfs=/rootfs'
      - '--path.sysfs=/host/sys'
      - '--collector.filesystem.ignored-mount-points=^/(sys|proc|dev|host|etc)($$|/)'
    restart: unless-stopped
    networks:
      backend:
        ipv4_address: 172.20.0.11

Tenemos que añadir el nuevo origen de datos en el fichero mon/prometheus/prometheus.yml (lineas 31 y siguientes) que nos quedaría:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
global:
  # How frequently to scrape targets (by default 1m)
  scrape_interval: 15s
  # How long until a scrape request times out (by default 10s)
  # scrape_timeout: 10s
  # How frequently evaluate the rules (by default 1m)
  evaluation_interval: 15s

# Alerting section specifies settings related to Alertmanager
alerting:
  alertmanagers:
  - static_configs:
    - targets:
    # whatever you want

# Rule files specifies a list of globs. Rules and alerts are read from all matching files
# rule_files:

# List of scrape configurations
scrape_configs:
  - job_name: 'prometheus'
    static_configs:
    - targets: ['prometheus:9090']
      labels:
        alias: 'prometheus'
  - job_name: 'cadvisor'
    static_configs:
    - targets: ['cadvisor:8080']
      labels:
        alias: 'cadvisor'
  - job_name: 'node-exporter'
    scrape_interval: 5s
    static_configs:
    - targets: ['nodeexporter:9100']
      labels:
        alias: 'nodeexporter'

Con la nueva fuente de datos definida podemos reiniciar el contenedor Prometheus con dco restart prometheus (docker-compose restart prometheus)

Podemos volver a conectarnos a Prometheus via túnel ssh como ya describimos y comprobar que los Targets son ahora tres y están todos arriba.

Prometheus con tres targets activos

Para ver los nuevos datos que aporta el Node Exporter a nuestro contenedor Prometheus tenemos que definir un nuevo Dashboard que nos los muestre en el contenedor Grafana.

Vamos a añadir el nuevo Dashboard definiendolo con un fichero .json para ello definiremos dos ficheros en nuestro directorio mon/grafana/provisioning/dashboards (más info)

El primer fichero dashboard.yml define el provider y tiene el siguiente contenido:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: 1

providers:
- name: 'Prometheus'
  orgId: 1
  folder: ''
  type: file
  disableDeletion: false
  editable: true
  options:
    path: /etc/grafana/provisioning/dashboards

En el segundo fichero dco_prom.json definiremos el Dashboard que hemos copiado de aquí.

Si reiniciamos ahora el contenedor de Grafana, dco restart grafana veremos que ya tenemos los dos Dashboard disponibles; el que añadimos desde la red y el que hemos añadido ahora mismo por fichero.

En el nuevo Dashboard podemos ver ya métricas del host, como la memoria usada, la carga de CPU del host y la capacidad del sistema de ficheros.

Vemos también que el nuevo Dashboard viene preparado para visualizar Alerts pero no tenemos ninguna definida (todavía).

Dashboard con datos de Node Exporter

Prometheus: Definición de Alarmas

Vamos a definir condiciones de Alarma en Prometheus. Necesitamos crear un nuevo fichero mon/prometheus/alert.rules, donde definiremos un par de alarmas:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
groups:
- name: example
  rules:

  # Alert for any instance that is unreachable for >2 minutes.
  - alert: service_down
    expr: up == 0
    for: 2m
    labels:
      severity: page
    annotations:
      summary: "Instance {{ $labels.instance }} down"
      description: "{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 2 minutes."

  - alert: high_load
    expr: node_load1 > 0.5
    for: 2m
    labels:
      severity: page
    annotations:
      summary: "Instance {{ $labels.instance }} under high load"
      description: "{{ $labels.instance }} of job {{ $labels.job }} is under high load."

Tenemos que editar también el fichero mon/prometheus/prometheus.yml para añadir nuestro fichero alert.rules en la sección de rule_files que quedaría así:

1
2
3
# Rule files specifies a list of globs. Rules and alerts are read from all matching files
rule_files:
  - 'alert.rules'

Si rearrancamos Prometheus (ya sabes dco restart prometheus o docker-compose restart prometheus) y se cumplen las condiciones para la alarma podremos verla en el Dashboard:

Dashboard con Alarma

Las alarmas que hemos definido no tienen demasiado sentido, pero antes de profundizar en como definir alarmas significativas vamos a configurar las notificaciones a través de Alert Manager.