Приветствую всех любителей 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. Этот механизм работает следующим образом:
Мы пишем конфигурацию наших data sources, plugins, dashboards и складываем её в определенный каталог.
Grafana при старте создает все описанные в YAML объекты и импортирует дашборды из указанного каталога.
При импорте дашбордов есть следующие возможности:
Grafana может импортировать структуру каталогов и создать их у себя в UI. Импортированные дашборды будут разложены по каталогам в соответствии с расположением JSON-файлов.
После импорта дашборды можно сделать нередактируемыми через UI (актуально, если планируете вносить все изменения только через код).
Для дашбордов можно задать статические uid, чтоб определить зафиксировать ссылки на получившиеся дашборды.
Grafana умеет перечитывать содержимое каталога и применять изменения в дашбордах.
Если JSON-файл исчез из каталога, Grafana может соответственно убирать его из UI.
Пример конфигурации Data Sources:
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:
Валидация всех файлов конфигурации (yamllint + check конфигов всех компонентов)
Компиляция дашбордов Grafana
Деплой всей конфигурации на сервер мониторинга (также можно использовать несколько инстансов, объединенных в кластер).
Для деплоя мы используем обычный 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.
Всем спасибо за внимание! Буду рад ответить на любые вопросы касательно данной темы.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какой стек вы используете для мониторинга?
-
100,0%Zabbix1
-
0,0%Prometheus0
-
0,0%Netdata0
-
0,0%Nagios0
-
0,0%другое0
Как вы управляете конфигурацией?
-
0,0%Ручное редактирование конфигурационных файлов0
-
100,0%Web UI1
-
0,0%GitOps0
-
0,0%другое0