После внедрения Loki на рабочем проекте — решил добавить его и себе.
А заодно — добавить node_exporter и alertmanager, что бы получать уведомления, когда на разделах будет заканчиваться место.
Обычно «Ссылки по теме» размещаю в конце поста, но тут стоит их добавить в начале.
Для общего знакомства с Prometheus:
- Prometheus: мониторинг — введение, установка, запуск, примеры
- Prometehus: обзор — federation, мониторинг Docker Swarm и настройки Alertmanager
- Prometheus: запуск сервера с Alertmanager, cAdvisor и Grafana
Loki:
- Grafana Labs: Loki — сбор и просмотр логов
- Grafana Labs: Loki – распределённая система, теги и фильтры
Содержание
Текущий мониторинг
Общий мониторинг сейчас осуществляется двумя сервисами — NGINX Amplify и uptrends.com.
NGINX Amplify
Умеет сразу всё из коробки, установка в несколько кликов, но есть одна проблема: алерты по метрике system.disk.in_use можно создавать только для корневого раздела.
У сервера, на котором работает RTFM, имеется дополнительный диск, смонтированный в /backups:
[simterm]
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
[/simterm]
Выглядит Amplify dashboard вот так:
Бекапы
В /backups сохраняются локальные копии бекапов, создаваемые скриптом simple-backup, см. описание в посте Python: скрипт бекапа файлов и баз MySQL в AWS S3.
Скрипт не идеальный, и многое в нём хочется поправить (или вообще переписать с нуля), но он работает, и свои функции выполняет.
Собственно проблема в том, что он сначала складывает файлы в /backups, а потом выполняет загрузку в AWS S3.
Если в /backups сохранить не удалось — то и в S3 данные не отправляются.
Пока для «решения» этой проблемы в cron-задаче просто добавил уведомление на почту при фейлах:
[simterm]
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" [email protected]
[/simterm]
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 — создание роли
Создаём каталоги:
[simterm]
$ mkdir -p roles/monitoring/{tasks,templates}
[/simterm]
Пока этого хватит.
Добавляем роль в плейбук:
...
- 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-окружении пока, конечно:
[simterm]
$ ./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.
[/simterm]
Проверям каталоги:
[simterm]
root@rtfm-do-dev:~# ll /data/prometheus/ /opt/prometheus/ /data/prometheus/: total 0 /opt/prometheus/: total 0
[/simterm]
Пользователя:
[simterm]
root@rtfm-do-dev:~# id prometheus uid=1003(prometheus) gid=1003(prometheus) groups=1003(prometheus)
[/simterm]
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
Запускаем скрипт, проверяем сервис:
[simterm]
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
[/simterm]
И контейнеры:
[simterm]
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
[/simterm]
(форматирование сбивается 🙁 )
(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 задаются в файлах переменных:
[simterm]
$ 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
[/simterm]
И выглядят так:
...
#######################
### 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:
[simterm]
root@rtfm-do-dev:~# dig dev.monitor.example.com +short 174.***.***.179
[/simterm]
Обновляем списки domains, и получаем сертификаты:
[simterm]
$ ./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.
[/simterm]
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
...
Запускаем:
[simterm]
$ ./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
[/simterm]
По идее — уже должен открываться 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
...
Деплоим:
[simterm]
$ ./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.
[/simterm]
Проверяем — нет, всё-равно 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»
Угу…
Проверяем владельца каталога на хосте:
[simterm]
root@rtfm-do-dev:/opt/prometheus# ls -l /data/ total 8 drwxr-xr-x 2 prometheus prometheus 4096 Mar 9 09:19 prometheus
[/simterm]
Пользователя, под которым работает процесс в контейнере:
[simterm]
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
[/simterm]
Сравниваем ID пользователя в контейнере:
[simterm]
root@rtfm-do-dev:/opt/prometheus# docker exec -ti prometheus_prometheus-server_1 id nobody uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
[/simterm]
И на хосте:
[simterm]
root@rtfm-do-dev:/opt/prometheus# id nobody uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
[/simterm]
Меняем владельца каталога /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
Перезапускам сервис, проверяем точки монтирования:
[simterm]
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
[/simterm]
Так, тут всё ОК, вроде.
Я за*бался…
Сейчас, когда форматирую черновик — то всё так быстро и просто выглядит… А на деле, даже уже имея готовые конфиги, примеры, и зная, что и как делать — повозиться пришлось.
Что осталось?
А…
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:
[simterm]
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]
[/simterm]
Угу.
Генерируем пароль:
[simterm]
$ pwgen 12 1 Foh***ae1
[/simterm]
Шифруем его:
[simterm]
$ 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
[/simterm]
Создаём переменные 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
...
Деплоим, проверяем данные в контейнере:
[simterm]
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
[/simterm]
Окей.
Теперь надо добавить датасорс 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 вечера…










