--- weight: 4 title: "Monitorización de contenedores Docker con cAdvisor, Prometheus y Grafana" date: 2022-01-06T22:40:20+0100 draft: false summary: "Notas sobre monitorización de contenedores" resources: - name: "featured-image" src: "featured-image.png" - name: "featured-image-preview" src: "featured-image-preview" categories: - notes tags: - docker - cAdvisor - prometheus - grafana - alertmanager --- {{< admonition type=warning title="Work in progress" open=true >}} Apuntes incompletos de monitorización de contenedores Docker {{< /admonition >}} ## 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. {{< admonition type=tip title="Referencias" open=true >}} - [Monitoring Docker Services with Prometheus](https://www.ctl.io/developers/blog/post/monitoring-docker-services-with-prometheus/) - [Monitoring setup with docker-compose](https://dev.to/ablx/minimal-prometheus-setup-with-docker-compose-56mp) - [Un template básico](https://github.com/vegasbrianc/prometheus) en Github - [Otro docker-compose programado por Eisteinish](https://github.com/Einsteinish/Docker-Compose-Prometheus-and-Grafana) Muy completo, también en Github {{< /admonition >}} ## 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](https://grafana.com/) y [Prometheus](https://prometheus.io/), pero añadiendo [cAdvisor](https://github.com/google/cadvisor) a la receta como [nos proponen](https://prometheus.io/docs/guides/cadvisor/) 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](https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md#prometheus-container-metrics). _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](https://prometheus.io/docs/instrumenting/clientlibs/) 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`: ```yaml 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: ```bash ssh -L 9081:172.20.0.10:8080 dockadmin@fomalhaut ``` Ahora podemos conectar nuestro navegador a y comprobar que _cAdvisor_ está funcionando y detecta nuestros contenedores en el VPS. {{< figure src="cAdvisor_web_a.png" title="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. {{< figure src="cAdvisor_web_b.png" title="Gráficas en la web de cAdvisor" >}} ### Prometheus Ya tenemos _cAdvisor_ funcionando. Vamos a configurar _Prometheus_ creando su fichero de configuración: ```bash mkdir mon/prometheus touch mon/prometheus/prometheus.yml ``` Con el contenido: ```yaml 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`: ```yaml 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: ```bash ssh -L 9082:172.20.0.10:9090 dockadmin@fomalhaut ``` Podremos acceder a nuestro nuevo _Prometheus_ en . Si queremos hacer pruebas de consultas en la documentación oficial tenemos unos cuantos ejemplos. {{< figure src="prometheus_targets.png" title="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](https://grafana.com/grafana/dashboards) donde se comparten los _Dashboards_ entre usuarios. La sección de _Grafana_ en el fichero `docker-compose.yml` quedaría ```yaml 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_. ```yaml 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_) {{< figure src="grafana_home.png" title="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: ```yaml 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_. {{< figure src="grafana_promsrc.png" title="Grafana, datasource de Prometheus" >}} Nuestro primer _Dashboard_ lo importaremos directamente desde la web. Vamos a usar como ejemplo [este Dashboard](https://grafana.com/grafana/dashboards/193) 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_. {{< figure src="grafana_contdash.png" title="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: ```yaml 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: ```yaml 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. {{< figure src="prometheus_targets_b.png" title="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](https://grafana.com/docs/grafana/latest/administration/provisioning/#dashboards)) El primer fichero `dashboard.yml` define el _provider_ y tiene el siguiente contenido: ```yaml 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í](https://github.com/vegasbrianc/prometheus/blob/master/grafana/provisioning/dashboards/Docker%20Prometheus%20Monitoring-1571332751387.json). 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). {{< figure src="grafana_nexporter.png" title="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: ```yaml 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í: ```yaml # 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_: {{< figure src="grafana_alarm.png" title="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_.