Приветствую всех любителей 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.
Примеры конфигурации 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:
-
Валидация всех файлов конфигурации (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.
Всем спасибо за внимание! Буду рад ответить на любые вопросы касательно данной темы.
ссылка на оригинал статьи https://habr.com/ru/post/568090/
Добавить комментарий