RunbookをYAML化してLLMに読ませ、夜間バッチ障害を自動トリアージした話

  • #LLM活用
  • #障害対応自動化
  • #Runbook

Runbook を YAML で構造化して LLM に夜間バッチ失敗を自動トリアージさせる

自分の現場では、Runbook がたいてい Confluence の片隅に眠っている。書いた本人にしか読めない文体、最終更新が 3 年前のスクリーンショット、本番手順と混在したメモ書き。深夜 2 時にバッチが落ちても、誰もそのページを開かない。

この問題を解消するために試したのが、「Runbook を YAML に構造化して LLM に読ませ、エラーログと突き合わせてトリアージ結果を Slack に流す」という仕組みだ。構築して 2 ヶ月が経つが、夜間の一次対応は明らかに減った。

なぜ YAML なのか

自然言語の Runbook をそのまま LLM に渡す方法も試した。GPT-4 はそれなりに読んでくれるが、「どのエラーコードに対してどの手順を適用するか」という条件分岐を正確に拾わせるには、プロンプトエンジニアリングで補完し続ける必要があった。属人化の問題が Runbook から LLM への指示側に移るだけで、本質は変わらない。

YAML にすると、キーと値の構造が LLM への入力として一貫性を持ちやすい。加えて、Runbook 自体がバージョン管理しやすくなるという副産物もある。

スキーマは以下のように設計した。

runbook:
  batch_name: "daily_sales_aggregation"
  description: "日次売上集計バッチ。毎日 01:00 JST に実行。"
  owners:
    primary: "infra-team"
    escalation: "data-platform-lead"
  error_patterns:
    - code: "DB_CONN_TIMEOUT"
      severity: "critical"
      likely_cause: "RDS のコネクション枯渇、またはセキュリティグループの変更"
      actions:
        - "RDS のアクティブコネクション数を CloudWatch で確認する"
        - "直近 1 時間のデプロイ・設定変更履歴を確認する"
        - "改善しない場合は escalation 先に連絡してリトライ判断を仰ぐ"
      auto_retry: false
    - code: "S3_ACCESS_DENIED"
      severity: "high"
      likely_cause: "IAM ロールのポリシー変更、またはバケットポリシーの更新"
      actions:
        - "対象バケットのポリシーと IAM ロールのアタッチ状況を確認する"
        - "CloudTrail で直近の変更イベントを確認する"
      auto_retry: false
  fallback:
    message: "上記パターンに一致しない場合は、ログ全文を primary owner に転送して判断を仰ぐ"

error_patterns に正規表現やエラーコードのリストを持たせるのがポイントだ。LLM がパターンマッチングの代わりに確率的な判断をするのではなく、「どのパターンに合致したか」を構造的に教えてあげることで、アクションの精度が上がる。

LLM との繋ぎ方

バッチ失敗イベントは CloudWatch Alarms から Lambda に流れてくる。Lambda 内で以下の処理を行う。

import yaml
import json
import boto3
from openai import OpenAI

def load_runbook(batch_name: str) -> dict:
    # S3 や Git から YAML を取得する想定
    s3 = boto3.client("s3")
    obj = s3.get_object(Bucket="runbooks", Key=f"{batch_name}.yaml")
    return yaml.safe_load(obj["Body"].read())

def triage(error_log: str, runbook: dict) -> str:
    client = OpenAI()
    system_prompt = """
あなたはインフラ運用の自動トリアージシステムです。
Runbook の内容とエラーログを照合し、以下のフォーマットで回答してください。

- 検出されたエラーパターン:
- 推定原因:
- 今すぐ実施すべきアクション (箇条書き):
- エスカレーション先:
- 自動リトライ可否:

推測で補完しすぎず、Runbook に記載のない内容は「Runbook に記載なし」と明記してください。
"""
    user_message = f"""
# Runbook
{yaml.dump(runbook, allow_unicode=True)}

# エラーログ
{error_log}
"""
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_message},
        ],
        temperature=0.2,
    )
    return response.choices[0].message.content

temperature=0.2 に下げているのは、トリアージ結果で毎回表現が揺れると運用者が混乱するためだ。ここは創造性より一貫性のほうが重要になる。

Slack への投稿はシンプルに requests で Incoming Webhook に叩くだけだが、メッセージ構造にこだわった。エラーパターンが特定できた場合は緑・黄・赤のラベルで severity を色分けし、Runbook に記載のないパターンの場合はアクション部分に「Runbook 未定義」と明示して、曖昧なまま作業させない設計にしている。

ハマったポイントと現状の限界

YAML の error_patterns に書くエラーコードは、実際のログに出力される文字列と完全に一致させる必要がある。最初は人間が読みやすい名前で書いていて、LLM がログとの対応を誤認するケースが頻発した。ログから抽出したエラーコードをそのままキーにするようにしてから安定した。

LLM が「Runbook に記載のない状況」で補完してしまう問題も完全には消えていない。プロンプトで「Runbook に記載のない内容は明記してください」と強制しているが、それでも稀に Runbook 外の情報を参照して回答することがある。これはトリアージ結果のログを蓄積してレビューする運用を別途入れて対処している。

意思決定者向けに一言添えると、この仕組みの導入で「深夜に誰かを叩き起こす前の一次確認作業」が人手ゼロになった。エンジニアが判断すべき場面だけに絞られるため、対応品質と担当者の睡眠の両方が改善する。

Runbook の品質がそのままトリアージ精度に直結するので、YAML 化の過程でこれまで曖昧にしていた手順の穴が否応なく見えてくる。それ自体が現場のドキュメント改善を後押しする副作用になっている。