Prometheus: мониторинг для RTFM — Grafana, Loki и promtail

Автор: | 03/09/2019
 

После внедрения Loki на рабочем проекте — решил добавить его и себе.

А заодно — добавить node_exporter и alertmanager, что бы получать уведомления, когда на разделах будет заканчиваться место.

Обычно «Ссылки по теме» размещаю в конце поста, но тут стоит их добавить в начале.

Для общего знакомства с Prometheus:

Loki:

Текущий мониторинг

Общий мониторинг сейчас осуществляется двумя сервисами — NGINX Amplify и uptrends.com.

NGINX Amplify

Умеет сразу всё из коробки, установка в несколько кликов, но есть одна проблема: алерты по метрике system.disk.in_use можно создавать только для корневого раздела.

У сервера, на котором работает RTFM, имеется дополнительный диск, смонтированный в /backups:

root@rtfm-do-production:/home/setevoy# lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda      8:0    0   20G  0 disk
└─sda1   8:1    0   20G  0 part /backups
vda    254:0    0   50G  0 disk
└─vda1 254:1    0   50G  0 part /
vdb    254:16   0  440K  1 disk

Выглядит Amplify dashboard вот так:

Бекапы

В /backups сохраняются локальные копии бекапов, создаваемые скриптом simple-backup, см. описание в посте Python: скрипт бекапа файлов и баз MySQL в AWS S3.

Скрипт не идеальный, и многое в нём хочется поправить (или вообще переписать с нуля), но он работает, и свои функции выполняет.

Собственно проблема в том, что он сначала складывает файлы в /backups, а потом выполняет загрузку в AWS S3.

Если в /backups сохранить не удалось — то и в S3 данные не отправляются.

Пока для «решения» этой проблемы в cron-задаче просто добавил уведомление на почту при фейлах:

root@rtfm-do-production:/home/setevoy# crontab -l | grep back
Ansible: simple-backup
0 1 * * * /opt/simple-backup/sitebackup.py -c /usr/local/etc/production-simple-backup.ini >> /var/log/simple-backup.log || cat /var/log/simple-backup.log  | mailx -s "RTFM production backup - Failed" notify@example.com

uptrends.com

Просто ping-сервис с уведомлением на почту, если веб-сервер отдаёт не 200 код.

В бесплатном варианте ограничение на один сайт, и лимиты на уведомления — но мне хватает почты:

Prometheus, Grafana и Loki

Сегодня будем добавлять дополнительный мониторинг.

Вообще, изначально — просто хотелось прикрутить Loki, что бы следить за логами, но раз уж буду добавлять её — почему бы не прикрутить Prometheus, node_exporter и Alertmanager, что бы слать себе уведомления на почту и в свой Slack?

Тем более, что все конфиги уже есть — остаётся их скопировать с рабочего проекта, и изменить под себя, т.к. такого количества алертов и метрик, как там — сейчас не нужно.

Пока этот стек запущу на самом сервере с RTFM, потом, может быть, вынесу на какой-то отдельный минимальный сервер: когда будут готовы роли и шаблоны это не составит труда.

Автоматизировать будем, как обычно, через Ansible.

План таков:

  • добавить роль monitoring в Ansible
  • добавить шаблон Docker Compose для запуска стека сервисов, в котором будут:
    • prometheus-server
    • node_exporter
    • loki
    • grafana 6.0
    • promtail для сбора логов
  • по ходу дела — надо будет обновить роли:
    • nginx — добавить новый виртуалхост для проксирования к Grafana и Prometheus
    • letsencrypt — для получения сертификата для виртуалхоста с мониторингом

Когда/если мониторинг будет выносится на отдельный сервер — будет смысл добавить blackbox_exporter и проверять все свои сайты.

В целом сейчас автоматизация RTFM выглядит примерно так же, как описано в посте AWS: миграция RTFM 3.0 (final) — CloudFormation и Ansible роли, только сейчас сервер хостится в DigitalOcean, а все файлы для Ansible собраны в едином приватном репозитоии Github (Miscrosoft сделала хороший подарок всем, разрешив использование приватных репозиториев в бесплатном аккаунте — видимо, испугавшись массового оттока пользователей после покупки Github).

Попозже вынесу все роли, которые используются в этом посте и нынешней автоматизации в публичный репозиторий с фейковыми данными.

Ansible — создание роли

Создаём каталоги:

mkdir -p roles/monitoring/{tasks,templates}

Пока этого хватит.

Добавляем роль в плейбук:

...
    - role: amplify
      tags: amplify, monitoring, app

    - role: monitoring
      tags: prometheus, monitoring, app 
...

Тег app используется как замена all, для запуска всех ролей кроме некоторых, monitoring — для запуска всего, связанного с мониторингом, а с помощью тега prometheus будем провиженить только то, что насетапим сейчас.

Запускается Ansible простым bash-скриптом, см. Скрипт запуска Ansible.

Создаём файл roles/monitoring/tasks/main.yml, в котором начинаем описывать задачи.

Пользователь и каталоги

Сразу задаём переменные в group_vars/all.yml:

...
# MONITORING
prometheus_home: "/opt/prometheus"
prometheus_data: "/data/prometheus"
prometheus_user: "prometheus"

В roles/monitoring/tasks/main.yml добавляем создание пользователя:

- name: "Add Prometheus user"
  user:
    name: "{{ prometheus_user }}"
    shell: "/usr/sbin/nologin"

Создание каталога, в котором будем хранить конфиги и Docker Compose файл:

- name: "Create monitoring stack dir {{ prometheus_home }}"
  file:
    path: "{{ prometheus_home }}"
    state: directory
    owner: "{{ prometheus_user }}"
    group: "{{ prometheus_user }}"
    recurse: yes

И каталог для TSDB Prometheus — метрики будут хранится неделю, больше не надо:

- name: "Create Prometehus TSDB data dir {{ prometheus_data }}"
  file:
    path: "{{ prometheus_data }}"                                                                                                                                                                   
    state: directory
    owner: "{{ prometheus_user }}"
    group: "{{ prometheus_user }}"

В рабочем проекте каталогов намного больше:

  • /etc/prometheus — с конфигами самого Prometheus, Alertmanager-а, blackbox-exporter-а
  • /etc/grafana — с конфигами Grafana
  • /opt/prometheus — с Compose файлом
  • /data/prometheus — TSDB Prometheus-а
  • /data/grafana — с данными Grafana (-rwxr-xr-x  1 grafana grafana 8.9G Mar  9 09:12 grafana.db — OMG!)

Можно запустить, и проверить.

На Dev-окружении пока, конечно:

./ansible_exec.sh -t prometheus
Tags: prometheus
Env: rtfm-dev
...
Dry-run check passed.
Are you sure to proceed? [y/n] y
Applying roles...
...
TASK [monitoring : Add Prometheus user] ****
changed: [ssh.dev.rtfm.co.ua]
TASK [monitoring : Create monitoring stack dir /opt/prometheus] ****
changed: [ssh.dev.rtfm.co.ua]
TASK [monitoring : Create Prometehus TSDB data dir /data/prometheus] ****
changed: [ssh.dev.rtfm.co.ua]
PLAY RECAP ****
ssh.dev.rtfm.co.ua      : ok=4    changed=3    unreachable=0    failed=0
Provisioning done.

Проверям каталоги:

root@rtfm-do-dev:~# ll /data/prometheus/ /opt/prometheus/
/data/prometheus/:
total 0
/opt/prometheus/:
total 0

Пользователя:

root@rtfm-do-dev:~# id prometheus
uid=1003(prometheus) gid=1003(prometheus) groups=1003(prometheus)

systemd и Docker Compose

Далее создаём шаблон systemd-юнит файла и шаблон для запуска стека, пока тут будет только два контейнера с prometehus-server и node_exporter.

Пример создания systemd-файла для запуска Docker Compose есть тут — Linux: systemd сервис для Docker Compose.

Создаём файл шаблона roles/monitoring/templates/prometheus.service.j2:

[Unit]
Description=Prometheus monitoring stack
Requires=docker.service
After=docker.service

[Service]
Restart=always
WorkingDirectory={{ prometheus_home }}

# Compose up
ExecStart=/usr/local/bin/docker-compose -f prometheus-compose.yml up
# Compose down, remove containers and volumes
ExecStop=/usr/local/bin/docker-compose -f prometheus-compose.yml down -v

[Install]
WantedBy=multi-user.target

И шаблон Compose-файла — roles/monitoring/templates/prometheus-compose.yml.j2:

version: '2.4'

networks:
  prometheus:

services:

  prometheus-server:
    image: prom/prometheus
    networks:
      - prometheus
    ports:
      - 9091:9090
    restart: unless-stopped
    mem_limit: 500m
    mem_reservation: 100m 

  node-exporter:
    image: prom/node-exporter
    networks:
      - prometheus
    ports:
      - 9100:9100
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - --collector.filesystem.ignored-mount-points
      - "^/(sys|proc|dev|host|etc|rootfs/var/lib/docker/containers|rootfs/var/lib/docker/overlay2|rootfs/run/docker/netns|rootfs/var/lib/docker/aufs)($$|/)"
    restart: unless-stopped
    mem_limit: 500m
    mem_reservation: 100m

В roles/monitoring/tasks/main.yml добавляем копирование шаблонов на сервер и запуск сервиса:

...
- name: "Copy Compose file {{ prometheus_home }}/prometheus-compose.yml"
  template:
    src: templates/prometheus-compose.yml.j2
    dest: "{{ prometheus_home }}/prometheus-compose.yml"
    owner: "{{ prometheus_user }}"
    group:  "{{ prometheus_user }}"
    mode: 0644

- name: "Copy systemd service file /etc/systemd/system/prometheus.service"
  template:
    src: "templates/prometheus.service.j2"
    dest: "/etc/systemd/system/prometheus.service"
    owner: "root"
    group:  "root"
    mode: 0644

- name: "Start monitoring service"
  service:
    name: "prometheus"
    state: restarted
    enabled: yes

Запускаем скрипт, проверяем сервис:

root@rtfm-do-dev:~# systemctl status prometheus.service
● prometheus.service - Prometheus monitoring stack
Loaded: loaded (/etc/systemd/system/prometheus.service; enabled; vendor preset: enabled)
Active: active (running) since Sat 2019-03-09 09:52:20 EET; 5s ago
Main PID: 1347 (docker-compose)
Tasks: 5 (limit: 4915)
Memory: 54.1M
CPU: 552ms
CGroup: /system.slice/prometheus.service
├─1347 /usr/local/bin/docker-compose -f prometheus-compose.yml up
└─1409 /usr/local/bin/docker-compose -f prometheus-compose.yml up

И контейнеры:

root@rtfm-do-dev:~# docker ps
CONTAINER ID        IMAGE                COMMAND                  CREATED             STATUS              PORTS                    NAMES
8decc7775ae9        jc5x/firefly-iii     ".deploy/docker/entr…"   7 seconds ago       Up 5 seconds        0.0.0.0:9090->80/tcp     firefly_firefly_1
3647286526c2        prom/node-exporter   "/bin/node_exporter …"   7 seconds ago       Up 5 seconds        0.0.0.0:9100->9100/tcp   prometheus_node-exporter_1
dbe85724c7cf        prom/prometheus      "/bin/prometheus --c…"   7 seconds ago       Up 5 seconds        0.0.0.0:9091->9090/tcp   prometheus_prometheus-server_1

(форматирование сбивается 🙁 )

(firefly-iii — это домашняя бухгалтерия, см. Firefly III: домашняя бухгалтерия)

Let’s Encrypt

Для мониторинга будет использоваться домен monitor.example.com (и dev.monitor.example.com для Dev-окружения), для которого надо получить сертификат.

Полностью его роль выглядит так:

- name: "Install Let's Encrypt client"                                                                                                                                                                                       
  apt:
    name: letsencrypt
    state: latest

- name: "Check if NGINX is installed"
  package_facts:
    manager: "auto"

- name: "NGINX test result - True"
  debug:
    msg: "NGINX found"
  when: "'nginx' in ansible_facts.packages"

- name: "NGINX test result - False"
  debug:
    msg: "NGINX NOT found"
  when: "'nginx' not in ansible_facts.packages"


- name: "Stop NGINX"
  systemd:
    name: nginx
    state: stopped
  when: "'nginx' in ansible_facts.packages"

# on first install - no /etc/letsencrypt/live/ will be present
- name: "Check if /etc/letsencrypt/live/ already present"
  stat:                                                                                                                                                                                                                      
    path: "/etc/letsencrypt/live/"
  register: le_live_dir

- name: "/etc/letsencrypt/live/ check result"
  debug: 
    msg: "{{ le_live_dir.stat.path }}"

- name: "Initialize live_certs with garbage if no /etc/letsencrypt/live/ found"
  command: "ls -1 /etc/letsencrypt/"
  register: live_certs
  when: le_live_dir.stat.exists == false

- name: "Check existing certificates"
  command: "ls -1 /etc/letsencrypt/live/"
  register: live_certs
  when: le_live_dir.stat.exists == true

- name: "Certs found"
  debug:
    msg: "{{ live_certs.stdout_lines }}"

- name: "Obtain certificates"
  command: "letsencrypt certonly --standalone --agree-tos -m {{ notify_email }} -d {{ item.1 }}"
  with_subelements:
    - "{{ web_projects }}"
    - domains 
  when: "item.1 not in live_certs.stdout_lines"

- name: "Start NGINX"
  systemd:
    name: nginx
    state: started
  when: "'nginx' in ansible_facts.packages"

- name: "Update renewal settings to web-root"
  lineinfile:
    dest: "/etc/letsencrypt/renewal/{{ item.1 }}.conf"
    regexp: '^authenticator '
    line: "authenticator = webroot"
    state: present
  with_subelements:
    - "{{ web_projects }}"
    - domains

- name: "Add Let's Encrypt cronjob for cert renewal"
  cron:
    name: letsencrypt_renewal
    special_time: weekly
    job: letsencrypt renew --webroot -w /var/www/html/ &> /var/log/letsencrypt/letsencrypt.log && service nginx reload

Список доменов, для которых необходимо получить сертификаты берётся из вложенного списка domains:

...
- name: "Obtain certificates"
  command: "letsencrypt certonly --standalone --agree-tos -m {{ notify_email }} -d {{ item.1 }}"
  with_subelements:
    - "{{ web_projects }}"
    - domains 
  when: "item.1 not in live_certs.stdout_lines"
...

При этом сначала выполняется проверка уже имеющихся сертификатов, что бы не делать запрос каждый раз:

...
- name: "Check existing certificates"
  command: "ls -1 /etc/letsencrypt/live/"
  register: live_certs
  when: le_live_dir.stat.exists == true
...

web_projects и domains задаются в файлах переменных:

ll group_vars/rtfm-*
-rw-r--r-- 1 setevoy setevoy 4731 Mar  8 20:26 group_vars/rtfm-dev.yml
-rw-r--r-- 1 setevoy setevoy 5218 Mar  8 20:26 group_vars/rtfm-production.yml

И выглядят так:

...
#######################
### Roles variables ###
#######################

# used in letsencrypt, nginx, php-fpm
web_projects:

  - name: rtfm
    domains:
      - dev.rtfm.co.ua

  - name: setevoy
    domains:
      - dev.money.example.com
      - dev.use.example.com
...

У регистратора добавляем субдомены monitor.example.com и dev.monitor.example.com, ждём обновления DNS:

root@rtfm-do-dev:~# dig dev.monitor.example.com +short
174.***.***.179

Обновляем списки domains, и получаем сертификаты:

./ansible_exec.sh -t letsencrypt
Tags: letsencrypt
Env: rtfm-dev
...
TASK [letsencrypt : Check if NGINX is installed] ****
ok: [ssh.dev.rtfm.co.ua]
TASK [letsencrypt : NGINX test result - True] ****
ok: [ssh.dev.rtfm.co.ua] => {
"msg": "NGINX found"
}
TASK [letsencrypt : NGINX test result - False] ****
skipping: [ssh.dev.rtfm.co.ua]
TASK [letsencrypt : Stop NGINX] ****
changed: [ssh.dev.rtfm.co.ua]
TASK [letsencrypt : Check if /etc/letsencrypt/live/ already present] ****
ok: [ssh.dev.rtfm.co.ua]
TASK [letsencrypt : /etc/letsencrypt/live/ check result] ****
ok: [ssh.dev.rtfm.co.ua] => {
"msg": "/etc/letsencrypt/live/"
}
TASK [letsencrypt : Initialize live_certs with garbage if no /etc/letsencrypt/live/ found] ****
skipping: [ssh.dev.rtfm.co.ua]
TASK [letsencrypt : Check existing certificates] ****
changed: [ssh.dev.rtfm.co.ua]
TASK [letsencrypt : Certs found] ****
ok: [ssh.dev.rtfm.co.ua] => {
"msg": [
"dev.use.example.com",
"dev.money.example.com",
"dev.rtfm.co.ua",
"README"
]
}
TASK [letsencrypt : Obtain certificates] ****
skipping: [ssh.dev.rtfm.co.ua] => (item=[{'name': 'rtfm'}, 'dev.rtfm.co.ua'])
skipping: [ssh.dev.rtfm.co.ua] => (item=[{'name': 'setevoy'}, 'dev.money.example.com'])
skipping: [ssh.dev.rtfm.co.ua] => (item=[{'name': 'setevoy'}, 'dev.use.example.com'])
changed: [ssh.dev.rtfm.co.ua] => (item=[{'name': 'setevoy'}, 'dev.monitor.example.com'])
TASK [letsencrypt : Start NGINX] ****
changed: [ssh.dev.rtfm.co.ua]
TASK [letsencrypt : Update renewal settings to web-root] ****
ok: [ssh.dev.rtfm.co.ua] => (item=[{'name': 'rtfm'}, 'dev.rtfm.co.ua'])
ok: [ssh.dev.rtfm.co.ua] => (item=[{'name': 'setevoy'}, 'dev.money.example.com'])
ok: [ssh.dev.rtfm.co.ua] => (item=[{'name': 'setevoy'}, 'dev.use.example.com'])
changed: [ssh.dev.rtfm.co.ua] => (item=[{'name': 'setevoy'}, 'dev.monitor.example.com'])
PLAY RECAP ****
ssh.dev.rtfm.co.ua      : ok=13   changed=5    unreachable=0    failed=0
Provisioning done.

NGINX

Далее — добавляем шаблоны виртуахостов для monitor.example.com и dev.monitor.example.com — roles/nginx/templates/dev/dev.monitor.example.com.conf.j2 и roles/nginx/templates/production/monitor.example.com.conf.j2:

upstream prometheus_server {
    server 127.0.0.1:9091;
}

upstream grafana {
    server 127.0.0.1:3000;
}

server {
    
    listen 80;
    server_name  {{ item.1 }};
    
    # Lets Encrypt Webroot
    location ~ /.well-known {
    root /var/www/html;
        allow all;
    }
    
    location / {
        allow {{ office_allow_location }};
        allow {{ home_allow_location }};
        deny all;
        return 301 https://{{ item.1 }}$request_uri;
    }
}

server {

    listen       443 ssl;
    server_name  {{ item.1 }};

    access_log  /var/log/nginx/{{ item.1 }}-access.log;
    error_log /var/log/nginx/{{ item.1 }}-error.log warn;

    auth_basic_user_file {{ web_data_root_prefix }}/{{ item.0.name }}/.htpasswd_{{ item.0.name }};
    auth_basic "Password-protected Area";

    allow {{ office_allow_location }};
    allow {{ home_allow_location }};
    deny all;

    ssl_certificate /etc/letsencrypt/live/{{ item.1 }}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{{ item.1 }}/privkey.pem;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/nginx/dhparams.pem;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;

    location / {

        proxy_redirect          off;
        proxy_set_header        Host            $host;
        proxy_set_header        X-Real-IP       $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://grafana$request_uri;
    }

    location /prometheus {

        proxy_redirect          off;
        proxy_set_header        Host            $host;
        proxy_set_header        X-Real-IP       $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://prometheus_server$request_uri;
    }

}

(см. пост OpenBSD: установка NGINX и настройки безопасности)

Шаблоны копируются из roles/nginx/tasks/main.yml, используя те же списки web_projects и domains:

...
- name: "Add NGINX virtualhosts configs"
  template:
    src: "templates/{{ env }}/{{ item.1 }}.conf.j2"
    dest: "/etc/nginx/conf.d/{{ item.1 }}.conf"
    owner: "root"
    group: "root"
    mode: 0644
  with_subelements:
    - "{{ web_projects }}"
    - domains
...

Запускаем:

./ansible_exec.sh -t nginx
Tags: nginx
Env: rtfm-dev
...
TASK [nginx : NGINX test return code] ****
ok: [ssh.dev.rtfm.co.ua] => {
"msg": "0"
}
TASK [nginx : Service NGINX restart and enable on boot] ****
changed: [ssh.dev.rtfm.co.ua]
PLAY RECAP ****
ssh.dev.rtfm.co.ua      : ok=13   changed=3    unreachable=0    failed=0

По идее — уже должен открываться Prometheus:

«404 page not found» — это сообщение уже от самого Prometheus, надо ещё ему путь настроить.

Так как с NGINX и SSL закончили — можно приступать к настройке самих сервисов.

Конфигурация prometheus-server

Создаём новый файл шаблона roles/monitoring/templates/prometheus-server-conf.yml.j2:

global:

  scrape_interval:     15s 
  external_labels:
    monitor: 'rtfm-monitoring-{{ env }}'

#alerting:
#  alertmanagers:
#  - static_configs:
#    - targets:
#      - alertmanager:9093

#rule_files:
#  - "alert.rules"

scrape_configs:

  - job_name: 'node-exporter'
    static_configs:
      - targets:
        - 'localhost:9100'

alerting пока комментируем — добавим позже.

Добавляем копирование шаблона на сервер:

...
- name: "Copy Prometheus server config {{ prometheus_home }}/prometheus-server-conf.yml"
  template:
    src: "templates/prometheus-server-conf.yml"
    dest: "{{ prometheus_home }}/prometheus-server-conf.yml"
    owner: "{{ prometheus_user }}"
    group:  "{{ prometheus_user }}"
    mode: 0644
...

Обновляем roles/monitoring/templates/prometheus-compose.yml.j2 — добавляем маппинг файла в контейнер:

...
  prometheus-server:
    image: prom/prometheus
    networks:
      - prometheus
    ports:
      - 9091:9090
    volumes:
      - {{ prometheus_home }}/prometheus-server-conf.yml:/etc/prometheus.yml
    restart: unless-stopped
...

Деплоим:

./ansible_exec.sh -t prometheus
Tags: prometheus
Env: rtfm-dev
...
TASK [monitoring : Start monitoring service] ****
changed: [ssh.dev.rtfm.co.ua]
PLAY RECAP ****
ssh.dev.rtfm.co.ua      : ok=7    changed=2    unreachable=0    failed=0
Provisioning done.

Проверяем — нет, всё-равно 404…

А, вспомнил — --web.external-url нужен. Правда — тут придётся получать домен из web_projects и domains, как в ролях nginx и letsencrypt.

И надо добавить указание на файл настроек — --config.file.

Обновляем Compose, заодно добавляем маппинг /data/prometheus:

...
  prometheus-server:
    image: prom/prometheus
    networks:
      - prometheus
    ports:
      - 9091:9090
    volumes:
      - {{ prometheus_home }}/prometheus-server-conf.yml:/etc/prometheus.yml
      - {{ prometheus_data }}:/prometheus/data/
    command:
      - '--config.file=/etc/prometheus.yml'
      - '--web.external-url=https://{{ item.1 }}/prometheus'
    restart: always
...

В задачу копирования шаблона — добавляем выборку домена:

...
- name: "Copy Compose file {{ prometheus_home }}/prometheus-compose.yml"
  template:
    src: "templates/prometheus-compose.yml.j2"
    dest: "{{ prometheus_home }}/prometheus-compose.yml"
    owner: "{{ prometheus_user }}"
    group:  "{{ prometheus_user }}"
    mode: 0644
  with_subelements:
    - "{{ web_projects }}"
    - domains
  when: "'monitor' in item.1.name"
...

Запускаем ещё раз, и:

prometheus-server_1  | level=error ts=2019-03-09T09:53:28.427567744Z caller=main.go:688 err=»opening storage failed: lock DB directory: open /prometheus/data/lock: permission denied»

Угу…

Проверяем владельца каталога на хосте:

root@rtfm-do-dev:/opt/prometheus# ls -l /data/
total 8
drwxr-xr-x 2 prometheus prometheus 4096 Mar  9 09:19 prometheus

Пользователя, под которым работает процесс в контейнере:

root@rtfm-do-dev:/opt/prometheus# docker exec -ti prometheus_prometheus-server_1 ps aux
PID   USER     TIME  COMMAND
1 nobody    0:00 /bin/prometheus --config.file=/etc/prometheus.yml --web.ex

Сравниваем ID пользователя в контейнере:

root@rtfm-do-dev:/opt/prometheus# docker exec -ti prometheus_prometheus-server_1 id nobody
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

И на хосте:

root@rtfm-do-dev:/opt/prometheus# id nobody
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

Меняем владельца каталога /data/prometheus в roles/monitoring/templates/prometheus-compose.yml.j2:

...
- name: "Create Prometehus TSDB data dir {{ prometheus_data }}"
  file:
    path: "{{ prometheus_data }}"
    state: directory
    owner: "nobody"
    group: "nogroup"                                                                                                                                                                                
    recurse: yes
...

Передеплоиваем всё, и — вуаля!

Осталось поправить таргеты — сейчас prometheus-server не может подключиться к node_exporter:

Потому что конфиги копипастил)

Обновляем roles/monitoring/templates/prometheus-server-conf.yml.j2, меняем localhost:

...
scrape_configs:
    
  - job_name: 'node-exporter'
    static_configs:
      - targets:
        - 'localhost:9100'
...

На имя контейнера, как оно задано в Compose-файле — node-exporter:

С этим, вроде, всё…

Нет — надо проверить — собирает ли node_exporter данные о дисках:

Нет… В node_filesystem_avail_bytes только корневой раздел.

Надо вспомнить из-за чего это.

Настройки node_exporter

Читаем тут — https://github.com/prometheus/node_exporter#using-docker.

Обновляем Compose — добавляем указание bind-mount == rslave и path.rootfs на /rootfs:

...
  node-exporter:
    image: prom/node-exporter
    networks:
      - prometheus
    ports:
      - 9100:9100
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro,rslave
    command:
      - '--path.rootfs=/rootfs'
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - --collector.filesystem.ignored-mount-points
      - "^/(sys|proc|dev|host|etc|rootfs/var/lib/docker/containers|rootfs/var/lib/docker/overlay2|rootfs/run/docker/netns|rootfs/var/lib/docker/aufs)($$|/)"
    restart: unless-stopped
    mem_limit: 500m
    mem_reservation: 100m

Перезапускам сервис, проверяем точки монтирования:

root@rtfm-do-dev:/opt/prometheus# curl -s localhost:9100/metrics | grep sda
node_disk_io_now{device="sda"} 0
node_disk_io_time_seconds_total{device="sda"} 0.044
node_disk_io_time_weighted_seconds_total{device="sda"} 0.06
node_disk_read_bytes_total{device="sda"} 7.448576e+06
node_disk_read_time_seconds_total{device="sda"} 0.056
node_disk_reads_completed_total{device="sda"} 232
node_disk_reads_merged_total{device="sda"} 0
node_disk_write_time_seconds_total{device="sda"} 0.004
node_disk_writes_completed_total{device="sda"} 1
node_disk_writes_merged_total{device="sda"} 0
node_disk_written_bytes_total{device="sda"} 4096
node_filesystem_avail_bytes{device="/dev/sda1",fstype="ext4",mountpoint="/backups"} 4.910125056e+09
node_filesystem_device_error{device="/dev/sda1",fstype="ext4",mountpoint="/backups"} 0
node_filesystem_files{device="/dev/sda1",fstype="ext4",mountpoint="/backups"} 327680
node_filesystem_files_free{device="/dev/sda1",fstype="ext4",mountpoint="/backups"} 327663
node_filesystem_free_bytes{device="/dev/sda1",fstype="ext4",mountpoint="/backups"} 5.19528448e+09
node_filesystem_readonly{device="/dev/sda1",fstype="ext4",mountpoint="/backups"} 0
node_filesystem_size_bytes{device="/dev/sda1",fstype="ext4",mountpoint="/backups"} 5.216272384e+09

Так, тут всё ОК, вроде.

Я за*бался…

Сейчас, когда форматирую черновик — то всё так быстро и просто выглядит… А на деле, даже уже имея готовые конфиги, примеры, и зная, что и  как делать — повозиться пришлось.

Что осталось?

А…

Grafana, Loki, promtail и alertmanager.

OMG…

Пьём чай.


Так, дальше уже по-быстрому.

Надо в /data сделать отдельный каталог /data/monitoring, и в нём уже — для Prometheus, Grafana и Loki.

Обновляем prometheus_data:

...
prometheus_data: "/data/monitoring/prometheus"
...

Добавляем:

...
loki_data: "/data/monitoring/loki"
grafana_data: "/data/monitoring/grafana"
...

Добавляем их создание в roles/monitoring/tasks/main.yml:

...
- name: "Create Loki's data dir {{ loki_data }}"
  file:
    path: "{{ loki_data }}"
    state: directory
    owner: "{{ prometheus_user }}"
    group: "{{ prometheus_user }}"
    recurse: yes

- name: "Create Grafana DB dir {{ grafana_data }}"
  file:
    path: "{{ grafana_data }}"
    state: directory
    owner: "{{ prometheus_user }}"
    group: "{{ prometheus_user }}"
    recurse: yes
...

Loki

В Compose шаблон добавляем Loki:

...
  loki:
    image: grafana/loki:master
    networks:
      - prometheus
    ports:                                                                                                                                                                                                                
      - "3100:3100"
    volumes:
      - {{ prometheus_home }}/loki-conf.yml:/etc/loki/local-config.yaml
      - {{ loki_data }}:/tmp/loki/
    command: -config.file=/etc/loki/local-config.yaml
    restart: unless-stopped
...

Создаём шаблон roles/monitoring/templates/loki-conf.yml.j2 — дефолтный, без DynamoDB и S3 — всё храним в /data/monitoring/loki:

auth_enabled: false
server:
  http_listen_port: 3100

ingester:
  lifecycler:
    address: 0.0.0.0
    ring:
      store: inmemory
      replication_factor: 1
  chunk_idle_period: 15m

schema_config:
  configs:
  - from: 0
    store: boltdb
    object_store: filesystem
    schema: v9
    index:
      prefix: index_
      period: 168h

storage_config:
  boltdb:
    directory: /tmp/loki/index

  filesystem:
    directory: /tmp/loki/chunks

limits_config:
  enforce_metric_name: false

В roles/monitoring/tasks/main.yml добавляем копирование файла:

...
- name: "Copy Loki config {{ prometheus_home }}/loki-conf.yml"
  template:
    src: "templates/loki-conf.yml.j2"
    dest: "{{ prometheus_home }}/loki-conf.yml"
    owner: "{{ prometheus_user }}"
    group:  "{{ prometheus_user }}"
    mode: 0644
...

Grafana

И Grafana:

...
  grafana:
    image: grafana/grafana:6.0.0
    ports:
      - "3000:3000"
    networks:
      - prometheus
    depends_on:
      - loki
    restart: unless-stopped
...

Каталоги и конфиги добавим позже.

Деплоим, проверяем:

Окей — Grafana уже работает, надо только поправить её настройки.

Создаём шаблон её конфига — по сути тут надо только указать:

...
[auth.basic]
enabled = false
...
[security]
# default admin user, created on startup
admin_user = {{ grafana_ui_username }}

# default admin password, can be changed before first start of grafana,  or in profile settings
admin_password = {{ grafana_ui_dashboard_admin_pass }}
...

Если мне память не изменяет — больше в нём ничего не менял.

Проверим на рабочем Production:

admin@monitoring-production:~$ cat /etc/grafana/grafana.ini | grep -v \# | grep -v ";" | grep -ve '^$'
[paths]
[server]
[database]
[session]
[dataproxy]
[analytics]
[security]
admin_user = user
admin_password = pass
[snapshots]
[users]
[auth]
[auth.anonymous]
[auth.github]
[auth.google]
[auth.generic_oauth]
[auth.grafana_com]
[auth.proxy]
[auth.basic]
enabled = false
[auth.ldap]
[smtp]
[emails]
[log]
[log.console]
[log.file]
[log.syslog]
[event_publisher]
[dashboards.json]
[alerting]
[metrics]
[metrics.graphite]
[tracing.jaeger]
[grafana_com]
[external_image_storage]
[external_image_storage.s3]
[external_image_storage.webdav]
[external_image_storage.gcs]

Угу.

Генерируем пароль:

pwgen 12 1
Foh***ae1

Шифруем его:

ansible-vault encrypt_string
New Vault password:
Confirm New Vault password:
Reading plaintext input from stdin. (ctrl-d to end input)
Foh***ae1!vault |
ANSIBLE_VAULT;1.1;AES256
38306462643964633766373435613135386532373133333137653836663038653538393165353931
...
6636633634353131350a343461633265353461386561623233636266376266326337383765336430
3038
Encryption successful

Создаём переменные grafana_ui_username и grafana_ui_dashboard_admin_pass:

...
# MONITORING
prometheus_home: "/opt/prometheus"
prometheus_user: "prometheus"
# data dirs
prometheus_data: "/data/monitoring/prometheus"
loki_data: "/data/monitoring/loki"
grafana_data: "/data/monitoring/grafana"
grafana_ui_username: "setevoy"                                                                                                                                    
grafana_ui_dashboard_admin_pass: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          38306462643964633766373435613135386532373133333137653836663038653538393165353931
          ...
          6636633634353131350a343461633265353461386561623233636266376266326337383765336430
          3038

Создаём шаблон конфига Grafana roles/monitoring/templates/grafana-conf.yml.j2:

[paths] 
[server]
[database]
[session]
[dataproxy]
[analytics]
[security]
admin_user = {{ grafana_ui_username }}
admin_password = {{ grafana_ui_dashboard_admin_pass }}                                                                                                                                                                                        
[snapshots]
[users]
[auth]
[auth.anonymous]
[auth.github]
[auth.google]
[auth.generic_oauth]
[auth.grafana_com]
[auth.proxy]
[auth.basic] 
enabled = false
[auth.ldap]
[smtp]
[emails]
[log]
[log.console]
[log.file] 
[log.syslog] 
[event_publisher]
[dashboards.json]
[alerting]
[metrics] 
[metrics.graphite]
[tracing.jaeger]
[grafana_com]
[external_image_storage]
[external_image_storage.s3]
[external_image_storage.webdav]
[external_image_storage.gcs]

Добавляем его копирование:

...
- name: "Copy systemd service file /etc/systemd/system/prometheus.service"
  template:
    src: "templates/prometheus.service.j2"
    dest: "/etc/systemd/system/prometheus.service"
    owner: "root"
    group:  "root"
    mode: 0644
...

Добавляем его маппинг в Compose:

...
  grafana:
    image: grafana/grafana:6.0.0
    ports:
      - "3000:3000"
    volumes:
      - {{ prometheus_home }}/grafana-conf.yml:/etc/grafana/grafana.ini
      - {{ grafana_data }}:/var/lib/grafana
...

Ещё надо будет замапить {{ prometheus_home }}/provisioning — тут Grafana будет свои настройки держать, но это потом.

Может вообще отдельный таки каталог ей придётся делать.

Деплоим, проверяем:

GF_PATHS_DATA=’/var/lib/grafana’ is not writable.
You may have issues with file permissions, more information here: http://docs.grafana.org/installation/docker/#migration-from-a-previous-version-of-the-docker-container-to-5-1-or-later
mkdir: cannot create directory ‘/var/lib/grafana/plugins’: Permission denied

Hu%^%*@d(&!!!

Читаем документацию:

default user id 472 instead of 104

Да, теперь вспомнил.

Добавляем создание юзера grafana со своим UID.

В переменные вносим:

...
grafana_user: "grafana"
grafana_uid: 472

Добавляем создание пользователя и группы:

- name: "Add Prometheus user"
  user:
    name: "{{ prometheus_user }}"
    shell: "/usr/sbin/nologin"

- name: "Create Grafana group {{ grafana_user }}"
  group:
    name: "{{ grafana_user }}"
    gid: "{{ grafana_uid }}"

- name: "Create Grafana's user {{ grafana_user }} with UID {{ grafana_uid }}"
  user:
    name: "{{ grafana_user }}"
    uid: "{{ grafana_uid }}"
    group: "{{ grafana_user }}"
    shell: "/usr/sbin/nologin"  
...

И меняем владельца каталога {{ grafana_data }}:

...
- name: "Create Grafana DB dir {{ grafana_data }}"
  file:
    path: "{{ grafana_data }}"
    state: directory
    owner: "{{ grafana_user }}"
    group: "{{ grafana_user }}"
    recurse: yes
...

Передеплоиваем, проверяем ещё раз:

Ура)

Но логов ещё нет, т.к. не добавляли promtail.

Кроме того — надо добавить настройку datasource для Grafana из Ansible.

Фух…

Погнали дальше.

Добавляем создание каталога {{ prometheus_home }}/grafana-provisioning/datasources:

...
- name: "Create {{ prometheus_home }}/grafana-provisioning/datasources directory"
  file:
    path: "{{ prometheus_home }}/grafana-provisioning/datasources"
    owner: "{{ grafana_user }}"
    group: "{{ grafana_user }}"
    mode: 0755
    state: directory
...

Добавляем его маппинг в Compose:

...
  grafana:
    image: grafana/grafana:6.0.0
    ports:
      - "3000:3000"
    volumes:
      - {{ prometheus_home }}/grafana-conf.yml:/etc/grafana/grafana.ini
      - {{ prometheus_home }}/grafana-provisioning:/etc/grafana/ 
      - {{ grafana_data }}:/var/lib/grafana
...

Деплоим, проверяем данные в контейнере:

root@rtfm-do-dev:/opt/prometheus# docker exec -ti prometheus_grafana_1 sh
ls -l /etc/grafana
total 8
drwxr-xr-x 2 grafana grafana 4096 Mar  9 11:46 datasources
-rw-r--r-- 1    1003    1003  571 Mar  9 11:26 grafana.ini

Окей.

Теперь надо добавить датасорс Loki — roles/monitoring/templates/grafana-datasources.yml.j2 (см. Grafana: добавление datasource из Ansible):

# config file version
apiVersion: 1

deleteDatasources:
  - name: Loki

datasources:
- name: Loki
  type: loki
  access: proxy
  url: http://loki:3100
  isDefault: true
  version: 1

И его копирование на сервер:

...
- name: "Copy Grafana datasources config {{ prometheus_home }}/grafana-provisioning/datasources/datasources.yml"
  template:
    src: "templates/grafana-datasources.yml.j2"
    dest: "{{ prometheus_home }}/grafana-provisioning/datasources/datasources.yml"
    owner: "{{ grafana_user }}"
    group: "{{ grafana_user }}"
...

Деплоим, проверяем:

t=2019-03-09T11:52:35+0000 lvl=eror msg=»can’t read datasource provisioning files from directory» logger=provisioning.datasources path=/etc/grafana/provisioning/datasources error=»open /etc/grafana/provisioning/datasources: no such file o
r directory»

А, да.

Фиксим путь в Compose — {{ prometheus_home }}/grafana-provisioning должен маппится не в корень /etc/grafana, а как /etc/grafana/provisioning:

...
    volumes:
      - {{ prometheus_home }}/grafana-conf.yml:/etc/grafana/grafana.ini
      - {{ prometheus_home }}/grafana-provisioning:/etc/grafana/provisioning
      - {{ grafana_data }}:/var/lib/grafana
...

Передеплоиваем, и с этим всё готово:

promtail

Так.

Добавляем контейнер с promtail.

Потом ещё alertmanager и его алерты… Но сегодня, наверно уже не успею.

Добавляем контейнер в Compose.

Не помню на счёт файла positions.yaml — надо ли его хранить на хосте…

Но раз на наших Production не делал — то, наверно, не критично — точно помню, что в Slack Grafan-ы спрашивал об этом, но в поиске тред уже не находится.

Пока пропустим, делаем без него:

...
  promtail:
    image: grafana/promtail:master
    volumes:
      - {{ prometheus_home }}/promtail-conf.yml:/etc/promtail/docker-config.yaml
#      - {{ prometheus_home }}/promtail-positions.yml:/tmp/positions.yaml
      - /var/log:/var/log
    command: -config.file=/etc/promtail/docker-config.yaml

Создаём шаблон roles/monitoring/templates/promtail-conf.yml.j2:

server:

  http_listen_port: 9080
  grpc_listen_port: 0

positions:

  filename: /tmp/positions.yaml

client:

  url: http://loki:3100/api/prom/push

scrape_configs:

  - job_name: system
    entry_parser: raw
    static_configs:
    - targets:
        - localhost
      labels:
        job: varlogs
        env: {{ env }}
        host: {{ set_hostname }}
        __path__: /var/log/*log

  - job_name: nginx
    entry_parser: raw
    static_configs:
    - targets:
        - localhost
      labels:
        job: nginx
        env: {{ env }}
        host: {{ set_hostname }}                                                                                                                                                                                                              
        __path__: /var/log/nginx/*log

Тут:

  • url: http://loki:3100/api/prom/push — URL aka имя контейнера с Loki, в который promtail будет PUSH-ить данные
  • env: {{ env }} и host: {{ set_hostname }} — дополнительные теги, задаются в group_vars/rtfm-dev.yml и group_vars/rtfm-production.yml:
    env: dev
    set_hostname: rtfm-do-dev

Добавляем копирование файла:

...
- name: "Copy Promtail config {{ prometheus_home }}/promtail-conf.yml"
  template:
    src: "templates/promtail-conf.yml.j2"
    dest: "{{ prometheus_home }}/promtail-conf.yml"
    owner: "{{ prometheus_user }}"
    group: "{{ prometheus_user }}"
...

Деплоим:

level=info ts=2019-03-09T12:09:07.709299788Z caller=tailer.go:78 msg=»start tailing file» path=/var/log/user.log
2019/03/09 12:09:07 Seeked /var/log/bootstrap.log — &{Offset:0 Whence:0}
level=info ts=2019-03-09T12:09:07.709435374Z caller=tailer.go:78 msg=»start tailing file» path=/var/log/bootstrap.log
2019/03/09 12:09:07 Seeked /var/log/dpkg.log — &{Offset:0 Whence:0}
level=info ts=2019-03-09T12:09:07.709746566Z caller=tailer.go:78 msg=»start tailing file» path=/var/log/dpkg.log
level=warn ts=2019-03-09T12:09:07.710448913Z caller=client.go:172 msg=»error sending batch, will retry» status=-1 error=»Post http://loki:3100/api/prom/push: dial tcp: lookup loki on 127.0.0.11:53: no such host»
level=warn ts=2019-03-09T12:09:07.726751418Z caller=client.go:172 msg=»error sending batch, will retry» status=-1 error=»Post http://loki:3100/api/prom/push: dial tcp: lookup loki on 127.0.0.11:53: no such host»

Так…

Логи он собирает, но не может увидеть Loki.

А пачиму?

А патамушта надо depends добавлять.

Обновляем Compose, для promtail добавляем:

...
    depends_on:
      - loki

Нет… Не помогло…

Что?

А! Сеть жеж!

Снова-таки — конфиги копировал, там немного другой сетап.

Добавляем networks в Compose:

...
  promtail:
    image: grafana/promtail:master
    networks:
      - prometheus
    volumes:
      - /opt/prometheus/promtail-conf.yml:/etc/promtail/docker-config.yaml
#      - /opt/prometheus/promtail-positions.yml:/tmp/positions.yaml
      - /var/log:/var/log
    command: -config.file=/etc/promtail/docker-config.yaml
    depends_on:
      - loki

И:

Готово.

Всё.

На этом хватит.

Alertmanager и интеграция со Slack описаны в посте Prometehus: обзор — federation, мониторинг Docker Swarm и настройки Alertmanager.

А теперь я, наконец-то, пойду завтракать 🙂

Потому что начал сетапить всё это часов в 9 утра, а сейчас — 14.30.

А привожу черновик в нормальный вид вообще в 8 вечера…