Monitoring as Code на базе VictoriaMetrics и Grafana

от автора

Приветствую всех любителей Infrastructure as Code.

Как я уже писал в предыдущей статье, я люблю заниматься автоматизацией инфраструктуры. Сегодня представляю вашему вниманию вариант построения GitOps для реализации подхода Monitoring as Code.

Немного контекста

Инфраструктура проекта, в котором я сейчас работаю, очень разнородна: k8s-кластера, отдельные docker-хосты с контейнерами, сервисы в обычных systemd-демонах и т.д. Кроме этого, у нас есть PROD, STAGE и DEV-окружения, которые с точки зрения архитектуры могут отличаться. Все эти окружения очень динамичны, постоянно деплоятся новые машины и удаляются старые. К слову, эту часть мы выполняем с помощью Terraform и Ansible (возможно расскажу подробнее в своей очередной статье). Для каждого окружения у нас используется своя инфраструктура мониторинга.

Исторически мы в проекте используем Prometheus-стек. Он отлично подходит для нашей динамической инфраструктуры. Если пройтись по отдельным компонентам, то получится следующий стандартный список компонентов:

  • Сбор и хранение метрик — Prometheus

  • Экспорт метрик — различные экспортеры (Node exporter, Postgres exporter, MongoDB exporter, …).

  • Визуализация — Grafana

  • Алертинг — Alertmanager

В какой-то момент мы заменили Prometheus на VictoriaMetrics (кластерную версию), благодаря чему сэкономили кучу ресурсов и начали хранить наши метрики глубиной в 1 год. Если кто-то еще не знаком с этим замечательным продуктом, советую почитать про него. Мы мигрировали на него практически безболезненно, даже не меняя свои конфиги. В результате Prometheus у нас был заменен на несколько компонентов: vmagent + amalert + vmselect + vminsert + vmstorage.

Большинство из описанных в статье конфигураций подходят как для VictoriaMetrics, так и для Prometheus.

Этапы автоматизации мониторинга

1 этап. Исходное состояние, отсутствие автоматизации

Изначально изменения в конфигурацию Prometheus мы вносили вручную. В Prometheus не использовался никакой Service Discovery, использовался обычный static_config. И, как вы уже наверное догадались, очень быстро наш файл prometheus.yml превратился в портянку из 1000+ строк, которые могли содержать в себе какие-то старые закомментированные targets, лишние jobs и т.д. Почему? Потому что админы никогда не удаляют строки из конфигов, строки просто комментируются до лучших времен.

Читать этот файл из консоли linux было невозможно, внести изменения и не накосячить где-то по пути — просто нереально. Про формат конфигурации Prometheus подробнее можно почитать здесь.

Аналогичная ситуация была и с конфигурацией алертов prometheus, а также Alertmanager.

Дашборды Grafana редактировались также вручную и перетаскивались между несколькими инстансами Grafana через механизмы экспорта/импорта.

На данном этапе у нас не было никакой автоматизации, и, следовательно, тут мы мучались со следующими проблемами:

  • Создали новую машину, но забыли добавить в мониторинг. Когда понадобились метрики, вспомнили про эту машины.

  • Удалили машину, но забыли удалить из мониторинга. Заморгал алертинг, возбудилась группа дежурных (у нас и такое тоже есть).

  • Проводим работы на какой-либо машине, забыли заглушить для неё алерты. Дежурная смена опять звонит.

  • Внесли изменения в дашборд Grafana в одном окружении, забыли перенести в другое окружение. В результате получаем разные дашборды в окружениях.

  • Конфигурация мониторинга является черным ящиком для всех, кто не имеет доступ к машине по ssh. У разработчиков часто возникают вопросы по метрикам, алертам и дашбордам.

  • Разработчики не могут внести изменения в дашборд, потому что у них права только Viewer.

  • И т.д.

2 этап. Хранение статической конфигурации в Git

Очень быстро мы намучились с ручной конфигурацией VictoriaMetrics и пришли к следующему варианту: решили хранить конфиги VictoriaMetrics и Alertmanager в Git. Доставка конфигурации пока выполнялась вручную (по факту — одна команда git pull).

Также мы переделали scrape-конфиги VictoriaMetrics в file_sd_config. Это не сильно упростило конфигурацию, но зато позволило структурировать её за счёт вынесения таргетов в отдельные файлы.

С точки зрения автоматизации данный этап не сильно отличается от предыдущего, поскольку мы по-прежнему испытываем все проблемы, описанные выше. Но теперь мы хотя бы храним конфигурацию в Git и можем командно работать с ней.

3 этап. GitOps для мониторинга

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

Сразу хочу обозначить, что в данный момент описанное решение находится в стадии тестирования и некоторые части могут измениться при вводе в продакшн.

Service Discovery

Вместо статической конфигурации мы решили использовать Service Discovery. У нас в инфраструктуре уже давно был Hashicorp Consul (в качестве KV-хранилища), но теперь мы решили его использовать как Service Discovery для мониторинга.

Для этого на каждую машину во всех наших окружениях мы установили consul-агент в режиме клиента. Через него мы начали регистрировать наши prometheus-экспортеры как сервисы в Consul. Делается это очень просто: в каталог конфигурации consul-агента необходимо подложить небольшой JSON-файл с минимальной информацией о сервисе. А затем сделать релоад сервиса consul на данном хосте, чтоб агент перечитал конфигурацию и отправил изменения в кластер. Подробнее о регистрации сервисов можно почитать в документации Consul.

Например, для Node Exporter файл может выглядеть следующим образом:

node_exporter.json
{   "service": {     "name": "node_exporter",     "port": 9100,     "meta": {       "metrics_path": "/metrics",       "metrics_scheme": "http"     }   } }

Такой способ регистрации сервиса очень удобен, потому что всю работу за нас делает нативный consul-агент, от нас требуется лишь подложить в нужное место JSON-файл. При этом обновление и дерегистрация сервиса выполняется аналогичным образом (с помощью обновления или удаления файла).

Дерегистрация машин/сервисов (например, для последующего удаления машины) может также производиться с помощью штатного выключения сервиса consul на машине. При остановке consul-агент выполняет graceful-shutdown, который выполняет дерегистрацию.

Кроме этого, дерегистрацию можно выполнить через Consul API.

VictoriaMetrics Configuration

Поскольку мы перешли на Service Discovery, теперь мы можем использовать consul_sd_config в нашем scrape-конфиге VictoriaMetrics. Таким образом, наш файл из 1000+ строк превратился в 30+ строк примерно следующего вида:

prometheus.yml
global:   scrape_interval: 30s  scrape_configs:   - job_name: exporters     consul_sd_configs:       - server: localhost:8500     relabel_configs:       # drop all targets that do not have metrics_path key in metadata       - source_labels: [__meta_consul_service_metadata_metrics_path]         regex: ^/.+$         action: keep       # set metrics path from metrics_path metadata key       - source_labels: [__meta_consul_service_metadata_metrics_path]         target_label: __metrics_path__       # set metrics scheme from metrics_scheme metadata key       - source_labels: [__meta_consul_service_metadata_metrics_scheme]         regex: ^(http|https)$         target_label: __scheme__       - source_labels: [__meta_consul_dc]         target_label: consul_dc       - source_labels: [__meta_consul_health]         target_label: consul_health       - action: labelmap         regex: __meta_consul_metadata_(.+)         replacement: $1       - source_labels: [__meta_consul_node]         target_label: host       - source_labels: [__meta_consul_node, __meta_consul_service_port]         separator: ":"         target_label: instance       - source_labels: [__meta_consul_service]         target_label: job       - source_labels: [__meta_consul_node, __meta_consul_service_port]         separator: ":"         target_label: __address__ 

Такая конфигурация заставляет Prometheus брать список хостов из Consul Service Discovery. Т.е. если хост добавился в Consul, то он через несколько секунд появляется в Prometheus.

С помощью relabel_config мы можем делать любые преобразования данных, полученных из Consul в лейблы Prometheus. Например, мы через метаданные сервиса Consul передаем схему (http или https) и путь к метрикам экспортера (обычно /metrics, но бывает и другой).

Также с помощью метаданных и тегов consul, мы можем фильтровать хосты, которые будут добавлены в Prometheus (при условии, что эти теги или метаданные мы добавили в конфигурацию Сonsul при регистрации сервиса). Например, вот так мы можем брать только хосты из DEV-окружения:

prometheus.yml
scrape_configs:   - job_name: exporters     consul_sd_configs:       - server: localhost:8500         tags:           - dev     relabel_configs:     ...

При использовании Consul Service Discovery мы можем также получать статус хоста (метка __meta_consul_health). С помощью данного поля мы можем выводить наши хосты в Maintenance-режим. Для этого у агента Consul есть специальная команда maint.

Примеры команд
# включение maintenance-режима для хоста consul maint -enable # включение maintenance-режима для отдельного сервиса consul maint -service=node_exporter -enable # выключение maintenance-режима для хоста consul maint -disable

С помощью этой метки мы может обрабатывать событие вывода хостов на обслуживание и не создавать лишние алерты. Для этого необходимо заранее предусмотреть данное исключение в своих правилах алертинга.

Grafana Provisioning

Если вы работали с Grafana, то Вы, наверное, уже знаете, что каждый дашборд представляет собой JSON-файл. Также у Grafana есть API, через который можно пропихивать эти дашборды.

Кроме этого, есть специальный механизм Grafana Provisioning, который позволяет вообще всю конфигурацию Grafana хранить в виде файлов в формате YAML. Этот механизм работает следующим образом:

  1. Мы пишем конфигурацию наших data sources, plugins, dashboards и складываем её в определенный каталог.

  2. Grafana при старте создает все описанные в YAML объекты и импортирует дашборды из указанного каталога.

При импорте дашбордов есть следующие возможности:

  • Grafana может импортировать структуру каталогов и создать их у себя в UI. Импортированные дашборды будут разложены по каталогам в соответствии с расположением JSON-файлов.

  • После импорта дашборды можно сделать нередактируемыми через UI (актуально, если планируете вносить все изменения только через код).

  • Для дашбордов можно задать статические uid, чтоб зафиксировать ссылки на получившиеся дашборды.

  • Grafana умеет перечитывать содержимое каталога и применять изменения в дашбордах.

  • Если JSON-файл исчез из каталога, Grafana может соответственно убирать его из UI.

Примеры конфигурации Grafana Provisioning:

datasources.yml
# config file version apiVersion: 1  # list of datasources that should be deleted from the database deleteDatasources: []  # list of datasources to insert/update depending # what's available in the database datasources:   # <string, required> name of the datasource. Required   - name: VictoriaMetrics     # <string, required> datasource type. Required     type: prometheus     # <string, required> access mode. proxy or direct (Server or Browser in the UI). Required     access: proxy     # <int> org id. will default to orgId 1 if not specified     orgId: 1     # <string> custom UID which can be used to reference this datasource in other parts of the configuration, if not specified will be generated automatically     uid: victoria_metrics     # <string> url     url: http://my.victoria.metrics:8481/select/0/prometheus     # <string> Deprecated, use secureJsonData.password     password:     # <string> database user, if used     user:     # <string> database name, if used     database:     # <bool> enable/disable basic auth     basicAuth:     # <string> basic auth username     basicAuthUser:     # <string> Deprecated, use secureJsonData.basicAuthPassword     basicAuthPassword:     # <bool> enable/disable with credentials headers     withCredentials:     # <bool> mark as default datasource. Max one per org     isDefault: true     # <map> fields that will be converted to json and stored in jsonData     jsonData:     # <string> json object of data that will be encrypted.     secureJsonData:     # datasource version     version: 1     # <bool> allow users to edit datasources from the UI.     editable: false 
dashboards.yml
apiVersion: 1  providers:   # <string> an unique provider name. Required   - name: dashboards     # <int> Org id. Default to 1     orgId: 1     # <string> name of the dashboard folder.     folder: ''     # <string> folder UID. will be automatically generated if not specified     folderUid: ''     # <string> provider type. Default to 'file'     type: file     # <bool> disable dashboard deletion     disableDeletion: false     # <int> how often Grafana will scan for changed dashboards     updateIntervalSeconds: 10     # <bool> allow updating provisioned dashboards from the UI     allowUiUpdates: false     options:       # <string, required> path to dashboard files on disk. Required when using the 'file' type       path: /var/lib/grafana/dashboards       # <bool> use folder names from filesystem to create folders in Grafana       foldersFromFilesStructure: true 

Согласно нашей конфигурации Grafana должна создать Data Source типа prometheus с URL http://my.victoria.metrics:8481/select/0/prometheus. Также из каталога /var/lib/grafana/dashboards должны быть импортированы каталоги и дашборды.

Таким образом, мы получаем полностью определяемое состояние Grafana из кода.

Dashboards as Code

Перейдем к самим JSON-файлам дашбордов. Те, кто видел эти JSON-ы, справедливо сделают замечание о том, что формировать и поддерживать их вручную (без Grafana UI) невозможно. С этим я соглашусь, но к счастью, для этого создали специальный фреймворк grafonnet-lib, который позволяет писать дашборды с использованием языка Jsonnet.

Указанный фреймворк уже содержит набор функций, с помощью которых можно формировать панели для дашбордов. Язык Jsonnet также позволяет писать собственные функции, а также структурировать код, раскладывая его по отдельным файлам и каталогам.

Язык Jsonnet очень простой, поэтому инженер даже с небольшими навыками программирования сможет через пару часов экспериментов создать свой первый дашборд Grafana из кода.

GitOps

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

Мы давно у себя используем Gitlab для хранения наших инфраструктурных репозиториев, а также Gitlab CI для CI/CD.

Собрав всё в кучу, мы получили следующую структуру каталогов.

  • /сi — файлы, используемые в Gitlab CI

  • /grafonnet-lib — git submodule для исходников фреймворка grafonnet-lib

  • /dev — конфигурация мониторинга DEV-окружения

  • /stage — то же самое для STAGE

  • /prod — то же самое для PROD

  • /tests — файлы для тестирования дашбордов (например docker-compose для запуска Grafana)

Каждый из каталогов dev, stage, prod в свою очередь содержит следующий набор каталогов:

  • alertmanager

  • blackbox

  • grafana

  • vmagent

  • vmalert

В указанных каталогах хранится конфигурация соответствующих компонентов системы мониторинга. В каталоге grafana, кроме конфигурации Provisioning, хранятся также исходники дашбордов на языке jsonnet, которые компилируются в JSON-файлы в процессе деплоя в Gitlab CI.

Конфигурация Gitlab CI у нас выглядит следующим образом:

.gitlab-ci.yml
variables:   CA_CERT_FILE: /etc/gitlab-runner/certs/ca.crt   VMETRICS_IMAGE: $CI_REGISTRY_IMAGE/vm:ci-0.0.5   JSONNET_IMAGE: $CI_REGISTRY_IMAGE/jsonnet:ci-0.0.5   YAMLLINT_IMAGE: cytopia/yamllint:1.26   RSYNC_IMAGE: instrumentisto/rsync-ssh:alpine3.14    # can be overrided in project CI/CD settings   GRAFANA_USER: admin   GRAFANA_PASSWORD: admin    # should be defined in project CI/CD settings   #SSH_PRIVATE_KEY_FILE:   #SSH_USERNAME:  include:   - local: '*/.gitlab-ci.yml'  stages:   - build_image   - validate   - build   # - review   - deploy  build_image:   before_script: []   stage: build_image   image:     name: gcr.io/kaniko-project/executor:debug     entrypoint: [""]   variables:     REGISTRY_TAG: $CI_COMMIT_TAG     CONTEXT_DIR: $CI_PROJECT_DIR/ci/$IMAGE_NAME   script:     - mkdir -p /kaniko/.docker     - cat $CA_CERT_FILE >> /kaniko/ssl/certs/additional-ca-cert-bundle.crt     - cp -L $CA_CERT_FILE $CONTEXT_DIR/ca.crt     - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json     - /kaniko/executor --context $CONTEXT_DIR --dockerfile $CONTEXT_DIR/Dockerfile $BUILD_ARGS --destination $CI_REGISTRY_IMAGE/$IMAGE_NAME:$REGISTRY_TAG   cache: {}   rules:     - if: '$CI_COMMIT_TAG =~ /^ci-.*/'   parallel:     matrix:       - IMAGE_NAME: jsonnet         GO_JSONNET_VERSION: '0.17.0'         BUILD_ARGS: '--build-arg GO_JSONNET_VERSION'       - IMAGE_NAME: vm         VMETRICS_TAG: 'v1.62.0'         ALERTMANAGER_TAG: 'v0.22.2'         BLACKBOX_TAG: 'v0.19.0'         BUILD_ARGS: '--build-arg VM_VERSION --build-arg ALERTMANAGER_TAG --build-arg BLACKBOX_TAG'  .yamllint:   stage: validate   image:     name: $YAMLLINT_IMAGE     entrypoint: [""]   script:     - yamllint ${WORK_DIR}/ -c .yamllint.yml  .jsonnetlint:   stage: validate   image: $JSONNET_IMAGE   variables:     GIT_SUBMODULE_STRATEGY: recursive     JSONNET_PATH: $CI_PROJECT_DIR/grafonnet-lib   script:     - cd $WORK_DIR/grafana/grafonnet     - if [[ -f "dashboards.jsonnet" ]]; then jsonnetfmt --test $(find . -name '*.jsonnet'); fi     - if [[ -f "dashboards.jsonnet" ]]; then jsonnet-lint dashboards.jsonnet; fi  .config_validate:   stage: validate   image: $VMETRICS_IMAGE   script:     - vmalert -dryRun -rule ${WORK_DIR}/vmalert/*.yml     - vmagent -dryRun -promscrape.config ${WORK_DIR}/vmagent/scrape_config.yml -promscrape.config.strictParse     - blackbox_exporter --config.check --config.file=${WORK_DIR}/blackbox/blackbox.yml     - amtool check-config ${WORK_DIR}/alertmanager/  .build:   stage: build   image: $JSONNET_IMAGE   variables:     GIT_SUBMODULE_STRATEGY: recursive     JSONNET_PATH: $CI_PROJECT_DIR/grafonnet-lib   script:     - cd ${WORK_DIR}/grafana/grafonnet     - jsonnet -m dashboards -c -V dasboardEditable=false dashboards.jsonnet   artifacts:     paths:       - ${WORK_DIR}/grafana/grafonnet/dashboards  .deploy:   stage: deploy   image: $RSYNC_IMAGE   variables:     SSH_CONFIG: |       Host *         StrictHostKeyChecking no         UserKnownHostsFile=/dev/null         LogLevel ERROR     SSH_SERVER: $SSH_USERNAME@$MON_SERVER     GRAFANA_PORT: 3000     GRAFANA_SCHEME: http   environment:     name: $WORK_DIR   script:     - set -x     - eval $(ssh-agent -s)     - mkdir ~/.ssh/     - chmod 700 ~/.ssh     - echo "$SSH_CONFIG" > ~/.ssh/config     - cat $SSH_PRIVATE_KEY_FILE | tr -d '\r' | ssh-add - > /dev/null     - alias rsync="rsync -ai --delete --no-perms --no-owner --no-group --rsync-path='sudo rsync' --timeout=15"     - RSYNC_OUT=$(rsync ${WORK_DIR}/vmagent $SSH_SERVER:/opt/vm/)     - |         if [ -n "$RSYNC_OUT" ]; then           ssh $SSH_SERVER "sudo chown -R vmcluster:vmcluster /opt/vm/vmagent/* && sudo systemctl reload vmagent.service"         fi     - RSYNC_OUT=$(rsync ${WORK_DIR}/vmalert $SSH_SERVER:/opt/vm/)     - |         if [ -n "$RSYNC_OUT" ]; then           ssh $SSH_SERVER "sudo chown -R vmcluster:vmcluster /opt/vm/vmalert/* && sudo systemctl reload vmalert.service"         fi     - RSYNC_OUT=$(rsync ${WORK_DIR}/blackbox/blackbox.yml $SSH_SERVER:/opt/blackbox_exporter/)     - |         if [ -n "$RSYNC_OUT" ]; then           ssh $SSH_SERVER "sudo chown -R blackbox_exporter:blackbox_exporter /opt/blackbox_exporter/blackbox.yml && sudo systemctl reload blackbox_exporter.service"         fi     - rsync ${WORK_DIR}/grafana/provisioning $SSH_SERVER:/etc/grafana/     - ssh $SSH_SERVER "sudo chown -R root:grafana /etc/grafana/provisioning"     - rsync ${WORK_DIR}/grafana/grafonnet/dashboards $SSH_SERVER:/var/lib/grafana/     - ssh $SSH_SERVER "sudo chown -R grafana:grafana /var/lib/grafana/dashboards"     - ssh $SSH_SERVER "curl -X POST -sSf $GRAFANA_SCHEME://$GRAFANA_USER:$GRAFANA_PASSWORD@localhost:$GRAFANA_PORT/api/admin/provisioning/dashboards/reload"     - ssh $SSH_SERVER "curl -X POST -sSf $GRAFANA_SCHEME://$GRAFANA_USER:$GRAFANA_PASSWORD@localhost:$GRAFANA_PORT/api/admin/provisioning/datasources/reload"

Какие действия мы выполняем в CI/CD:

  1. Валидация всех файлов конфигурации (yamllint + check конфигов всех компонентов)

  2. Компиляция дашбордов Grafana

  3. Деплой всей конфигурации на сервер мониторинга (также можно использовать несколько инстансов, объединенных в кластер).

Для деплоя мы используем обычный rsync с набором необходимых ключей (например, для удаления лишних файлов на сервере назначения).

Для локальной разработки мы используем скрипт, который компилирует дашборды и запускает Grafana в docker-compose. Разработчик дашборда может сразу увидеть внесенные изменения.

Заключение

В данной статье описаны этапы автоматизации системы мониторинга на базе Prometheus и Grafana. Используемые подходы позволяют решить ряд задач:

  • Используя Service Discovery, мы получаем полную автоматизацию добавления и удаления хостов в мониторинг. Т.е. новые машины встают на мониторинг сразу после деплоя. Для удаления машин с мониторинга, можно использовать любые механизмы (наприме, можно использовать Destroy-Time Provisioners для Terraform, который будет выполнять дерегистрацию сервиса в Consul)

  • С помощью maintenance-режима мы можем выводить хосты на обслуживание и не получать при этом лишних алертов. Дежурная смена может спать спокойно 🙂

  • Используя подход Grafana as Code, мы получаем полностью детерминированное состояние наших дашбордов. При внесении изменений в конфигурацию Prometheus, мы сразу вносим изменения в дашборды.

  • Используя Gitlab CI, мы выстраиваем процесс GitOps для нашей системы мониторинга. Т.е. Git становится единым источником правды для всей системы мониторинга. Больше не требуется никаких ручных кликов в Grafana UI и никакой правки файлов конфигурации в консоли Linux.

  • И самое главное: теперь наши разработчики могут приходить в этот репозиторий, вносить изменения и присылать Pull Request.

Всем спасибо за внимание! Буду рад ответить на любые вопросы касательно данной темы.

ссылка на оригинал статьи https://habr.com/ru/post/568090/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *