собрать incident tooling в один аудируемый Slack-workflow
ChatOps при инцидентах: от алерта Alertmanager до решения в Slack
12 минут
On-call до сих пор прыгает между PagerDuty, Grafana, kubectl и wiki, пока горят минуты. В материале — как связать Prometheus Alertmanager со Slack-ботом: обогащение алертов, runbook-действия и remediation с RBAC.
Почему фрагментированный incident response расширяет blast radius
Когда срабатывает production-алерт, типичный путь on-call проходит через acknowledge в PagerDuty, dashboard'ы Grafana, runbook в Confluence, локальный терминал с kubectl и тред в Slack для координации. Каждый шаг — context switch под давлением. Диагностика живёт как tribal knowledge, и когда основной респондер недоступен, MTTR растёт. Узкое место редко в мониторинге. Узкое место — в workflow.
Архитектура ChatOps: ingest, orchestrate, execute
ChatOps сворачивает диагностику и координацию в чат, который команда и так использует во время инцидента. Для production-пилота достаточно трёх слоёв. Ingest отправляет webhook payload'ы Prometheus Alertmanager в бот при срабатывании правил. Orchestration разбирает labels и annotations, запрашивает enrichment из Prometheus или Grafana и публикует Block Kit-сообщения со ссылками на runbook и кнопками действий. Execution запускает утверждённые remediation-команды через sandboxed executor с Kubernetes RBAC, проверкой подписи Slack interactivity и структурированным audit log. Тот же паттерн работает в Microsoft Teams с другими SDK; ниже Slack, потому что большинство команд сначала стандартизирует incident-каналы именно там.
Подключите Alertmanager к Slack-боту с enrichment
Маршрутизируйте critical-алерты в отдельный webhook receiver, не дублируя каналы уведомлений. Grouping keys выравнивайте с тем, как думают респондеры — alertname плюс namespace обычно достаточно. Добавьте annotation promql_query в alert rules, чтобы бот мог запросить live-значение; никогда не передавайте alertname как PromQL-выражение. Зарегистрируйте interactivity URL бота в настройках Slack app, чтобы клики по кнопкам попадали на signed endpoint.
route:
receiver: chatops-bot
group_by: ['alertname', 'namespace']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity="critical"
receiver: chatops-bot
continue: true
receivers:
- name: chatops-bot
webhook_configs:
- url: 'https://chatops.example.com/api/alertmanager'
send_resolved: trueimport os
import logging
from flask import Flask, request
from slack_bolt import App
from slack_bolt.adapter.flask import SlackRequestHandler
from slack_sdk import WebClient
import requests
logger = logging.getLogger(__name__)
flask_app = Flask(__name__)
slack_app = App(
token=os.environ["SLACK_BOT_TOKEN"],
signing_secret=os.environ["SLACK_SIGNING_SECRET"],
)
handler = SlackRequestHandler(slack_app)
client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
SLACK_CHANNEL = os.environ.get("SLACK_CHANNEL", "C0123456789")
PROMETHEUS_URL = os.environ.get("PROMETHEUS_URL", "http://prometheus:9090")
def enrich_alert(alert):
annotations = alert.get("annotations", {})
query = annotations.get("promql_query")
if not query:
return "No promql_query annotation on alert rule."
try:
resp = requests.get(
f"{PROMETHEUS_URL}/api/v1/query",
params={"query": query},
timeout=10,
)
data = resp.json()
if data.get("status") == "success" and data["data"]["result"]:
return f"Current value: {data['data']['result'][0]['value'][1]}"
except Exception as exc:
logger.warning("Prometheus query failed: %s", exc)
return "Unable to fetch current metrics."
def build_incident_blocks(alert):
labels = alert.get("labels", {})
annotations = alert.get("annotations", {})
deployment = labels.get("deployment", labels.get("service", "unknown"))
namespace = labels.get("namespace", "default")
action_value = f"{namespace}/{deployment}"
return [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"Incident: {labels.get('alertname', 'unknown')}",
},
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"*Severity:* {labels.get('severity', 'n/a')}"},
{"type": "mrkdwn", "text": f"*Namespace:* {namespace}"},
{"type": "mrkdwn", "text": f"*Deployment:* {deployment}"},
{"type": "mrkdwn", "text": f"*Started:* {alert.get('startsAt', 'unknown')}"},
],
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": annotations.get("description", "No description"),
},
},
{
"type": "section",
"text": {"type": "mrkdwn", "text": f"*Context:*\n```{enrich_alert(alert)}```"},
},
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {"type": "plain_text", "text": "Runbook"},
"url": annotations.get("runbook_url", "https://runbooks.example.com"),
},
{
"type": "button",
"text": {"type": "plain_text", "text": "Restart deployment"},
"action_id": "restart_deployment",
"value": action_value,
"style": "danger",
"confirm": {
"title": {"type": "plain_text", "text": "Confirm restart"},
"text": {
"type": "plain_text",
"text": f"Restart {deployment} in {namespace}?",
},
"confirm": {"type": "plain_text", "text": "Restart"},
"deny": {"type": "plain_text", "text": "Cancel"},
},
},
],
},
]
@flask_app.post("/api/alertmanager")
def receive_alert():
for alert in request.json.get("alerts", []):
client.chat_postMessage(
channel=SLACK_CHANNEL,
blocks=build_incident_blocks(alert),
text=f"Incident: {alert.get('labels', {}).get('alertname', 'unknown')}",
)
return {"status": "ok"}, 200
@slack_app.action("restart_deployment")
def restart_deployment(ack, body, client):
ack()
namespace, deployment = body["actions"][0]["value"].split("/", 1)
user = body["user"]["id"]
# Call an internal executor service with RBAC instead of shelling out from the bot process.
result = requests.post(
"http://remediation-executor/remediate",
json={"action": "rollout_restart", "namespace": namespace, "deployment": deployment, "requested_by": user},
timeout=30,
)
client.chat_postEphemeral(
channel=body["channel"]["id"],
user=user,
text=f"Restart result: {result.text}",
)
@flask_app.post("/slack/events")
def slack_events():
return handler.handle(request)
@flask_app.get("/health")
def health():
return {"status": "ok"}, 200Деплой бота в Kubernetes с least-privilege RBAC
Запускайте бота в отдельном monitoring namespace с секретами для Slack bot token и signing secret. Пробуйте /health для probe и терминируйте TLS на ingress. Привязывайте Role, который разрешает только get и patch на Deployments в утверждённых namespace — никогда не выдавайте cluster-admin автоматизации, которую могут триггерить пользователи Slack. Отдельный remediation executor может держать kubectl credentials, чтобы публичный процесс бота оставался тонким.
apiVersion: apps/v1
kind: Deployment
metadata:
name: chatops-bot
namespace: monitoring
spec:
replicas: 2
selector:
matchLabels:
app: chatops-bot
template:
metadata:
labels:
app: chatops-bot
spec:
serviceAccountName: chatops-bot
containers:
- name: bot
image: registry.example.com/chatops-bot:latest
ports:
- containerPort: 8080
envFrom:
- secretRef:
name: chatops-secrets
env:
- name: PROMETHEUS_URL
value: http://prometheus.monitoring:9090
readinessProbe:
httpGet:
path: /health
port: 8080
livenessProbe:
httpGet:
path: /health
port: 8080
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: chatops-bot
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list"]Операционные практики: безопасный и аудируемый ChatOps
Внедряйте поэтапно: первая неделя — read-only enrichment и ссылки на runbook; затем low-risk действия вроде acknowledge или deep link на логи; destructive remediation — в последнюю очередь. Для production-мутаций требуйте второго approver — публикуйте confirmation thread и выполняйте только после одобрения другим on-call в канале, а не только через Slack confirm dialog одного пользователя. Логируйте каждое действие в Loki, Elasticsearch или incident database с timestamp, user, alert name, target и result, чтобы post-incident review не зависел от retention Slack. Держите каждый инцидент в отдельном треде: при связанном алерте отвечайте в существующий тред, а не спамьте канал. ChatOps не исправит слабые алерты или отсутствие runbook'ов, но убирает фрагментацию инструментов — респондеры тратят время на диагностику и восстановление, а не на поиск вкладок.
ChatOps работает только при надёжном контексте алертов — сначала задайте сигналы и dashboard'ы по базовому гайду по observability для небольших платформенных команд.
Свяжите срочность remediation с политикой надёжности через практики SLO, SLI и error budget для платформенных команд.
