Article

インシデント発生時の報告書テンプレート

高田晃太郎
インシデント発生時の報告書テンプレート

Uptime Instituteの2023年調査では、重大障害の過半数が10万ドル以上の損失をもたらし、100万ドル超に達する例も珍しくないと報告されています[1]。開発運用の研究領域であるDORAの知見では、回復の速い組織は平均修復時間(MTTR)を短く保ち、学習サイクルを高速に回す傾向が明確です[2]。つまり、被害額は運だけでなく、平時に用意した型の良し悪しで変わるということです。インシデント対応は混乱の連続に見えますが、報告書のテンプレートを設計しておけば、判断の重さを減らし、検証可能なデータを土台に学習を積み上げられます。同じ失敗を二度繰り返さないための仕組み化は、書式の統一から始まるという前提で、本稿はテンプレートの完全版と、自動化で「書ける化」する実装、さらにMTTR短縮と再発防止に直結する具体策までをまとめて提示します。

本稿の位置づけと前提

対象はCTOやエンジニアリングマネージャ、SREのリーダー層です。目的は、現場が迷わず記述でき、経営が意思決定に使える報告書を最短で出すための標準を確立することにあります。想定する環境は、監視はPrometheusもしくはCloud Monitoring、チケットはJiraまたはGitHub Issues、データストアはPostgreSQL、スクリプトはPython 3.11です。これはあくまで一例で、異なるスタックでも概念は不変です。フィールドの意味と収集の流れを守れば移植は容易で、オンコール体制やインシデント・コマンド・システム(ICS)と組み合わせることで運用全体の一貫性が高まります。

なぜテンプレートがROIを生むのか

意思決定は構造化された情報に依存します。影響範囲、顧客影響、SLO逸脱、検知から復旧までの時系列、原因と対策の検証可能性、オーナーシップと期限——これらが行間に埋もれた報告書では、レビューも投資判断も遅れます。テンプレートは視点の抜け漏れを防ぎ、集計可能なデータを副産物として残します。定量化してみると投資回収は直感以上に速く、たとえば四半期にP1相当の障害が三件、ダウンタイムの損失が時給10万ドル(一般的に用いられる試算レンジの一つ[7])のシステムにおいて、平均修復時間が四時間から二時間半へ短縮できれば、削減額は四半期で約45万ドル、年で約180万ドルという試算になります。こうした「MTTR短縮の経済効果」は、インシデントマネジメントの実務でも多く報告されています[4]。テンプレートの導入と自動記載の仕組み化は、数%の稼働改善に匹敵する利益を生む可能性がある、という視点で捉えるべきです。

組織学習の観点でも優位性があります。復旧の早いチームほど学習のサイクルが短く、デプロイ頻度や変更失敗率の改善に波及します[2]。報告書テンプレートがもたらすのは単なる文章の統一ではなく、後続の事後検証会、エラーバジェットの再配分、リリース戦略の見直しまでつながる意思決定の速度です。実務では、MTTD/MTTRを明示的に記録し、SLOの燃尽率(burn rate)と合わせて可視化するだけでも、次の投資判断が一段具体的になります[3]。

参考までに、簡易な節約額の試算式を示します。

# mttr_roi.py: MTTR短縮の粗い節約額を試算
def savings(cost_per_hour: float, mttr_before_h: float, mttr_after_h: float, incidents_per_quarter: int) -> float:
    return cost_per_hour * max(mttr_before_h - mttr_after_h, 0) * incidents_per_quarter

print(savings(100_000, 4.0, 2.5, 3))  # => 450000.0

報告書テンプレートの完全版

テンプレートは、エグゼクティブが一画面で全体を把握でき、現場が再現可能なレベルで事実を残せる粒度に整えます。見出しの順序は要約、影響、時系列、原因、検知、対応、対策、証跡、オーナー、付記の流れが扱いやすく、読み手の負担が最小になります。標準化されたポストモーテム様式は、抜け漏れを防ぎ、各レビューでの再現性を高める実務的効果が報告されています[5]。まずはMarkdownでそのまま使える雛形を提示します。

# インシデント報告書

## 概要
発生日: {{YYYY-MM-DD HH:MM TZ}}
重大度: {{P0|P1|P2}}
現在の状態: {{復旧済み|復旧中|監視中}}
要約: {{一文で要約}} 例: 「APIのスロットリング設定誤りにより、JP/EUで99分の遅延発生。ロールバックで復旧。」

## 影響
影響範囲: {{サービス名/領域}} 例: 「payments, api」
顧客影響: {{件数/割合/地域}} 例: 「全リクエストの2.3%が5xx(JP/EU)」
SLO/SLA: {{対象SLO, 逸脱の有無と量}} 例: 「availability 99.9% 月次、今回の燃尽0.12%」
事業影響推定: {{金額/機会損失/規制影響}} 例: 「約$25,000相当の機会損」

## タイムライン
検知: {{YYYY-MM-DD HH:MM}} {{検知手段}} 例: 「2025-08-30 01:03Z Alertmanager」
エスカレーション: {{担当/役割}} 例: 「IC/Comms/Page」
主要イベント: {{時刻と出来事を時系列で}} 例: 「01:15Z デプロイa1b2開始」「01:27Z ロールバック」
復旧: {{YYYY-MM-DD HH:MM}} {{復旧手段}} 例: 「02:42Z ロールバック完了」

## 技術的原因
直接原因: {{現象}} 例: 「スロットリング阈値が十分でない」
根本原因: {{設計/運用/プロセス}} 例: 「設定のレビュー不足とステージングの負荷試験欠如」
トリガー: {{変更/外部要因}} 例: 「直近の設定変更」

## 検知と対応
検知方法: {{自動/手動}} 例: 「自動検知(SLO燃尽アラート)」
対応内容: {{コマンド/リリース/設定変更}} 例: 「ロールバック、レプリカ数増」
意思決定: {{判断根拠と選択肢}} 例: 「p99遅延上昇とエラー率を根拠に切り戻し」

## 再発防止と改善
恒久対策: {{設計変更/自動化/ガードレール}} 例: 「負荷試験のゲート化、スロットリングの自動調整」
短期対策: {{暫定的な緩和策}} 例: 「レート上限引き上げ、キャッシュ延長」
フォローアップ: {{チケットID・期限・担当}} 例: 「OPS-123 2025-09-15 owner: sato」

## 証跡
ダッシュボード: {{URL}}
ログ/トレース: {{URL}}
PR/Issue: {{URL}}
会議メモ: {{URL}}

## 連絡と周知
ステータスページ: {{URL}}
顧客連絡: {{チャネルと時刻}}
社内連絡: {{チャンネルと時刻}}

## 付記
その他: {{インサイト/残課題}}

構造化データで永続化する場合は、下記のJSON Schemaをベースにすると集計や検索が柔軟になります。重大度、時刻、SLO参照、タイムラインのイベント種別を必須にしておくと、後からの可視化が容易になります。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "IncidentReport",
  "type": "object",
  "required": ["id", "severity", "status", "summary", "startedAt", "timeline", "impact"],
  "properties": {
    "id": {"type": "string"},
    "severity": {"enum": ["P0", "P1", "P2", "P3"]},
    "status": {"enum": ["resolved", "mitigated", "monitoring", "ongoing"]},
    "summary": {"type": "string", "minLength": 10},
    "startedAt": {"type": "string", "format": "date-time"},
    "resolvedAt": {"type": "string", "format": "date-time"},
    "impact": {
      "type": "object",
      "required": ["services", "usersAffected", "slo"],
      "properties": {
        "services": {"type": "array", "items": {"type": "string"}},
        "usersAffected": {"type": "integer", "minimum": 0},
        "regions": {"type": "array", "items": {"type": "string"}},
        "slo": {"type": "string"},
        "businessLossUSD": {"type": "number", "minimum": 0}
      }
    },
    "timeline": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["at", "type", "note"],
        "properties": {
          "at": {"type": "string", "format": "date-time"},
          "type": {"enum": ["detect", "page", "escalate", "mitigate", "recover", "note"]},
          "note": {"type": "string"}
        }
      }
    },
    "rootCause": {"type": "string"},
    "actions": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["title", "owner", "due"],
        "properties": {
          "title": {"type": "string"},
          "owner": {"type": "string"},
          "due": {"type": "string", "format": "date"},
          "status": {"enum": ["open", "in_progress", "done"]}
        }
      }
    },
    "evidence": {"type": "array", "items": {"type": "string", "format": "uri"}}
  }
}

実際の記載は、要約に一文の全体像、影響で具体的な人数や割合、SLOの逸脱量、事業への換算、タイムラインに検知から復旧までの事実を淡々と並べるのが基本です。原因の章では現象と背景を分け、再発防止では行動に落ちた改善のみを並べ、各項目にオーナーと期限を必ず付与します。事実と解釈を分けて書くことが、読み手の信頼を担保します。

自動で「書ける化」:集計可能な報告の実装

人手で集めにくいデータは自動で差し込むと精度が上がります。監視のアラート、デプロイログ、メトリクス、ステータスページの更新、チャットのエスカレーション時刻などは、APIやログから回収して時系列に整形すると、後から検証しやすくなります。ここでは、メトリクスのスナップショット取得、イベント抽出、タイムライン生成、テンプレートへの差し込みまでの一連を例示します。

#!/usr/bin/env bash
# metrics_snapshot.sh: Prometheusから期間内のSLO関連メトリクスを取得
set -euo pipefail
INCIDENT_ID="$1"
START="$2"   # 例: 2025-08-30T01:00:00Z
END="$3"     # 例: 2025-08-30T03:00:00Z
PROM="https://prom.example.com/api/v1/query_range"
QUERY_UP='sum_over_time(up{job="api"}[5m])'
QUERY_LAT='histogram_quantile(0.99, sum by (le)(rate(http_request_duration_seconds_bucket{job="api"}[5m])))'
for q in "$QUERY_UP" "$QUERY_LAT"; do
  curl -sG "$PROM" --data-urlencode "query=$q" --data-urlencode "start=$START" --data-urlencode "end=$END" --data-urlencode "step=60" \
    > "./artifacts/${INCIDENT_ID}_$(echo $q | cut -d'(' -f1).json"
done
echo "saved artifacts to ./artifacts for $INCIDENT_ID"
-- events.sql: デプロイ/アラート/ページの主要イベントを抽出(PostgreSQL想定)
-- 事前にincident_eventsテーブルを用意してAlertmanagerやCIからwebhookで蓄積しておくと良い
WITH ev AS (
  SELECT incident_id,
         occurred_at AT TIME ZONE 'UTC' AS at,
         type,
         details
  FROM incident_events
  WHERE incident_id = $1
)
SELECT incident_id, at, type, details
FROM ev
ORDER BY at ASC;
# timeline.py: ログやDBから時系列を統合しJSONに整形
import json, sys, psycopg
from datetime import datetime, timezone

def fetch_events(incident_id: str):
    with psycopg.connect() as conn:
        with conn.cursor() as cur:
            cur.execute(open('events.sql').read(), (incident_id,))
            return [dict(incident_id=row[0], at=row[1].replace(tzinfo=timezone.utc).isoformat(), type=row[2], note=row[3]) for row in cur]

def infer_summary(timeline):
    started = next((e for e in timeline if e['type'] in ('detect','page')), timeline[0])
    recovered = next((e for e in reversed(timeline) if e['type'] in ('recover','mitigate')), None)
    mttr = None
    if recovered:
        t1 = datetime.fromisoformat(started['at'].replace('Z', '+00:00'))
        t2 = datetime.fromisoformat(recovered['at'].replace('Z', '+00:00'))
        mttr = (t2 - t1).total_seconds() / 60
    return started, recovered, mttr

def main():
    incident_id = sys.argv[1]
    timeline = fetch_events(incident_id)
    started, recovered, mttr = infer_summary(timeline)
    obj = {
        "id": incident_id,
        "timeline": timeline,
        "startedAt": started['at'],
        "resolvedAt": recovered['at'] if recovered else None,
        "meta": {"mttr_minutes": mttr}
    }
    print(json.dumps(obj, ensure_ascii=False, indent=2))

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        sys.stderr.write(f"error: {e}\n")
        sys.exit(1)
{# report.md.j2: Jinja2でMarkdown報告書を生成 #}
# インシデント報告書

## 概要
発生日: {{ startedAt }}
重大度: {{ severity }}
現在の状態: {{ status }}
要約: {{ summary }}{% if meta and meta.mttr_minutes %}(推定MTTR: {{ '%.0f' % meta.mttr_minutes }}分){% endif %}

## 影響
影響範囲: {{ impact.services | join(', ') }}
顧客影響: {{ impact.usersAffected }} users
SLO/SLA: {{ impact.slo }}
事業影響推定: ${{ '%.2f' % impact.businessLossUSD }}

## タイムライン
{% for e in timeline %}
- {{ e.at }} {{ e.type }} {{ e.note }}
{% endfor %}

## 技術的原因
{{ rootCause }}

## 再発防止と改善
{% for a in actions %}
- [{{ a.title }}]({{ a.link }}) owner: {{ a.owner }} due: {{ a.due }} status: {{ a.status }}
{% endfor %}
# render.py: 取得したJSONを雛形に差し込み
import json
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('.'))
tpl = env.get_template('report.md.j2')

def render(report_json_path: str) -> str:
    with open(report_json_path) as f:
        data = json.load(f)
    data.setdefault('severity', 'P1')
    data.setdefault('status', 'resolved')
    data.setdefault('impact', {})
    data['impact'].setdefault('services', ['api'])
    data['impact'].setdefault('usersAffected', 0)
    data['impact'].setdefault('slo', 'availability 99.9% monthly')
    data['impact'].setdefault('businessLossUSD', 0)
    data.setdefault('actions', [])
    return tpl.render(**data)

if __name__ == '__main__':
    import sys
    md = render(sys.argv[1])
    print(md)

テンプレート化の価値は、書く手間を減らすだけではなく、SLOの参照や影響額の算定など、後から意思決定に使える数字を同じ場所と形式で残す点にあります。可観測性ツールやチケットシステムと接続し、生成までを定常ジョブに組み込むと、報告書はインシデント終了直後のデブリーフィングに間に合う品質で出力されるようになります[5][6]。

加えて、MTTD/MTTRの継続的計測とボトルネック可視化は短縮に直結します。過去インシデントから四半期のMTTRを算出するクエリ例を示します。

-- mttr_stats.sql: 四半期のMTTR中央値と90パーセンタイル
WITH spans AS (
  SELECT id,
         extract(epoch from (resolved_at - started_at))/60 AS mttr_min
  FROM incident_reports
  WHERE started_at >= date_trunc('quarter', now())
)
SELECT
  percentile_disc(0.5) WITHIN GROUP (ORDER BY mttr_min) AS p50_min,
  percentile_disc(0.9) WITHIN GROUP (ORDER BY mttr_min) AS p90_min,
  avg(mttr_min) AS avg_min
FROM spans;

品質と再発防止を高める記述の勘所

現場で効く書き方には共通点があります。まず、要約は一文で読み手に結論を届けることを最優先にし、影響の章では人数、割合、地理、SLO逸脱の具体値に必ず触れます。次に、タイムラインはチャットログやアラート時刻、デプロイのハッシュやタグなど、検証可能な証拠と結び付けると、後日異なる立場の読者が再構成できる文章になります。原因の章は現象・背景・制度を階層化し、個人名や責任追及の文言を避けることが、改善の質を保つ近道です。対策に関しては、恒久対策は設計の原理に紐づけ、短期対策はリスクを明記し、どちらもオーナーと期限をセットで示します。期日がない対策は存在しないに等しいため、チケットへのリンクと同時に合意を取る運用が欠かせません。ブレームレス・ポストモーテムの文化は、学習の質と速度を高める実践としてSREの文献でも重視されています[6]。

再発防止は「動くガードレール」に落とすと効きます。具体例を二つ示します。

  • SLO燃尽の早期検知(burn rate)アラート。復旧判断の遅れを抑え、MTTR短縮に寄与します[3]。
# Alertmanager向けPrometheusRule例(burn rate)
groups:
- name: slo-burn
  rules:
  - alert: api_availability_burn_fast
    expr: (1 - (sum(rate(http_requests_total{job="api",code!~"5.."}[5m])) / sum(rate(http_requests_total{job="api"}[5m])))) > (1-0.999) * 14.4
    for: 5m
    labels: {severity: page}
    annotations:
      summary: "API availability fast burn"
      description: "Error budget burning fast over 5m window"
  • デプロイ前の自動チェック。設定変更にレビュー漏れがないかをCIで止める。
# .github/workflows/preflight.yml: 変更のガードレール
name: preflight
on: [pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Lint configs
        run: ./scripts/lint-configs.sh
      - name: Load test gate (smoke)
        run: ./scripts/smoke-load-test.sh

影響額の推定は、システムのビジネスモデルに合わせて簡便式を一つ決めておくと再現性が高まります。トランザクション型なら失注件数に平均粗利を乗算、サブスクリプション型なら返金・クレジット・解約率の増分を基に、マーケティングやカスタマーサクセスと合意した係数を使うと良いでしょう。エラーバジェットに連動させる運用では、SLA(外部コミットメント)とSLO(内部目標)を混同せず、両者を明示的に分けて運用・記述します[3]。

書式の統一に加えて、運用の役割分担を明文化すると品質が安定します。インシデント指揮系統は、Incident Commander、Operations、Communications、Scribeなどの役割を定義しておくと、書き手が誰なのかで品質が揺れにくくなります。役割の定義例や運用手順は、インシデント・コマンド・システムの一般的な解説を参照してください。あわせて、再発防止の質を底上げしたいときは、ブレームレス・ポストモーテムの原則を引用すると、議論が建設的に進みます。人を責めず、プロセスを直すという合意があるだけで、原因の掘り下げは数段進みやすくなります[6]。

データの扱いも重要です。SLOの逸脱は時系列グラフと数値の双方を示し、検知に関してはMTTD、復旧に関してはMTTRを分けて計測・記載します。デプロイや構成の変更が絡む場合、変更失敗率やロールバック率を添えると、リリース戦略の最適化に直結します。可観測性のダッシュボード、トレースのサンプル、ログの抜粋は、報告書本文に全てを貼り付けず、リンクで証跡として残すだけに留める方が、可読性と再利用性の両立に有効です。なお、オンコール運用の成熟度がMTTDやMTTRに大きく影響するため、オンコール・ランブックの整備と連動させると、報告書テンプレートの効果が最大化します。組織学習の文化と指標運用の整備は、ソフトウェアデリバリのパフォーマンス向上と相関があることが報告されています[2]。

よくある落とし穴と回避策

事実と推測の混在は、後日読み返したときに改善の方向性を誤らせます。推測は推測として明示し、事実は証跡に紐付ける、といった書き分けを徹底します。また、原因の章で五つのなぜにこだわり過ぎると、解のない抽象に迷い込みがちです。設計原則や運用ルールと結び付く層で止め、対策の検証可能性が担保される粒度で筆を置く方が、現実的な改善になります。最後に、会議体の遅延は学習速度を落とすため、報告書の初版は復旧から24時間以内、レビューは一週間以内、対策は四週間以内といったタイムボックスをチームの規律として合意しておくと、継続的に回せます[5]。

まとめ:混乱を構造に変え、次の障害に強くなる

インシデントは避けられない出来事ですが、被害の大きさと学びの深さは設計できます。インシデント報告書テンプレートは、現場の記述負荷を減らし、経営の意思決定に耐えるデータを残し、組織学習の速度を上げる、費用対効果の高い投資です。まずは本稿の雛形をそのまま導入し、監視・チケット・ソース管理と連携させて自動で差し込まれる情報を増やしてください。事実に基づく一貫した書式と、タイムボックスを伴うレビュー運用が回り始めれば、平均修復時間は短くなり、再発防止は行動に落ち、次の障害に強いチームに変わります。自社のSLOやビジネスモデルに合わせて、影響額の算定式や役割分担を微調整する準備はできていますか。今日発生した小さなアラートから、最初の一枚を出してみることが、明日の重大障害を小さく抑える第一歩になります。

参考文献

  1. Uptime Institute. Annual Outage Analysis 2023. https://uptimeinstitute.com/resources/research-and-reports/annual-outage-analysis-2023
  2. DORA. Learning culture. https://dora.dev/capabilities/learning-culture/
  3. Google Cloud Blog. Availability Part Deux: CRE life lessons (SLOとSLAの違いなど). https://cloud.google.com/blog/products/devops-sre/availability-part-deux-cre-life-lessons
  4. Squadcast. ROI of Reducing MTTR: Real-World Benefits and Savings. https://www.squadcast.com/blog/roi-of-reducing-mttr-real-world-benefits-and-savings
  5. Atlassian. Postmortem templates. https://www.atlassian.com/incident-management/postmortem/templates
  6. Google SRE Book. Postmortem Culture. https://sre.google/sre-book/postmortem-culture/
  7. TechTarget. The cost of downtime and how businesses can avoid it. https://www.techtarget.com/searchdatabackup/feature/the-cost-of-downtime-and-how-businesses-can-avoid-it