Всем привет! Работая DevOps-инженером, я задумался о мониторинге IPsec-туннелей, которых у нас уже накопилось достаточно. Они в основном используются для связи между облаками, так как инфраструктура разнесена — например, dev и prod живут у разных облачных провайдеров. Также есть интеграции со сторонними организациями, кластеры Kubernetes в AWS, GCP и т.д. Основная цель — получать алерты о падении туннеля раньше, чем сработают алерты о недоступности сервисов. Это особенно важно, поскольку Prometheus у нас один, он живёт в одном из облаков, а prometheus-stack в Kubernetes-кластерах работают в режиме агентов.
Первая проблема — выбор экспортера или разработка своего
Изначально наткнулся на экспортер от dennisstritzke, но проект уже архивный, последний релиз датируется сентябрем 2021 года, в README автор рекомендует использовать более свежий и поддерживаемый экспортер. Однако он использует VICI, соответственно необходима миграция с более старого подхода конфигурирования с помощью ipsec.conf на swanctl.conf. В документации есть подробное описание, и даже ссылка на скрипт-конвертор. Но зачем ломать то, что уже работает, пусть даже и deprecated? В итоге написал свой python скрипт, который дергает ipsec status, парсит вывод и формирует необходимые мне метрики для Prometheus.
app.py
Скрытый текст
#!/usr/bin/env python3 import time import logging import subprocess from prometheus_client import start_http_server, Gauge import re # Настройка логирования logging.basicConfig( level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s", handlers=[ logging.StreamHandler() ] ) # Инициализация метрик UP_TUNNELS = Gauge('ipsec_up_tunnels', 'Number of active IPsec tunnels') CONNECTING_TUNNELS = Gauge('ipsec_connecting_tunnels', 'Number of connecting IPsec tunnels') def get_tunnel_metrics(): """Получаем количество активных и подключающихся туннелей StrongSwan через ipsec status.""" try: logging.info("Выполнение команды `ipsec status` для получения состояния туннелей.") # Выполнение команды ipsec status result = subprocess.run( ["ipsec", "status"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) if result.returncode != 0: logging.error(f"Ошибка при выполнении команды `ipsec status`: {result.stderr.strip()}") UP_TUNNELS.set(0) CONNECTING_TUNNELS.set(0) return status_output = result.stdout if not status_output.strip(): logging.warning("Вывод команды `ipsec status` пустой. Устанавливаю метрики в 0.") UP_TUNNELS.set(0) CONNECTING_TUNNELS.set(0) return # Ищем строку "Security Associations (X up, Y connecting)" sa_match = re.search(r"Security Associations \((\d+) up, (\d+) connecting\):", status_output) if sa_match: up_tunnels = int(sa_match.group(1)) connecting_tunnels = int(sa_match.group(2)) logging.info(f"Активные туннели (up): {up_tunnels}, подключающиеся туннели (connecting):" f" {connecting_tunnels}") else: logging.warning("Не удалось найти строку Security Associations. Устанавливаю метрики в 0.") up_tunnels = 0 connecting_tunnels = 0 # Обновляем метрики UP_TUNNELS.set(up_tunnels) CONNECTING_TUNNELS.set(connecting_tunnels) except Exception as ee: logging.exception(f"Ошибка при сборе метрик: {ee}") UP_TUNNELS.set(0) CONNECTING_TUNNELS.set(0) if __name__ == '__main__': port = 9641 try: logging.info(f"Запуск IPSec Exporter на порту {port}") start_http_server(port) logging.info(f"HTTP-сервер успешно запущен на порту {port}") except Exception as e: logging.exception(f"Не удалось запустить HTTP-сервер на порту {port}: {e}") exit(1) # Основной цикл опроса метрик while True: try: get_tunnel_metrics() except Exception as e: logging.exception(f"Неожиданная ошибка в основном цикле: {e}") time.sleep(5) # Опрос каждые 5 секунд
Dockerfile:
Скрытый текст
FROM python:3.9-slim WORKDIR /usr/local/bin RUN pip install --no-cache-dir prometheus-client RUN apt-get update && apt-get install -y --no-install-recommends \ strongswan \ && rm -rf /var/lib/apt/lists/* COPY docker/ipsec-exporter/app.py . RUN chmod +x app.py EXPOSE 9641 CMD ["app.py"]
docker-compose.yaml
Скрытый текст
services: ipsec_exporter: image: prometheus_exporters/ipsec-exporter:latest restart: unless-stopped pid: "host" ports: - "9641:9641" volumes: - /var/run/starter.charon.pid:/var/run/starter.charon.pid - /var/run/charon.pid:/var/run/charon.pid - /var/run/charon.ctl:/var/run/charon.ctl
И вроде всё хорошо, метрики в Prometheus прилетают, настроили алерты:
Скрытый текст
- alert: NoActiveIPSecTunnels expr: ipsec_up_tunnels == 0 for: 1m labels: severity: average annotations: summary: "Нет активных туннелей IPsec" description: "Все туннели IPsec неактивны в течение минуты." - alert: TooManyConnectingIPSecTunnels expr: ipsec_connecting_tunnels > 0 for: 30s labels: severity: warning annotations: summary: "Не все ipsec туннели в статусе up" description: "Количество подключающихся туннелей IPsec {{ $value }}."
Но сама идея пробрасывать через volumes сокет charon в контейнер мягко говоря не очень, потому как при выполнении команды ipsec restart на хостовой машине связь с сокетом пропадала и не восстанавливалась, что логично. Привожу подробные конфиги для тех кто захочет повторить нечто подобное, возможно для других целей. Дорабатывать скрипт, писать какие-то дополнительные, костыльные решения не было желания, поэтому от собственного экспортера быстро отказались. Решили таки переписать конфиги туннелей и использовать готовый экспортер.
Вторая проблема — лаконичность конфигов или «те же яйца, только в профиль»
Для миграции с ipsec.conf на swanctl.conf первым делом я попробовал использовать скрипт-конвертер. Я добавил его в PyCharm, создал необходимые директории и файлы. Скрипт отработал, но на выходе я не получил ожидаемого результата. Видимо, требовался рефакторинг кода или использование более старых версий Python. Конечно, самый правильный подход это использование документации и самостоятельное формирование конфигов, но это занимает уж очень много времени. В итоге за основу была взята статья неизвестного мне автора. Привожу пример одного из своих старых конфигов и то, что получилось при его миграции:
Старый подход — /etc/ipsec.conf
Скрытый текст
# ipsec.conf - strongSwan IPsec configuration file config setup charondebug="all" uniqueids=yes strictcrlpolicy=no conn tun-to-rogaikopyta authby=secret left=%defaultroute leftid=my_public_ip leftsubnet=my_subnet right=remote_public_ip rightid=remote_public_ip rightsubnet=remote_subnet ike=aes256-sha1-modp1024 esp=aes256-sha1-modp1024 keyingtries=1 leftauth=psk rightauth=psk keyexchange=ikev1 ikelifetime=24h lifetime=1h auto=route conn tun-to-rogaikopyta-2 also=tun-to-rogaikopyta leftsubnet=my_subnet rightsubnet=remote_subnet1 conn tun-to-rogaikopyta-3 also=tun-to-rogaikopyta leftsubnet=my_subnet rightsubnet=remote_subnet2 conn tun-to-rogaikopyta-4 also=tun-to-rogaikopyta leftsubnet=my_subnet rightsubnet=remote_subnet3 ...
Новый подход — /etc/swanctl/conf.d/ipsec.conf
Скрытый текст
connections { tun-to-rogaikopyta { version = 1 local_addrs = my_private_ip remote_addrs = remote_public_ip proposals = aes256-sha1-modp1024 keyingtries = 1 local { auth = psk id = my_public_ip } remote { auth = psk id = remote_public_ip } children { tun-to-rogaikopyta-1 { mode = tunnel local_ts = my_subnet remote_ts = remote_subnet1 start_action = trap esp_proposals = aes256-sha1-modp1024 } tun-to-rogaikopyta-2 { mode = tunnel local_ts = my_subnet remote_ts = remote_subnet2 start_action = trap esp_proposals = aes256-sha1-modp1024 } tun-to-rogaikopyta-3 { mode = tunnel local_ts = my_subnet remote_ts = remote_subnet3 start_action = trap esp_proposals = aes256-sha1-modp1024 } ...
Одним из основных преимуществ перехода на новую модель конфигурирования ipsec туннелей многие называют лаконичность конфигов. С одной стороны это правда, ведь мы могли сделать так:
Скрытый текст
connections { tun-to-rogaikopyta { version = 1 local_addrs = my_private_ip remote_addrs = remote_public_ip proposals = aes256-sha1-modp1024 keyingtries = 1 local { auth = psk id = my_public_ip } remote { auth = psk id = remote_public_ip } children { tun-to-rogaikopyta-1 { mode = tunnel local_ts = my_subnet remote_ts = remote_subnet1, remote_subnet2, remote_subnet3 start_action = trap esp_proposals = aes256-sha1-modp1024
В моём случае это бы сильно помогло, но есть одно но, с другой стороны Cisco ASA. Она принадлежит сторонней компании, доступа к ней у меня нет. А просить тамошних сетевых инженеров перенастроить туннели с их стороны, потому что я гонюсь за лаконичностью такое себе. Новый конфиг был протестирован в тестовом окружении, для этого пришлось развернуть в облаке две машины, две VPC, две таблицы маршрутизации и т.д. После тестирования, в не рабочее время было организовано переключение.
Третья проблема — скупой README.md в проектах
После успешного поднятия туннелей на одном из серверов, решил запустить экспортер и посмотреть какие метрики он отдаёт. В README проекта на github есть пример запуска экспортера в docker:
docker run -it -p 8079:8079 -v $(pwd)/my-config.yaml:/config.yaml --rm torilabs/ipsec-prometheus-exporter:latest
Удивление вызвало то, что при внесении правок в config.yaml и перезапуска контейнера ничего не менялось, потому что в entrypoint не было упоминаний о config.yaml.
По умолчанию сокет vici в Ubuntu имеет следующий адрес socket = unix://var/run/charon.vici. А экспортер может подключаться по tcp, либо по udp. Для того чтобы заставить его работать по tcp привёл конфиги strongswan к следующему виду:
Скрытый текст
# cat /etc/strongswan.d/charon/vici.conf vici { # Whether to load the plugin. Can also be an integer to increase the # priority of this plugin. load = yes # Socket the vici plugin serves clients. # socket = unix://var/run/charon.vici socket = tcp://127.0.0.1:4502 }
# cat /etc/strongswan.d/swanctl.conf swanctl { # Plugins to load in swanctl. # VICI socket to connect to by default. # socket = unix://var/run//charon.vici socket = tcp://127.0.0.1:4502 }
# cat /etc/strongswan.conf # strongswan.conf - strongSwan configuration file # # Refer to the strongswan.conf(5) manpage for details # # Configuration changes should be made in the included files charon { load_modular = yes plugins { include strongswan.d/charon/*.conf } } include strongswan.d/*.conf
Так как vici теперь работает на localhost хостовой машины, будем запускать docker контейнер с параметром network_mode: «host», финальный конфиг экспортера и docker-compose.yaml будет выглядеть следующим образом:
Скрытый текст
# cat config.yaml # Logger configuration logging: level: DEBUG # HTTP server configuration server: port: 8079 # Vici configuration vici: network: "tcp" host: "127.0.0.1" port: 4502
# cat docker-compose.yml services: ipsec-exporter: network_mode: "host" image: torilabs/ipsec-prometheus-exporter:v0.2.1 command: ["--config=/config.yaml"] restart: always volumes: - ./config.yaml:/config.yaml
Проверить метрики можно командой — curl http://localhost:8079/metrics
Далее осталось сделать дашборд в Grafana и настроить аллертинг, но это уже тема для отдельной статьи. За код на python, орфографию, пунктуацию и network_mode: «host» в docker-compose прошу сильно не пинать :).
ссылка на оригинал статьи https://habr.com/ru/articles/862506/
Добавить комментарий