ポストモーテムの原則:Blameless
Blameless(責任追及なし)はポストモーテム文化の根幹です。 Google の SRE 本でも強調されるこの原則は、「人」ではなく「システムとプロセス」に注目することで、担当者が萎縮せず正確な情報を共有できる環境を作ります。
⚠️ 「なぜ山田さんはミスしたのか」はポストモーテムではない
個人の失敗を責めるドキュメントは組織の学習を阻害します。「なぜシステムが人間のミスを許す状態になっていたか」「なぜプロセスがミスを検出できなかったか」を問います。
💡 ポストモーテム vs インシデントレポート
インシデントレポート(Runbook)は障害対応中のリアルタイム記録。ポストモーテムは障害解決後に深く根本原因を分析し、再発防止策を導き出す振り返りドキュメントです。
障害サマリー
ポストモーテムの冒頭には、マネジメント層が 1 分で全体像を把握できるサマリーを配置します。
| フィールド | 定義すべき内容 |
|---|---|
| タイトル | 「注文 API 全断(2026-06-15 14:32〜15:48)」— 日時と影響を含む |
| インシデント番号 | INC-2026-0042(トラッカーへのリンク) |
| 重大度 | SEV1〜4 または P1〜P4(定義に従う) |
| 発生日時 | 2026-06-15 14:32 JST(ユーザー影響開始時刻) |
| 復旧日時 | 2026-06-15 15:48 JST |
| ダウンタイム | 1時間16分 |
| 検出方法 | Datadog アラート / ユーザー報告 / オンコール担当者が気づく |
| 一言概要 | Redis 接続プールの枯渇により注文 API が全断。新規デプロイで接続設定が誤っていた。 |
| 作成者 | 山田太郎(オンコール担当) |
| レビュアー | 田中花子(SRE)、佐藤一郎(バックエンド TL) |
影響範囲の定義
ビジネスへの影響を数字で定義します。感覚論ではなく計測値で記述します。
| 定義項目 | 内容・例 |
|---|---|
| 影響ユーザー数 | 約 12,000 ユーザーが注文操作不能(当該時間帯の平均アクティブユーザーの 80%) |
| 失敗リクエスト数 | POST /orders エラー計 24,500 件(Datadog ログより) |
| 売上損失(推定) | 約 380 万円(過去同時刻帯の平均売上 × ダウンタイム分) |
| カスタマーサポート問い合わせ | 障害時間中に 156 件の問い合わせが発生(通常の 8 倍) |
| SLA への影響 | 月次可用性 99.9%(目標)に対して今月の実績が 99.72% に低下 |
タイムライン
障害開始から復旧まで、「誰が・何をしたか・何が起きたか」を時系列で記録します。UTC または JSTで統一します。
## タイムライン(全時刻 JST)
| 時刻 | イベント | 担当者 |
|-------|---------|--------|
| 14:28 | v2.3.1 本番デプロイ完了(接続プールサイズ設定ミスを含む) | CI/CD 自動 |
| 14:32 | Datadog が `/orders` の 5xx エラー率 50% 超えをアラート | — |
| 14:33 | オンコール担当(山田)が Pagerduty 通知を受信 | 山田 |
| 14:36 | Datadog ダッシュボードで Redis 接続待ち時間の急上昇を確認 | 山田 |
| 14:41 | 佐藤(バックエンドTL)に Slack でエスカレーション | 山田 |
| 14:45 | デプロイログを確認し、v2.3.1 で接続プール設定が変更されていたことを特定 | 佐藤 |
| 14:52 | v2.3.0 へのロールバック開始 | 佐藤 |
| 15:01 | ロールバック完了。/orders エラー率が 0% に回復 | 佐藤 |
| 15:10 | Redis 接続プールが完全に解放されてメトリクスが正常値に戻る | — |
| 15:15 | カスタマーサポートに障害解消をアナウンス | 山田 |
| 15:48 | 全サービスの正常動作を確認し、インシデントをクローズ | 山田 |
根本原因分析(RCA)
根本原因分析(Root Cause Analysis)は「直接原因」と「根本原因」を分けて考えます。 直接原因は「何が障害を引き起こしたか」、根本原因は「なぜそれが起きたか・なぜ防げなかったか」です。 5 Why(なぜなぜ分析)が広く使われます。
## 根本原因分析(5 Why)
**直接原因:** Redis 接続プールが枯渇し、注文 API が新規接続を取得できなかった。
**なぜ 1:** v2.3.1 のデプロイで Redis 接続プールサイズが 100 → 10 に誤って変更されたから。
**なぜ 2:** 接続プールサイズを環境変数ではなくハードコードで変更し、
開発環境の値がそのまま本番に混入したから。
**なぜ 3:** 本番デプロイ前のコードレビューで、設定値の変更が目的外の変更と
気づかれなかったから(PR の変更差分が大きく埋もれていた)。
**なぜ 4:** 接続プールサイズの変更は CI で自動検出されず、
レビュアーが目視確認するしかない状態だったから。
**なぜ 5:** インフラ設定値の変更に対するポリシー(Infrastructure as Code の
必須化・設定変更の Lint チェック)が整備されていなかったから。
**根本原因:**
インフラ設定値(接続プールサイズ等)が環境変数または IaC で管理されておらず、
コード内のハードコードと混在していたため、レビューで検出できなかった。
また、設定値の異常を CI/CD パイプラインで自動検出する仕組みがなかった。
再発防止策・アクションアイテム
再発防止策は「具体的・担当者付き・期限付き」で定義します。「〜を強化する」という抽象的な表現は避けます。
| ID | アクション | 担当者 | 期限 | 優先度 |
|---|---|---|---|---|
| ACT-01 | 接続プールサイズを環境変数(REDIS_POOL_SIZE)に移行し、環境別に設定する |
佐藤 | 2026-06-22 | 🔴 高 |
| ACT-02 | CI パイプラインに設定値の範囲チェック(接続プール: 50〜200)を追加 | 田中 | 2026-06-25 | 🔴 高 |
| ACT-03 | 本番デプロイ前のカナリアリリース(5%トラフィック)を必須化 | 田中 | 2026-07-10 | 🟡 中 |
| ACT-04 | Redis 接続待ち時間のアラート閾値を 100ms → 50ms に引き下げ(早期検出) | 山田 | 2026-06-20 | 🔴 高 |
| ACT-05 | ロールバック手順を Runbook に追記し、全員が実行できるよう訓練 | 山田 | 2026-07-15 | 🟡 中 |
ポストモーテムテンプレート
# ポストモーテム: [タイトル]
> **ステータス:** ドラフト / レビュー中 / 最終版
> **インシデント番号:** INC-YYYY-NNNN
> **作成者:** [名前]
> **レビュアー:** [名前], [名前]
> **最終更新:** YYYY-MM-DD
---
## 1. 障害サマリー
| 項目 | 内容 |
|------|------|
| 重大度 | SEV? |
| 発生日時 | YYYY-MM-DD HH:MM JST |
| 復旧日時 | YYYY-MM-DD HH:MM JST |
| ダウンタイム | ? 時間 ? 分 |
| 検出方法 | アラート / ユーザー報告 |
**一言概要(2〜3文):**
[何が起きて、なぜ起きて、どう解決したか]
---
## 2. 影響範囲
- 影響ユーザー数:
- 失敗リクエスト数:
- 売上損失(推定):
- SLA 影響:
---
## 3. タイムライン
| 時刻 | イベント | 担当者 |
|------|---------|--------|
| HH:MM | [イベント] | [名前] |
---
## 4. 根本原因分析
**直接原因:**
**5 Why:**
1. なぜ:
2. なぜ:
3. なぜ:
4. なぜ:
5. なぜ:
**根本原因:**
---
## 5. うまくいったこと / うまくいかなかったこと
**うまくいったこと:**
- [良かった対応]
**うまくいかなかったこと:**
- [改善すべき対応]
---
## 6. アクションアイテム
| ID | アクション | 担当者 | 期限 | 優先度 |
|----|-----------|--------|------|--------|
| ACT-01 | | | | |
Python でアラートログからタイムラインを生成する
① Datadog/CloudWatch アラートログからタイムラインを自動生成
"""
Datadog / PagerDuty などのアラートログ(JSON Lines 形式)から
ポストモーテム用タイムラインの Markdown 草稿を生成する。
入力例 (alerts.jsonl):
{"timestamp": "2026-06-15T05:32:00Z", "source": "datadog", "message": "5xx error rate > 50%", "level": "CRITICAL"}
{"timestamp": "2026-06-15T05:41:00Z", "source": "slack", "message": "佐藤にエスカレーション", "level": "INFO"}
{"timestamp": "2026-06-15T05:52:00Z", "source": "github", "message": "Rollback PR merged v2.3.0", "level": "INFO"}
{"timestamp": "2026-06-15T06:01:00Z", "source": "datadog", "message": "5xx error rate back to 0%", "level": "INFO"}
{"timestamp": "2026-06-15T06:48:00Z", "source": "pagerduty", "message": "Incident INC-0042 resolved", "level": "INFO"}
使い方:
python gen_postmortem_timeline.py alerts.jsonl --tz Asia/Tokyo
"""
import json
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
import argparse
JST = timezone(timedelta(hours=9))
LEVEL_ICON = {
"CRITICAL": "🔴",
"ERROR": "🟠",
"WARNING": "🟡",
"INFO": "🔵",
}
SOURCE_LABEL = {
"datadog": "Datadog",
"pagerduty": "PagerDuty",
"slack": "Slack",
"github": "GitHub",
"aws": "AWS",
}
def parse_alerts(path: Path, local_tz: timezone = JST) -> list[dict]:
events = []
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
event = json.loads(line)
dt = datetime.fromisoformat(event["timestamp"].replace("Z", "+00:00"))
dt_local = dt.astimezone(local_tz)
events.append({
"time": dt_local,
"source": SOURCE_LABEL.get(event.get("source", ""), event.get("source", "")),
"message": event.get("message", ""),
"level": event.get("level", "INFO"),
})
return sorted(events, key=lambda e: e["time"])
def render_timeline(events: list[dict]) -> str:
lines = ["## タイムライン(全時刻 JST)\n", "| 時刻 | ソース | レベル | イベント |",
"|------|-------|--------|---------|"]
for e in events:
time_str = e["time"].strftime("%H:%M")
icon = LEVEL_ICON.get(e["level"], "⚪")
lines.append(f"| {time_str} | {e['source']} | {icon} {e['level']} | {e['message']} |")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("alerts_file", nargs="?", default="alerts.jsonl")
args = parser.parse_args()
path = Path(args.alerts_file)
if not path.exists():
print(f"ファイルが見つかりません: {path}", file=sys.stderr)
sys.exit(1)
events = parse_alerts(path)
print(f"# タイムライン草稿({len(events)} 件のイベント)")
print(f"# ※ 手動で対応内容・担当者を追記してください\n")
print(render_timeline(events))
if __name__ == "__main__":
main()
② 障害頻度のトレンド分析
"""
postmortems/ ディレクトリの Markdown ファイルを解析して
障害の発生パターン・重大度分布・根本原因カテゴリを集計する。
ファイル名規則: YYYY-MM-DD-INC-NNNN-title.md
フロントマター例:
severity: SEV1
root_cause_category: configuration / code / infra / external
downtime_minutes: 76
"""
import re
from collections import Counter, defaultdict
from datetime import datetime
from pathlib import Path
FRONTMATTER = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
KV = re.compile(r"^(\w+):\s*(.+)$", re.MULTILINE)
def parse_postmortem(path: Path) -> dict | None:
text = path.read_text(encoding="utf-8")
m = FRONTMATTER.match(text)
if not m:
return None
meta = dict(KV.findall(m.group(1)))
# ファイル名から日付を抽出
date_m = re.search(r"(\d{4}-\d{2}-\d{2})", path.name)
meta["date"] = datetime.strptime(date_m.group(1), "%Y-%m-%d") if date_m else None
return meta
def analyze(root: str = "postmortems/") -> None:
files = sorted(Path(root).glob("*.md"))
incidents = [m for f in files if (m := parse_postmortem(f)) is not None]
print(f"=== ポストモーテム分析レポート ({len(incidents)} 件) ===\n")
# 重大度分布
sev_count = Counter(i.get("severity", "不明") for i in incidents)
print("【重大度別件数】")
for sev, cnt in sorted(sev_count.items()):
print(f" {sev}: {cnt} 件")
# 根本原因カテゴリ
rc_count = Counter(i.get("root_cause_category", "不明") for i in incidents)
print("\n【根本原因カテゴリ別件数】")
for rc, cnt in sorted(rc_count.items(), key=lambda x: -x[1]):
bar = "█" * cnt
print(f" {rc:<15} {bar} ({cnt} 件)")
# 月別件数
monthly = defaultdict(int)
for i in incidents:
if i.get("date"):
monthly[i["date"].strftime("%Y-%m")] += 1
print("\n【月別件数】")
for month in sorted(monthly.keys()):
bar = "█" * monthly[month]
print(f" {month}: {bar} ({monthly[month]} 件)")
# 平均ダウンタイム
downtimes = [int(i["downtime_minutes"]) for i in incidents if "downtime_minutes" in i]
if downtimes:
print(f"\n【ダウンタイム統計】")
print(f" 平均: {sum(downtimes)/len(downtimes):.1f} 分")
print(f" 最大: {max(downtimes)} 分")
print(f" 最小: {min(downtimes)} 分")
if __name__ == "__main__":
import sys
analyze(sys.argv[1] if len(sys.argv) > 1 else "postmortems/")
ポストモーテムプロセス
| フェーズ | タイミング | 内容 |
|---|---|---|
| ドラフト作成 | インシデント解消後 24 時間以内 | タイムライン・影響範囲・根本原因の初版を作成 |
| ミーティング | 解消後 72 時間以内 | 関係者全員で内容をレビュー・ディスカッション(30〜60分) |
| 最終版公開 | ミーティング後 48 時間以内 | 全チームが閲覧できる場所に公開(Notion / Confluence / GitHub) |
| アクション追跡 | 毎週スプリントレビューで確認 | アクションアイテムの完了状況をトラッキング |
まとめ
✅ ポストモーテムで定義すべき要素
□ 障害サマリー(重大度・発生/復旧時刻・ダウンタイム・検出方法・一言概要)
□ 影響範囲(ユーザー数・失敗リクエスト数・売上損失・SLA 影響)
□ タイムライン(時系列で誰が・何をした・何が起きたか)
□ 根本原因分析(5 Why で直接原因と根本原因を分けて定義)
□ うまくいったこと / いかなかったこと
□ 再発防止策(具体的・担当者付き・期限付き・優先度付き)
□ Blameless 原則(個人ではなくシステム・プロセスに注目)
□ インシデント解消後 24 時間以内にドラフト作成