LabHub

ブログ

Prometheus・Alertmanagerアラートパイプライン構築:ルール作成からPagerDuty・Slackルーティングまで

한국어English日本語

Prometheus Alertmanagerパイプライン

はじめに

モニタリングシステムを構築してダッシュボードを作ったからといって、オブザーバビリティが完成するわけではない。ダッシュボードは人が能動的に確認しなければならないが、アラートはシステムが異常状態を検知したときに適切な人へ適時に通知する。真のオブザーバビリティはメトリクス収集から始まり、アラートパイプラインで完成する。

Google SRE出身のRob Ewaschukは「My Philosophy on Alerting」で2つの中心的な原則を提示している。第一に、原因(Cause)ではなく症状(Symptom)にアラートを設定せよ。ディスク使用率80%のような原因ベースのアラートより、「ユーザーリクエストのエラー率が1%を超える」のような症状ベースのアラートのほうが、実際のユーザー影響をより正確に反映する。第二に、すべてのアラートは即座の行動を引き起こさなければならない。アラートを受け取っても「あとで見ればいい」なら、そのアラートは存在する理由がない。

この記事では、Prometheusのアラートルール(Alerting Rules)作成、Alertmanagerのルーティングツリー設計、PagerDutyとSlackの統合、Alert Fatigue防止戦略まで、プロダクションのアラートパイプライン構築の全過程を扱う。


Prometheusアラートルールの作成

alerting rulesの構造

Prometheusのアラートルールは5つの主要フィールドで構成される。

pending vs firing の状態遷移

アラートは3つの状態を循環する。inactiveは条件が満たされていないデフォルト状態だ。expr条件が初めて満たされるとpending状態に入り、forで指定した時間だけ継続して条件が満たされるとfiring状態へ遷移してAlertmanagerへ送信される。for期間の途中で条件が解消されると再びinactiveに戻る。

keep_firing_forの活用

Prometheus 2.42から導入された keep_firing_for フィールドは、間欠的なメトリクス(intermittent metric)に有用だ。例えばバッチジョブのメトリクスがジョブ完了後に消えるとアラートも即座に解消されるが、keep_firing_for を設定するとメトリクスが消えたあとも指定した時間だけfiring状態を維持し、オンコールエンジニアが確認する時間を確保できる。

基本のインフラアラートルール

CPU、メモリ、ディスクに対する基本のアラートルールだ。症状ベースのアラートが優先だが、インフラ資源の枯渇は依然として監視すべき主要な原因指標だ。

groups:
  - name: infrastructure-alerts
    interval: 30s
    rules:
      # CPU使用率が5分間80%以上継続
      - alert: HighCpuUsage
        expr: |
          100 - (avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
        for: 5m
        labels:
          severity: warning
          team: infra
        annotations:
          summary: 'CPU使用率が高い'
          description: 'インスタンス {{ $labels.instance }} のCPU使用率が {{ $value | printf "%.1f" }}% です。'
          runbook_url: 'https://wiki.internal/runbook/high-cpu'

      # メモリ使用率が90%を超過
      - alert: HighMemoryUsage
        expr: |
          (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
        for: 5m
        labels:
          severity: warning
          team: infra
        annotations:
          summary: 'メモリ使用率が高い'
          description: 'インスタンス {{ $labels.instance }} のメモリ使用率が {{ $value | printf "%.1f" }}% です。'

      # ディスクが24時間以内に枯渇する予測
      - alert: DiskWillFillIn24Hours
        expr: |
          predict_linear(node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"}[6h], 24*3600) < 0
        for: 10m
        labels:
          severity: critical
          team: infra
        annotations:
          summary: 'ディスクが24時間以内に枯渇する予測'
          description: 'インスタンス {{ $labels.instance }} のマウントポイント {{ $labels.mountpoint }} が24時間以内に枯渇すると予測されます。'
          runbook_url: 'https://wiki.internal/runbook/disk-full'

SLOベースのburn rateアラートルール

Google SRE Workbookが提案するMulti-Window Multi-Burn-Rateのアラート方式だ。エラーバジェットの消費速度(burn rate)を基準に、速い消費と遅い消費をそれぞれ検知して対応の緊急度を差別化する。

groups:
  - name: slo-burn-rate-alerts
    rules:
      # エラー率のRecording Rule(事前計算)
      - record: slo:http_error_rate:ratio_rate5m
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[5m])) by (service)
          /
          sum(rate(http_requests_total[5m])) by (service)

      - record: slo:http_error_rate:ratio_rate30m
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[30m])) by (service)
          /
          sum(rate(http_requests_total[30m])) by (service)

      - record: slo:http_error_rate:ratio_rate1h
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[1h])) by (service)
          /
          sum(rate(http_requests_total[1h])) by (service)

      - record: slo:http_error_rate:ratio_rate6h
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[6h])) by (service)
          /
          sum(rate(http_requests_total[6h])) by (service)

      # SLO 99.9%(エラーバジェット 0.1%)
      # 速い消費: 5m/30mウィンドウ、burn rate 14.4x → 1時間以内にバジェットを消費する速度
      - alert: SloHighBurnRate
        expr: |
          slo:http_error_rate:ratio_rate5m{service="api-gateway"} > (14.4 * 0.001)
          and
          slo:http_error_rate:ratio_rate30m{service="api-gateway"} > (14.4 * 0.001)
        for: 2m
        labels:
          severity: critical
          slo: 'availability'
          window: 'fast'
        annotations:
          summary: 'SLOの速い消費を検知 - 即時対応が必要'
          description: 'api-gatewayのエラーバジェットが1時間以内に消費される速度で減少しています。'

      # 遅い消費: 1h/6hウィンドウ、burn rate 6x → 4時間以内にバジェットを消費する速度
      - alert: SloMediumBurnRate
        expr: |
          slo:http_error_rate:ratio_rate1h{service="api-gateway"} > (6 * 0.001)
          and
          slo:http_error_rate:ratio_rate6h{service="api-gateway"} > (6 * 0.001)
        for: 5m
        labels:
          severity: warning
          slo: 'availability'
          window: 'slow'
        annotations:
          summary: 'SLOの遅い消費を検知 - 調査が必要'
          description: 'api-gatewayのエラーバジェットが4時間以内に消費される速度で減少しています。'

上記のルールで中心となるのは2つのウィンドウをAND条件で結合することだ。短いウィンドウ(5m)だけを見ると一時的なスパイクに反応し、長いウィンドウ(30m)だけを見ると検知が遅くなる。2つのウィンドウが同時に条件を満たすときだけ発火させて精度を高める。


アラートルールのテスト

promtoolを活用した単体テスト

アラートルールをプロダクションへデプロイする前に必ずテストしなければならない。promtoolは仮想の時系列データを入力し、特定の時点でアラートが期待どおり発火するかを検証する。

# alert_test.yaml
rule_files:
  - infrastructure_alerts.yaml

evaluation_interval: 1m

tests:
  # テスト1: CPU 80%以上が5分継続したらfiring
  - interval: 1m
    input_series:
      - series: 'node_cpu_seconds_total{mode="idle",instance="node1:9100",cpu="0"}'
        values: '0+0.15x20' # 毎分0.15秒のidle = 使用率85%
      - series: 'node_cpu_seconds_total{mode="idle",instance="node1:9100",cpu="1"}'
        values: '0+0.15x20'
    alert_rule_test:
      # 4分時点: pending状態であるべき (for: 5m 未充足)
      - eval_time: 4m
        alertname: HighCpuUsage
        exp_alerts: []
      # 6分時点: firing状態であるべき
      - eval_time: 6m
        alertname: HighCpuUsage
        exp_alerts:
          - exp_labels:
              severity: warning
              team: infra
              instance: 'node1:9100'
            exp_annotations:
              summary: 'CPU使用率が高い'

  # テスト2: メモリが90%未満ならアラートなし
  - interval: 1m
    input_series:
      - series: 'node_memory_MemAvailable_bytes{instance="node1:9100"}'
        values: '2147483648x10' # 2GB available
      - series: 'node_memory_MemTotal_bytes{instance="node1:9100"}'
        values: '8589934592x10' # 8GB total = 使用率75%
    alert_rule_test:
      - eval_time: 10m
        alertname: HighMemoryUsage
        exp_alerts: []

CI/CDでのアラートルール検証の自動化

アラートルールファイルをGitで管理し、CIパイプラインで自動検証する。構文エラー、PromQL式のエラー、テスト失敗をデプロイ前に捕捉できる。

# 構文検証
promtool check rules infrastructure_alerts.yaml
promtool check rules slo_alerts.yaml

# 単体テストの実行
promtool test rules alert_test.yaml

# Alertmanager設定の検証
amtool check-config alertmanager.yaml

# CIパイプラインの例 (GitHub Actions)
# jobs:
#   validate-alerts:
#     steps:
#       - name: Check alert rules syntax
#         run: promtool check rules rules/*.yaml
#       - name: Run alert rule tests
#         run: promtool test rules tests/*.yaml
#       - name: Check Alertmanager config
#         run: amtool check-config alertmanager.yaml

Alertmanager設定の詳細

ルーティングツリー(routing tree)の設計

Alertmanagerのルーティングはツリー構造で動作する。最上位のrouteはすべてのアラートのデフォルト受信者を定義し、下位のrouteがラベルマッチングを通じてアラートを適切なチャネルへ分岐させる。continue: true を設定するとマッチしたあとも同一レベルの次のルーティングルールを評価し続けるため、1つのアラートを複数の受信者へ届けられる。

group_by は同じラベル値を持つアラートを1つのグループにまとめる。例えば group_by: [alertname, cluster] と設定すると、同じアラート名で同じクラスタから発生したアラートが1つのアラートメッセージにまとめて配信される。これにより100個のPodで同時に発生したOOMKilledアラートが、100個の個別メッセージではなく1つのグループメッセージとして届く。

# alertmanager.yaml
global:
  resolve_timeout: 5m
  smtp_smarthost: 'smtp.internal:587'
  smtp_from: 'alertmanager@company.com'

route:
  receiver: default-slack
  group_by: [alertname, cluster, namespace]
  group_wait: 30s # 最初のアラート受信後、グループ化のための待機
  group_interval: 5m # 既存グループに新しいアラートが追加されたときの再送間隔
  repeat_interval: 4h # 同一アラートの繰り返し送信間隔

  routes:
    # Criticalアラート → PagerDuty (即時ページング)
    - match:
        severity: critical
      receiver: pagerduty-critical
      group_wait: 10s
      repeat_interval: 1h
      continue: true # Slackにも同時送信

    # Criticalアラート → Slack criticalチャネル (同時送信)
    - match:
        severity: critical
      receiver: slack-critical

    # SLO関連アラート → SREチーム専用チャネル
    - match_re:
        alertname: '^Slo.*'
      receiver: slack-sre-slo
      group_by: [alertname, service]

    # インフラチームのアラート
    - match:
        team: infra
      receiver: slack-infra
      group_by: [alertname, instance]

    # バックエンドチームのアラート
    - match:
        team: backend
      receiver: slack-backend
      group_by: [alertname, service, namespace]

    # Watchdogアラート (Alertmanagerの正常動作確認用)
    - match:
        alertname: Watchdog
      receiver: 'null'
      repeat_interval: 24h

Inhibition Rulesによる下位アラートの抑制

Inhibition Rulesは特定のアラートが発火している間、関連する下位アラートを抑制する。例えばノード自体がダウンすれば、そのノードのすべてのPodアラートは不要だ。インフラレベルの障害が発生したときに、それによって連鎖的に発生する数十個の下位アラートを抑制し、アラートの氾濫を防ぐ。

# alertmanager.yaml (inhibition section)
inhibit_rules:
  # ノードダウン時に該当ノードのすべての下位アラートを抑制
  - source_matchers:
      - alertname = NodeDown
    target_matchers:
      - severity =~ "warning|info"
    equal: [instance]

  # クラスタレベルの障害時に個別サービスのアラートを抑制
  - source_matchers:
      - alertname = ClusterUnreachable
    target_matchers:
      - severity =~ "warning|critical"
    equal: [cluster]

  # Criticalがfiring中なら同一アラートのWarningを抑制
  - source_matchers:
      - severity = critical
    target_matchers:
      - severity = warning
    equal: [alertname, cluster, namespace]

  # 大規模障害時にSLOアラートを抑制 (インフラレベルの問題であるため)
  - source_matchers:
      - alertname =~ "NodeDown|ClusterUnreachable"
    target_matchers:
      - alertname =~ "^Slo.*"
    equal: [cluster]

Silencesによるメンテナンス時のアラート停止

計画的なメンテナンス時には関連アラートを一時的に停止できる。amtool CLIまたはAlertmanager UIからSilenceを作成する。Silenceはラベルマッチャで適用範囲を指定し、有効期限を過ぎると自動的に解除される。

# amtoolでSilenceを作成 (2時間、特定クラスタのアラートを停止)
amtool silence add \
  --alertmanager.url=http://alertmanager:9093 \
  --author="sre-team" \
  --comment="Planned maintenance: k8s cluster upgrade" \
  --duration=2h \
  cluster="production-us-east-1"

# 有効なSilenceの一覧確認
amtool silence query --alertmanager.url=http://alertmanager:9093

# Silenceの解除
amtool silence expire --alertmanager.url=http://alertmanager:9093 <silence-id>

PagerDutyとSlackの統合

複数受信者の設定

重大度に応じてアラートを別のチャネルへルーティングすることが中心となる。CriticalアラートはPagerDutyを通じてオンコールエンジニアへ即座にページングし、WarningアラートはSlackチャネルへ届けて勤務時間内に調査させる。

# alertmanager.yaml (receivers section)
receivers:
  # デフォルト受信者: Slack generalチャネル
  - name: default-slack
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/T00/B00/XXXXX'
        channel: '#alerts-general'
        send_resolved: true
        title: '{{ if eq .Status "firing" }}FIRING{{ else }}RESOLVED{{ end }} - {{ .CommonLabels.alertname }}'
        text: >-
          *Severity:* {{ .CommonLabels.severity | toUpper }}
          *Cluster:* {{ .CommonLabels.cluster }}
          *Namespace:* {{ .CommonLabels.namespace }}
          {{ range .Alerts }}
          - *{{ .Labels.instance }}*: {{ .Annotations.description }}
          {{ end }}

  # PagerDuty Critical受信者
  - name: pagerduty-critical
    pagerduty_configs:
      - service_key_file: '/etc/alertmanager/secrets/pagerduty-service-key'
        severity: '{{ .CommonLabels.severity }}'
        description: '{{ .CommonAnnotations.summary }}'
        details:
          firing: '{{ .Alerts.Firing | len }}'
          resolved: '{{ .Alerts.Resolved | len }}'
          cluster: '{{ .CommonLabels.cluster }}'
          namespace: '{{ .CommonLabels.namespace }}'
          runbook_url: '{{ .CommonAnnotations.runbook_url }}'

  # Slack Criticalチャネル (PagerDutyと同時送信)
  - name: slack-critical
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/T00/B00/YYYYY'
        channel: '#alerts-critical'
        send_resolved: true
        color: '{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}'
        title: 'CRITICAL: {{ .CommonLabels.alertname }}'
        text: >-
          *Status:* {{ .Status | toUpper }}
          {{ range .Alerts }}
          - {{ .Annotations.description }}
            Runbook: {{ .Annotations.runbook_url }}
          {{ end }}

  # SRE SLO専用チャネル
  - name: slack-sre-slo
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/T00/B00/ZZZZZ'
        channel: '#sre-slo-alerts'
        send_resolved: true
        title: 'SLO Alert: {{ .CommonLabels.alertname }}'
        text: >-
          *Service:* {{ .CommonLabels.service }}
          *Window:* {{ .CommonLabels.window }}
          {{ range .Alerts }}
          - {{ .Annotations.description }}
          {{ end }}

  # インフラチームのチャネル
  - name: slack-infra
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/T00/B00/INFRA'
        channel: '#team-infra-alerts'
        send_resolved: true

  # バックエンドチームのチャネル
  - name: slack-backend
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/T00/B00/BACKEND'
        channel: '#team-backend-alerts'
        send_resolved: true

  # Null受信者 (Watchdogなど不要なアラートを吸収)
  - name: 'null'

PagerDuty Events API v2を使う場合は routing_keyservice_key_file を適切に選択しなければならない。service_key_file はファイルベースで秘密鍵を注入するため、Kubernetes Secretと連携しやすい。Slack Incoming Webhookはチャネルごとに別々のWebhook URLを作成し、受信者ごとに異なるチャネルへ届ける。


Grafana Alertingとの比較

Grafana 9以降でUnified Alertingが導入され、Grafana自体でもアラートルールを管理できるようになった。2つのシステムの特性を比較し、環境に合った選択をすべきだ。

項目Prometheus AlertmanagerGrafana Unified Alerting
データソースPrometheus専用複数データソース (Loki、Tempo、SQLなど)
ルール管理YAMLファイル (GitOps親和)UIまたはProvisioning API
ルーティングツリーベースの精密ルーティングポリシーベースのルーティング (Notification Policies)
評価エンジンPrometheusサーバ内蔵Grafanaサーバまたは外部Ruler
HA対応Gossipプロトコル内蔵Grafana HAに依存
テストツールpromtool (CLIベース)UIプレビュー、APIベース
成熟度非常に高い (10年以上)成長中 (Grafana 9+以降)
拡張性Thanos/Cortex Ruler連携Mimir Ruler連携
エコシステム豊富なコミュニティルールGrafanaエコシステム統合

ハイブリッドアプローチ戦略

実務では2つのうち1つだけを選ぶより、ハイブリッドなアプローチが効果的だ。

Grafanaから外部Alertmanagerを接続すると、Grafanaの多様なデータソース対応とAlertmanagerの強力なルーティング/グループ化を同時に活用できる。


Alert Fatigue防止戦略

アラート疲れの原因と症状

Alert Fatigue(アラート疲れ)は、過剰なアラートによってオンコールエンジニアの注意が分散し、結局は重要なアラートまで無視するようになる現象だ。PagerDutyの2024 State of Digital Operationsレポートによると、オンコールエンジニアが1日平均10件以上のアラートを受け取ると対応品質が急激に低下する。

アラート疲れの主な原因は以下のとおりだ。

信号対雑音比(SNR)の改善

すべてのアラートは次の質問を通過しなければならない。

  1. このアラートを受け取ったら即座に行動すべきか。
  2. このアラートはユーザーに影響を与える症状を示しているか。
  3. このアラートがなければどんなリスクがあるか。
  4. このアラートに対する明確なRunbookが存在するか。

1つでも「いいえ」なら、そのアラートは削除するかダッシュボードのパネルへ移すべきだ。

アラート優先度体系: P1-P4

優先度定義アラートチャネル対応時間
P1 (Critical)サービス全面障害またはデータ損失のリスクPagerDutyで即時ページング5分以内API全体のダウン、データベース障害
P2 (High)部分障害または性能の深刻な低下PagerDuty + Slack30分以内特定エンドポイントのエラー率急増
P3 (Warning)潜在的な問題、調査が必要Slackチャネル営業日基準4時間ディスク使用率の増加傾向
P4 (Info)参考情報Slackまたはダッシュボードのみ次のスプリント証明書が30日以内に期限切れ

週次アラートレビューのプロセス

毎週チーム単位で過去1週間のアラートをレビューする。

  1. 定量分析: 総アラート数、severity別の分布、チーム別の分布、MTTRの測定
  2. 誤検知の分類: 各アラートが実際に行動を引き起こしたかを確認
  3. しきい値の調整: 誤検知が繰り返されるアラートのしきい値またはfor期間を調整
  4. ルールの整理: 3週連続で無視されたアラートは削除候補
  5. 文書化: 調整内容とその根拠を記録

Cloudflareのアラート可観測性の事例

Cloudflareは「Alerts Observability」という概念を導入し、アラート自体を観察の対象とした。アラートの発火頻度、応答時間、自動解消率、エスカレーション率をダッシュボードで可視化し、それを基にアラート品質を継続的に改善している。アラートシステムに対するメタモニタリングが、Alert Fatigueを体系的に管理する鍵だ。


失敗事例と復旧手順

事例1: group_byの誤設定によるアラートの氾濫

状況: group_byを [alertname] だけに設定した状態で、100ノードのDiskWillFillIn24Hoursアラートが同時に発火した。すべてのノードのアラートが1つのグループにまとめられて単一のアラートしか送信されず、個別ノードの情報が欠落して対応が不可能だった。

原因: group_byinstance ラベルを含めなかったため、すべてのノードのアラートが1つに統合された。

解決: group_by: [alertname, instance] へ変更し、ノードごとに別々のアラートグループが生成されるよう修正した。ただし、group_byにラベルを多く含めすぎるとグループが過度に細分化され、逆の問題(アラートの氾濫)が発生しうるため、適切なバランスが必要だ。

事例2: PagerDutyルーティングの漏れによる障害の未検知

状況: 新しいマイクロサービスをデプロイしたが、そのサービスのアラートに team: payments ラベルが欠落していた。ルーティングルールにマッチせずデフォルト受信者(Slack generalチャネル)にのみ配信され、決済サービスの障害が30分間検知されなかった。

原因: サービスのデプロイ時にアラートルールのラベル検証プロセスがなかった。

解決: CIパイプラインにラベル検証ステップを追加し、すべてのアラートルールに teamseverity ラベルが必ず含まれるよう強制した。amtoolを活用したルーティングシミュレーションをデプロイ前チェックに含めた。

# amtoolでルーティング経路をシミュレーション
amtool config routes test \
  --config.file=alertmanager.yaml \
  --tree \
  severity=critical team=payments alertname=HighErrorRate

# 想定される出力: pagerduty-critical → slack-critical
# team=payments ラベルがない場合: default-slack (警告が発生)

復旧手順のチェックリスト

アラートパイプラインの障害時に段階的に確認する項目だ。

  1. Alertmanagerクラスタの状態確認: amtool cluster show
  2. 現在有効なアラートの確認: Alertmanager UIまたは amtool alert query
  3. Silence状態の確認: 意図しないSilenceが有効になっていないか
  4. Prometheusアラートルールの評価状態: Prometheus UIのAlertsタブでエラーを確認
  5. ネットワーク接続: PrometheusからAlertmanagerへの通信を確認
  6. 受信者の接続: PagerDuty API、Slack Webhookの応答を確認
  7. 設定ファイルの整合性: amtool check-config alertmanager.yaml

運用時の注意事項

アラートルールの命名規約

一貫した命名はルーティングルールの作成とダッシュボードのフィルタリングに不可欠だ。次の規約を推奨する。

パターン説明
対象 + 症状何がどんな状態かHighCpuUsage, DiskWillFillIn24Hours
SLO接頭辞SLO関連アラートの区別SloHighBurnRate, SloLatencyBudgetExhausted
サービス接頭辞サービス固有のアラートApiGatewayHighLatency, PaymentServiceDown

PascalCaseを使い、アラート名だけで何が問題なのかを把握できるようにする。「Alert1」「Check5」のような意味のない名前は禁止する。

ラベルの標準化とガバナンス

すべてのアラートルールに含めるべき必須ラベルを定義する。

ラベル必須かどうか説明許容値
severity必須アラートの重大度critical, warning, info
team必須担当チームinfra, backend, frontend, data, sre
service推奨関連サービスサービスディスカバリの名前と一致
slo任意SLO関連かどうかavailability, latency

ラベル値は必ず列挙型で管理すべきで、自由テキストを許すとルーティングルールが複雑になり管理が不可能になる。OPA(Open Policy Agent)やCIパイプラインでラベルスキーマを検証する方法が効果的だ。

定期的なアラート監査(audit)

四半期ごとに全アラートルールに対する監査を実施する。

監査結果を基にルールを整理し、不要なアラートを削除し、しきい値を調整する。アラートルールはコードと同じく、継続的な保守が必要な生きた資産だ。


参考資料

コメント

まだコメントはありません。

ログインするとコメントできます