Приветствую всех любителей 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.
Всем спасибо за внимание! Буду рад ответить на любые вопросы касательно данной темы.
33.33%
Zabbix
1
66.67%
Prometheus
2
0%
Netdata
0
0%
Nagios
0
0%
другое
0
Воздержались 3 пользователя.
50%
Ручное редактирование конфигурационных файлов
1
50%
Web UI
1
0%
GitOps
0
0%
другое
0
Воздержались 3 пользователя.