Prometheus Alertmanagerをマスターする:ルーティング、グルーピング、抑制、TelegramとPagerDutyへのスマートアラート配信

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

誰も教えてくれないアラートノイズ問題

Prometheusをセットアップして、いくつかのルールを設定すると、最初はうまくいきます。しかし、深夜3時に些細なネットワーク障害が発生した途端、200件のアラートが一斉に発火します。スマートフォンが爆発寸前になります。眠れるようにするためにすべてをサイレントにします。翌朝、本当のインシデントがノイズに埋もれていて見落としていたことに気づきます。

これは本番環境で最もよく見られるAlertmanagerの失敗パターンです。監視スタックは技術的には正しいのですが、アラートの設定が後回しにされています。単一のレシーバーとルーティングロジックなしの素のalertmanager.ymlは、1週間以内にオンコール体制を崩壊させます。

解決策はアラートを減らすことではなく、より賢く届けることです。Alertmanagerには3つのコアツールがあります:ルーティンググルーピング抑制。これらを組み合わせることで、生のアラート量を、正しい理由で正しい担当者を起こす、アクション可能でコンテキスト豊富な通知に変換できます。

アプローチの比較:シンプル設定 vs 構造化Alertmanager

設定に手を付ける前に、実際に何を選ぶのかを把握しておくと役立ちます。

シンプルな設定 — 単一レシーバー、ロジックなし

route:
  receiver: 'slack-default'
receivers:
  - name: 'slack-default'
    slack_configs:
      - api_url: 'https://hooks.slack.com/...'

すべてのアラートを1つのチャンネルに送ります。設定は5分で完了します。ただし、忙しい週が来た途端に破綻します。

構造化設定 — グループ配信を持つルーティングツリー

route:
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  receiver: 'default'
  routes:
    - match:
        severity: critical
      receiver: 'pagerduty-critical'
    - match:
        team: platform
      receiver: 'telegram-platform'
inhibit_rules:
  - source_match:
      severity: critical
    target_match:
      severity: warning
    equal: ['alertname', 'cluster']

アラートはグループ化・重複排除され、ラベルでルーティングされます。クリティカルアラートが発火している場合は警告ノイズが抑制されます。これが本番環境の姿です。

各アプローチのメリット・デメリット

  • シンプル設定 — メリット:設定の手間ゼロ。デメリット:アラートストーム、優先度の分離なし、数日でオンコール疲弊。
  • 構造化設定 — メリット:ノイズ削減、チームベースのルーティング、明確なエスカレーションパス。デメリット:事前計画とアラートルール全体でのラベル規律が必要。

ルーティングの設定ミスは単なる迷惑にとどまらず、信頼を損ないます。エンジニアがアラートはノイズが多いと学習すると、確認しなくなります。その時点で、監視スタックは安全網ではなく見かけだけのものになってしまいます。

推奨設定:ラベルファーストの設計

Alertmanagerはラベルでルーティングします。それらのラベルはPrometheusのアラートルールから来ます — Alertmanagerの設定に手を付ける前に、まずラベルを正しく設定しましょう。

# Prometheusルールファイル内
groups:
  - name: app.rules
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
        for: 2m
        labels:
          severity: critical
          team: backend
          service: api
        annotations:
          summary: "{{ $labels.service }}のエラー率が高い"
          description: "エラー率は{{ $value | humanizePercentage }}です"

severityteamserviceをすべてのルールに一貫して設定することで、ルーティングツリーが予測可能でメンテナンスしやすくなります。これらのラベルを省略すると、ルーティングは当て推量になります。

実装ガイド

ステップ1 — Alertmanagerのインストールと起動

# ダウンロード
wget https://github.com/prometheus/alertmanager/releases/download/v0.27.0/alertmanager-0.27.0.linux-amd64.tar.gz
tar xvf alertmanager-0.27.0.linux-amd64.tar.gz
cd alertmanager-0.27.0.linux-amd64

# 設定ファイルで起動
./alertmanager --config.file=alertmanager.yml --storage.path=/var/lib/alertmanager

次に、prometheus.ymlに以下を追加してPrometheusにAlertmanagerの場所を教えます:

alerting:
  alertmanagers:
    - static_configs:
        - targets: ['localhost:9093']

ステップ2 — ルーティングの設定

ルーティングツリーは上から下にルートを評価し、最初のマッチで停止します。複数のレシーバーに配信するには、ルートにcontinue: trueを設定します。最も具体的なマッチャーを上部に置きましょう。

route:
  receiver: 'default-receiver'
  group_by: ['alertname', 'cluster']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 12h

  routes:
    # クリティカルアラートは即時エスカレーションのためPagerDutyへ
    - match:
        severity: critical
      receiver: 'pagerduty'

    # プラットフォームチームはTelegram通知を受信
    - match:
        team: platform
      receiver: 'telegram-platform'

    # データベースアラートは分離して管理
    - match_re:
        service: 'postgres|mysql|redis'
      receiver: 'telegram-db'

ステップ3 — グルーピングの設定

グルーピングは深夜3時のアラートストームを防ぐ機能です。ノードがダウンして50件のアラートが同時に発火した場合、Alertmanagerは50件の個別ページの代わりに1件の通知にまとめます。

この動作を制御する3つの設定があります:

  • group_wait: 30s — 新しいグループの最初の通知を送信する前にバッファリングする時間。関連するアラートがまとめて到着するための時間を確保します。デフォルトの30秒は実際によく機能します。
  • group_interval: 5m — 既存のグループに新しいアラートが追加された場合、フォローアップを送信するまでの待機時間。ほとんどのチームにとって5分が合理的な出発点です。
  • repeat_interval: 12h — 既に発火中のアラートについて再通知する頻度。クリティカルには1h、警告には4〜12hを使用します。警告に1分のリピートを設定すると、誰かが修正するまで1分ごとに通知されます — これは避けましょう。

ステップ4 — 抑制ルールの設定

抑制は、同じシステムに対してより重大度の高いアラートがすでに発火しているときに、低優先度のアラートをサイレントにします。Alertmanagerが持つ最大のノイズ削減手段です。

inhibit_rules:
  # 同じサービスでクリティカルがアクティブな場合、警告を抑制
  - source_match:
      severity: 'critical'
    target_match:
      severity: 'warning'
    equal: ['alertname', 'cluster', 'service']

  # ノード全体がダウンしているとき、すべてのアプリアラートを抑制
  - source_match:
      alertname: 'NodeDown'
    target_match_re:
      alertname: '.*'
    equal: ['instance']

2番目のルールは、誰もが後から「もっと早く追加しておけばよかった」と思うものです。あるホストでNodeDownが発火すると、そのホストから発生するすべてのアラート(ディスクフル、高CPU、サービスダウン)が自動的に抑制されます。ノードを修正すれば、すべてクリアになります。手動でサイレント設定する必要はありません。

ステップ5 — Telegramとの統合

まずTelegramで@BotFatherを通じてボットを作成し、トークンを取得します。次にチャットIDを確認します:ボットにメッセージを送り、https://api.telegram.org/bot<TOKEN>/getUpdatesにアクセスして、JSONレスポンスのchat.idフィールドを探します。グループIDは負の値です。

receivers:
  - name: 'telegram-platform'
    telegram_configs:
      - bot_token: 'YOUR_BOT_TOKEN'
        chat_id: -1001234567890
        message: |
          {{ range .Alerts }}
          *[{{ .Status | toUpper }}]* {{ .Labels.alertname }}
          サービス: {{ .Labels.service }}
          {{ .Annotations.summary }}
          {{ end }}
        parse_mode: 'Markdown'

messageフィールドはGoテンプレートを使用します。簡潔に保ちましょう — Telegramは4096文字を超えるメッセージを切り捨て、密度の高いアラートダンプはモバイルでは読みにくいです。重大度とサービス名を含む1行/アラートで、対応するのに十分なコンテキストが得られます。

ステップ6 — PagerDutyとの統合

PagerDutyはクリティカルなオンコールエスカレーションを担当します。PagerDutyでサービス → 「Integrations」タブ → 「Add an integration」 → Prometheusを選択します。取得したルーティングキーをコピーします。

receivers:
  - name: 'pagerduty'
    pagerduty_configs:
      - routing_key: 'YOUR_PAGERDUTY_ROUTING_KEY'
        description: '{{ .GroupLabels.alertname }} — {{ .GroupLabels.cluster }}'
        severity: '{{ if eq .CommonLabels.severity "critical" }}critical{{ else }}warning{{ end }}'
        details:
          firing: '{{ template "pagerduty.default.instances" .Alerts.Firing }}'
          resolved: '{{ template "pagerduty.default.instances" .Alerts.Resolved }}'

PagerDutyはグループラベルから導出されるdedup_keyを使って重複排除します。発火中のアラートと解決状態が自動的にリンクされます — 問題が解決した後も未解決インシデントが残ることはありません。

ステップ7 — amtoolで検証

新しいalertmanager.ymlを検証せずにプッシュしてはいけません。バンドルされているamtoolは、ルーティングのミスが本番環境に到達する前に検出します。

# 設定構文の検証
amtool check-config alertmanager.yml

# ルーティングのテスト — アラートがどのレシーバーに届くか確認
amtool config routes test --config.file=alertmanager.yml \
  severity=critical team=backend service=api

# テストアラートを手動で発火
amtool alert add alertname=TestAlert severity=critical service=api \
  --annotation=summary="amtoolからのテストアラート" \
  --alertmanager.url=http://localhost:9093

避けるべきよくある間違い

  • group_byを省略する:すべてのアラートが1つの巨大な通知にまとめられます。少なくともalertnameclusterinstanceなどのトポロジーラベル1つでグループ化しましょう。
  • repeat_intervalを短く設定しすぎる:警告アラートに1分を設定すると、誰かが修正するまで1分ごとにページが届きます。非クリティカルアラートには4〜12時間が適切な範囲です。
  • 抑制ルールでequalを忘れる:これがないと、1台のホストのクリティカルアラートがクラスター内の他のすべてのホストの警告をサイレントにします。常にマッチングラベルで抑制スコープを絞り込みましょう。
  • すべてに1つのレシーバーを使う:データベースのオンコールとアプリケーションのオンコールは、異なるコンテキストを持つ異なる担当者です。最初から別々にルーティングしましょう。

完全なalertmanager.yml

global:
  resolve_timeout: 5m

route:
  receiver: 'default-receiver'
  group_by: ['alertname', 'cluster', 'service']
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 12h
  routes:
    - match:
        severity: critical
      receiver: 'pagerduty'
    - match:
        team: platform
      receiver: 'telegram-platform'
    - match_re:
        service: 'postgres|mysql|redis'
      receiver: 'telegram-db'

inhibit_rules:
  - source_match:
      severity: 'critical'
    target_match:
      severity: 'warning'
    equal: ['alertname', 'cluster', 'service']
  - source_match:
      alertname: 'NodeDown'
    target_match_re:
      alertname: '.*'
    equal: ['instance']

receivers:
  - name: 'default-receiver'
    telegram_configs:
      - bot_token: 'YOUR_BOT_TOKEN'
        chat_id: -1001234567890
        message: '[{{ .Status | toUpper }}] {{ .GroupLabels.alertname }}'

  - name: 'telegram-platform'
    telegram_configs:
      - bot_token: 'YOUR_BOT_TOKEN'
        chat_id: -1009876543210
        message: |
          {{ range .Alerts }}
          *{{ .Status | toUpper }}* — {{ .Labels.alertname }}
          {{ .Annotations.summary }}
          {{ end }}
        parse_mode: 'Markdown'

  - name: 'telegram-db'
    telegram_configs:
      - bot_token: 'YOUR_BOT_TOKEN'
        chat_id: -1001111111111
        message: '[DB] {{ .GroupLabels.alertname }}({{ .GroupLabels.instance }})'

  - name: 'pagerduty'
    pagerduty_configs:
      - routing_key: 'YOUR_PAGERDUTY_ROUTING_KEY'
        description: '{{ .GroupLabels.alertname }} — {{ .GroupLabels.cluster }}'

本当のテストは、これが本番稼働した後の最初のインシデントです。90秒以内に200件の個別通知が届く代わりに、1件のグループ化されたメッセージが届きます:アラート14件発火中、cluster=prod、severity=critical。担当者だけが確認します。他の全員はそのまま眠り続けます。

Share: