← Все статьи

Алерты, которые не могут сработать: слепой ServiceMonitor и мёртвый канал доставки

Пир Vault 32 часа был sealed, а секреты неделю не синхронизировались — при алерте VaultSealed в репозитории и здоровом Alertmanager. Инцидент спрятали два независимых сбоя.

В Optimacros пир Vault 32 часа простоял в состоянии sealed, а 53 из 55 объектов VaultStaticSecret больше недели не синхронизировались. Алертов не было никому. При этом алерт VaultSealed лежал в репозитории, Prometheus был здоров, Alertmanager работал.

Друг на друга наложились два независимых сбоя, и любого одного хватило бы, чтобы спрятать инцидент.

Сбой первый: алерт был слеп по построению

Helm-чарт Vault ставит ServiceMonitor, который выбирает поды с меткой vault-active: "true". В трёхнодовом Vault это ровно один под — активный лидер. А активный под по определению распечатан: запечатанная нода не может быть лидером Raft.

Получается, Prometheus скрейпил только тот под, который никогда не мог оказаться sealed. Резервные пиры — как раз те, что возвращаются запечатанными после рестарта, — не скрейпились вовсе. Выражению vault_core_unsealed == 0 было не на чем вычисляться.

Ирония в том, что метрика была доступна. Неаутентифицированный доступ к метрикам был включён, и запечатанный под спокойно бы её отдал. Повязкой на глазах был селектор, а не экспортер.

Сбой второй: доставка два дня была мертва

Каждое уведомление Alertmanager падало с connect: connection timed out. Хост API Telegram из этого кластера резолвился в адрес, который был недоступен, а другой адрес дата-центра Telegram отвечал нормально и с валидным сертификатом. Общий выход наружу работал — другие внешние сервисы отвечали, — значит, это была блокировка конкретного адреса, а не политика файрвола. Небольшой статус-бот на том же API тоже молча падал.

Алерты срабатывали. Они просто не покидали кластер.

Исправление

  1. Направить ServiceMonitor на все пиры: поменять селектор на метку headless-сервиса, за которым стоят все поды Vault. Чарт позволяет это через values, патчить шаблоны не нужно.
  2. Добавить алерт по истине, независимой от телеметрии Vault: запечатанный под никогда не бывает Ready, а kube-state-metrics всегда отдаёт готовность. Алерт на kube_pod_status_ready == 0 для подов Vault ловит seal, даже когда собственных метрик Vault нет.
  3. Алертить на отсутствие: absent(vault_core_unsealed). Правило вида == 0 никогда не сработает на ряд, которого нет.
  4. Восстановить доставку: закрепить доступный адрес Telegram через hostAliases в Alertmanager и в статус-боте.

Неудобные оговорки

  • Закрепление IP — обходной путь. Если заблокируют и этот адрес, доставка снова молча умрёт. Надёжный ответ — второй, независимый канал (почта для критических алертов) или egress-прокси.
  • AlertmanagerFailedToSendAlerts существует, но доставляется через тот же сломанный канал. Этот класс отказов ловит только dead man’s switch — внешний получатель, который ждёт регулярный heartbeat и поднимает шум, когда тот пропадает.
  • Клиентская библиотека Telegram писала в лог полный URL запроса вместе с токеном бота открытым текстом. Любой с доступом к логам владел ботом. Токен мы перевыпустили.

Выводы

  • metric == 0 слеп к отсутствию метрики. Пишите отдельный алерт на отсутствие.
  • Селектор целей может тихо исключить ровно те экземпляры, которые ломаются. Сверяйте число реально скрейпящихся целей с числом экземпляров.
  • Предпочитайте истину самооценке. Сломанный компонент хуже всех способен сообщить, что он сломан.
  • Проверяйте путь доставки, а не только правило. Сработавший алерт, который не покинул кластер, неотличим от отсутствия алерта.