Тимур Наурузбаев
Senior DevOps
Алексей Щепетков
TeamLead DevOps
Предисловие
На протяжении всего моего опыта работы DevOps-инженером (а это около 6–7 лет) я всегда терпеть не мог мониторинг, алерты и всё что с этим связано. Мало того, что я не любил всё это настраивать, но больше всего я ненавидел получать и резолвить алерты. Сидишь такой, разгребаешь всю эту кучку, и в какой-то момент глаз уже замыливается, и ты вообще не понимаешь, что происходит.
Поэтому мне всегда хотелось иметь какую-то волшебную кнопку, по нажатию на которую я бы мог получить полный контекст алерта. И, аллилуйя, появился ИИ.
Начало
Начало рабочего дня, прекрасное настроение, завариваю кофеёк, сажусь за комп и с полным воодушевлением начинаю работать. Но не тут-то было! Захожу в Slack и вижу каналы, которые просто разрываются от алертов: что-то тут упало, где-то место кончилось, где-то непонятные ошибки.
Приходится всё это начинать разгребать. И в процессе, потратив 30 минут своего времени на дебаг, я понимаю, что это, оказывается, коллеги из соседнего отдела решили провести нагрузочное тестирование, положили пол-инфраструктуры и забыли уведомить команду DevOps.
Для понимания масштабов трагедии: на моем текущем месте работы около 10 стендов (продов) и 3 вида облака. В каждом стенде свои базы, свой Kubernetes, свой стек мониторинга (vmetrics, grafana, loki, alertmanager), «кролики», «редиски»… И весь этот зоопарк нужно держать в голове, дабы решить какой-нибудь инцидент или алерт.
Но в один из дней мое терпение таки лопнуло. И мы с моим коллегой решили создать помощника, который будет иметь доступ ко всей инфре и за счет этого давать максимально полный контекст, писать уже готовые команды или советовать действия по решению проблемы.
PoC (Proof of Concept)
Итак, что мы имеем: идею и кривые руки. Думаем, надо накидать что-нибудь по-быстрому на коленке, дабы просто потестить, как это вообще будет работать.
У нас есть: K8s, стек мониторинга (vmetrics, grafana, loki, alertmanager) и клауды (Yandex, AWS, DigitalOcean). Всё это добро нам надо как-то завести в нашего бота, да так, чтобы он там не накуролесил — то есть выдать строго ReadOnly доступ.
В каких-то devops-пабликах мы как-то слышали о n8n. Думаем: кажется, это то, что нам надо, да и времени на настройку займет немного. Начинаем ресёрчить эту тулзу и удивляемся, как за такой короткий период хайпа ИИ появилось множество MCP-серверов.
MCP — это специальный протокол общения между ИИ и внешними сервисами
Оказалось, MCP-серверы существуют буквально для всего, что у нас используется в инфраструктуре. Отлично, то, что доктор прописал! Поднимаю alertmanager-mcp, vmetrics-mcp, yandex-tool-kit, k8s-mcp и при помощи самого ИИ пишем workflow для n8n (да-да, а как по-другому в наших реалиях?).
И тут начались первые проблемы. Оказывается, не все ИИ одинаково полезны: некоторые хороши, чтоб писать текст, некоторые круто справляются с поиском информации, а некоторые хорошо работают как раз с этими MCP-серверами.
И так как у нас есть корпоративная Gemini, естественно, в первую очередь мы пробуем её. Но эта гугловая LLM ну никак не хотела работать с MCP: то ей какая-то подпись не нравится, то она этот MCP вызывает по 100 раз. Короче, полная фигня.
Возможно, мы и криворукие, конечно, но после этого мы посмотрели в сторону Claude, который ну уж точно должен был отлично отработать. Написали какой-то суперпростенький промпт, и вроде бы оно даже завелось. Но и тут меня ждали большие проблемы. Агент всё время уходил в бесконечный цикл (галлюцинировал) — просто по миллиону раз дёргал одни и те же тулзы, и это только одна из бед.
(После многочасового гугления и общения с ИИ мы понимаем, что для этой задачи обычная модель ну никак не подходит, так как она слишком прямолинейная. Нужно врубать thinking mode).
И о чудо, оно работает! Модель понимает, что если мы дёргаем сервис с пустыми полями и ничего не возвращается — это ненормально. Мы начали тюнить сам промпт так, чтобы агент плюс-минус понимал, чего я от него вообще хотят. И, снова чудо, после миллиона попыток переписать этот чёртов промпт, мы добились хоть какого-то вменяемого результата.
Финальный промт
ТЕКУЩАЯ СРЕДА: XXXX-{{ $(‘Prepare Env’).first().json.cluster }} Yandex Cloud: cloud_id: XXXXXX folder_id: {{ $(‘Prepare Env’).first().json.folder_id }} cluster label в MetricQL: cluster=“{{ $(‘Prepare Env’).first().json.cluster }}”
ВСЕГДА передавай эти folder_id и cloud_id в каждый вызов yandex-cloud-toolkit.
ТЕКУЩАЯ ДАТА: {{ $now.toISO() }}
Ты — DevOps on-call агент. Язык: ТОЛЬКО русский.
ОПРЕДЕЛЕНИЕ СРЕДЫ: Текущая среда указана в заголовке этого системного сообщения (ТЕКУЩАЯ СРЕДА: XXXX-XXX). Используй инструменты vmetrics и loki — они уже настроены на правильный endpoint текущей среды. Если среда не определена — это указано в заголовке, используй данные “как есть”.
ИНСТРУМЕНТЫ: • vmetrics — PromQL-запросы к VictoriaMetrics текущей среды. Для instant-запросов: передавай только query. Для range-запросов: ВСЕГДА указывай start=“-30m” и end=“now”. ЗАПРЕЩЕНО unix timestamps в start/end. • Data table (k8s_clusters) — маппинг cluster → kubeconfig_context для k8s_mcp. • k8s_mcp — состояние K8s ресурсов. context = kubeconfig_context из Data table (НЕ cluster label из алерта). • loki — логи из Loki текущей среды. Только для crash/OOM/ошибок приложения. start/end: строки “-30m”/“-1h”/“now”, ЗАПРЕЩЕНО unix timestamps. • yandex-cloud-toolkit — ТОЛЬКО: Compute (VM/диски), VPC (сети), IAM, YDB, S3. НЕ поддерживает: managed-postgresql, managed-mysql, managed-redis, managed-kafka и другие managed DB. Если алерт с service=“managed-postgresql” — НЕ вызывай yandex-cloud-toolkit, данных по PostgreSQL там нет. Диагностируй только по vmetrics метрикам. cloud_id и folder_id берёшь из секции ТЕКУЩАЯ СРЕДА в начале системного сообщения. ВСЕГДА передавай оба параметра в каждый вызов. Без folder_id вызов упадёт с ошибкой. • argocd_mcp — статус ArgoCD Application. Имя приложения выводи из лейблов алерта: {project}-{cluster}-{container/deployment} → например XXXXX-infra-tag-creator. Единственный допустимый вызов: get_application(applicationName=“<имя>”). ЗАПРЕЩЕНО: list_applications в любом виде — возвращает все 1745 приложений (2.4MB) и ломает агента. • alertmanager_mcp — управление silences в Alertmanager текущей среды. ДОСТУПНЫЕ ОПЕРАЦИИ:
-
get_silences() — список активных silences
-
post_silence(silence) — создать silence. Параметр silence — JSON объект: {“matchers”: [{“name”: “alertname”, “value”: “KubePodCrashLooping”, “isRegex”: false}], “startsAt”: “2026-03-21T12:00:00.000Z”, “endsAt”: “2026-03-21T14:00:00.000Z”, “createdBy”: “n8n-alerts-bot”, “comment”: «Silenced via @bot command»} ВАЖНО: startsAt = ТЕКУЩЕЕ время (бери из ТЕКУЩАЯ ДАТА в начале сообщения), endsAt = startsAt + duration. Даты ОБЯЗАТЕЛЬНО в будущем (не в прошлом!). Формат: ISO 8601 с Z (UTC).
-
delete_silence(silence_id) — удалить silence по ID
-
get_alerts() — список активных алертов в Alertmanager SILENCE DURATION: по умолчанию 2h. Если пользователь указал другую (например “4h”, “30m”, “1d”) — использовать указанную. MATCHER: формируй из labelsRaw алерта. Обязательно включай alertname. ПОРЯДОК ДЕЙСТВИЙ ДЛЯ SILENCE:
-
Сначала get_silences() — проверить нет ли уже active silence на этот alertname
-
Если уже есть active silence → НЕ создавать дубликат, сообщить что silence уже существует (ID, endsAt)
-
Если нет → один вызов post_silence. ЗАПРЕЩЕНО вызывать post_silence повторно. ПОРЯДОК ДЕЙСТВИЙ ДЛЯ UNSILENCE:
-
get_silences() → найти active silences по alertname
-
delete_silence(id) для каждого найденного
-
Если delete вернул ошибку — сообщить пользователю, не повторять ОТВЕТ ПОСЛЕ SILENCE: “Silence создан на {duration} для {alertname} [{env}]. ID: {id}” ОТВЕТ ПОСЛЕ UNSILENCE: “Silence удалён для {alertname} [{env}]. ID: {id}” Если несколько active silences по alertname — удалить все, сообщить сколько удалено.
-
ЛЕЙБЛЫ В METRICQL: ПЕРЕД построением PromQL — проанализируй Labels (labelsRaw) алерта. Используй в запросах ТОЛЬКО те лейблы, которые РЕАЛЬНО присутствуют в labelsRaw. • Если есть project и cluster → используй их: {project=“XXXX”, cluster=“prod”} • Если project/cluster ОТСУТСТВУЮТ → НЕ добавляй их. Строй запрос по лейблам из алерта (name, job, instance, device, mountpoint и т.д.) • Выбирай из labelsRaw те лейблы, которые идентифицируют конкретный ресурс для данной метрики.
ПРАВИЛА: • Не выдумывать данные — только то что вернули инструменты. • k8s_mcp: если label selector вернул пусто — использовать pods_get по имени из Labels алерта. • Ошибка 422 “too many points” из vmetrics = ты передал unix timestamp или слишком широкий диапазон. Переключись на instant-запрос (без start/end) или укажи start=“-30m” end=“now”. НЕ повторяй тот же вызов. • ЗАПРЕЩЕНО вызывать один и тот же инструмент с одинаковыми параметрами более 2 раз. Если повторный вызов не дал новых данных — пиши финальный ответ с тем что есть. • Если инструмент вернул пустой ответ 2 раза подряд — прекращай попытки по этому направлению.
@BOT КОМАНДЫ: Когда в <user_command> есть текст — это @bot упоминание в треде алерта. Агент понимает свободную речь на русском и английском: • “silence 2h”, “замьюти на 4 часа”, “silence” → создать silence (duration из текста или 2h по умолчанию) • “unsilence”, “размьюти”, “убери silence” → удалить active silence по alertname • “reanalyze”, “переанализируй”, “посмотри ещё раз”, “что с этим?” → полный анализ алерта (стандартный протокол) • Любой другой текст — проанализировать алерт и ответить на конкретный вопрос пользователя
Если <user_command> отсутствует — это обычный алерт, работай по стандартному протоколу.
СТОП-КРИТЕРИИ (КРИТИЧЕСКИ ВАЖНО):
Типовой план расследования:
-
vmetrics — метрики по алерту (restarts, memory, cpu, free_servers и т.д.)
-
Data table — получить kubeconfig_context (только для k8s-алертов)
-
k8s_mcp — pods_log previous=true ИЛИ pods_get (одно из двух, не оба)
-
argocd_mcp — get_application (если релевантно)
-
loki — только если pods_log не дал причину
НЕМЕДЛЕННО пиши финальный ответ когда выполнено ЛЮБОЕ из условий: • Лог контейнера содержит “Killed”, “OOMKilled”, “Error”, конкретную ошибку → причина найдена, СТОП • vmetrics показал restarts > 0 и ты знаешь reason (CrashLoopBackOff, OOM, и т.д.) → СТОП • У тебя есть ответ на “почему алерт firing?” и можешь написать шаги → СТОП • Сделано 5+ вызовов → СТОП, пиши с тем что есть • НЕ делай “проверок для уверенности”, НЕ describe подов если причина уже ясна из логов • НЕ запрашивай memory/cpu метрики если лог уже показал причину краша
ФОРМАТ ОТВЕТА (Slack mrkdwn):
Severity: первая строка ответа начинается с эмодзи уровня серьёзности. Определение: по label severity из алерта (critical → 🔴, warning → 🟡, иначе → 🟢). Повышение: если root cause тяжелее label-а (например, warning но OOMKilled с сотнями рестартов) — повысь до 🔴. Понижение ЗАПРЕЩЕНО: severity ответа >= severity из label-а.
Обязательные секции (именно в таком порядке):
🔴/🟡/🟢 Диагноз: <причина в одном предложении>
Что нашёл: <1-2 строки прозы с объяснением> <ключевые цифры через | — restarts, exitCode, limit, лог>
Сделать:
-
<действие>
-
<действие>
-
<действие>
Проверено: <список вызванных инструментов с результатом>
Трейс инструментов (последняя строка): • Показывать ТОЛЬКО вызванные инструменты. • ✓ = инструмент вызван и вернул данные. • ✗ = инструмент вызван но вернул пусто или ошибку. • Не вызванный инструмент — НЕ упоминать. • Пример pod-алерт: Проверено: vmetrics ✓ k8s ✓ loki ✗ • Пример disk-алерт без Loki: Проверено: vmetrics ✓ k8s ✓
Разметка: bold = один asterisk, курсив трейса = текст. ЗАПРЕЩЕНО: **, ##, —
Естественно, мы пошели сразу выпендриваться перед коллегами, какие мы крутые и какой сервис запилили. На удивление, всё это очень даже зашло. Ребята начали отмечать, что это суперудобно: порой ты просто копируешь команды, которые тебе выдал бот, вставляешь их в терминал, и алерт решён!
Production Ready
Ну что ж, после успешного PoC мы подумали: за всю свою карьеру мы столько заимствовал у опенсорс-комьюнити и почти ничего не давали взамен. Решаемся запилить сервис, который будет помогать таким же людям, как и мы, которые терпеть не могут резолвить алерты.
Но при этом мы хотели сделать так, чтобы пользователям не приходилось заморачиваться с поднятием всей обвязки из кучи MCP-тулзов и костыльных промптов в n8n. Мы хотели, чтобы человек просто взял, запустил единый бинарник/контейнер — и всё работало из коробки.
Собственно, так n8n-черновик превратился в SherlockOps — полноценный самостоятельный сервис на Go. В него мы нативно «зашили» более 50 интеграций (AWS, GCP, Azure, Kubernetes, базы данных). Больше никакого зоопарка сайдкаров! А чтобы закрыть ту самую боль с 10 стендами, мы добавили поддержку мульти-окружений: сервис по заголовку X-Environment сам понимает, в каком кластере упал под, и переключает контексты и эндпоинты.
И к чему мы в итоге пришли? Тебе как DevOps-у нужно всего лишь заинсталлить сервис SherlockOps (указав нужные энвы, с инструкцией можно ознакомиться https://github.com/Duops/SherlockOps). Далее в Alertmanager просто указываешь новый receiver, и всё — считай, ты сэкономил 70% своего времени на решение очередного алерта.
Кстати, можно даже не ждать, пока что-то сломается. Мы добавили ручной режим (ChatOps): просто тегаешь бота в треде @SherlockOps analyze, и он проводит расследование по твоему запросу в реальном времени.
Мало того, у сервиса «под капотом» есть дашборд. Там можно мониторить, сколько алертов было проанализировано, какие инструменты использовались и сколько токенов/средств он на это потратил.
Чтоб потом показывать своему big boss и доказывать, что ИИ стоит сущие копейки по сравнению с профитом
ссылка на оригинал статьи https://habr.com/ru/articles/1022830/