Runbook とは何か

Runbook(ランブック)は「決まった操作手順を、誰でも・どんな状況でも再現できるよう記述したドキュメント」です。 デプロイ手順・定期バッチの起動・障害時の初動対応・DR(災害復旧)など、繰り返し発生する運用作業をカバーします。

Runbook の価値は「書いた人がいなくても作業できる」点にあります。深夜に呼び出された担当者が Runbook を開き、上から順に実行するだけで作業が完結する——そのレベルの具体性が求められます。

⚠️ Runbook と SOP の違い

SOP(Standard Operating Procedure:標準作業手順書)は業務プロセス全体を定義するのに対し、Runbook はシステム操作の具体的なコマンド列・チェックリストに特化しています。粒度が異なります。

① 概要・目的

Runbook の冒頭に「何のための手順書か」を1段落で明示します。定義すべき項目は次のとおりです。

項目説明
タイトル操作内容を端的に表す名詞句本番 DB フェイルオーバー手順
目的この手順で何を達成するかプライマリ障害時にスタンバイへ切り替える
対象システム影響を受けるサービス・コンポーネントorder-api / PostgreSQL クラスタ
所要時間平均的な作業時間の目安約 15 分(ダウンタイム 2〜3 分)
最終更新日手順の鮮度を確認するため2026-06-20

② 対象読者と前提条件

誰が実行するか・何を知っている前提で書かれているかを明示します。

カテゴリ定義すべき内容
対象読者オンコール担当者 / SRE / DBA など
必要な権限sudo 権限 / AWS IAM ロール / DB 管理者アカウントなど
必要ツールpsql コマンド・aws CLI・kubectl など、事前インストールが必要なもの
前提状態「プライマリが応答不能であること」などの実行開始条件
影響範囲の確認実行前にステークホルダーへの通知が必要か

③ 手順(番号付きステップ)

Runbook の核心部分です。番号付きリストで「1つのステップ = 1つの操作」を徹底します。 コマンドはコピー&ペーストで実行できる形式で記載し、期待される出力も添えます。

Markdown — 手順の書き方テンプレート
## 手順

### 1. 現在のプライマリを確認する
```bash
# どのノードがプライマリかを確認(pg_is_in_recovery = f がプライマリ)
psql -h db-cluster.internal -U admin -c "SELECT pg_is_in_recovery();"
```
**期待される出力:**
```
 pg_is_in_recovery
-------------------
 f
(1 row)
```
⚠️ プライマリが応答しない場合は手順 3 へ進む

### 2. スタンバイの同期状態を確認する
```bash
psql -h db-standby.internal -U admin -c "SELECT pg_last_wal_receive_lsn(), pg_last_wal_replay_lsn();"
```
**確認ポイント:** receive_lsn と replay_lsn が一致していること(ラグが 0 に近いこと)

### 3. フェイルオーバーを実行する
```bash
# pg_promote() でスタンバイをプライマリに昇格
psql -h db-standby.internal -U admin -c "SELECT pg_promote();"
```

💡 ステップの粒度

「サーバーにログインする」から「コマンドを実行する」まで、1ステップで複数操作が混在しないよう分割します。迷う余地をなくすことが Runbook の品質です。

④ 確認手順(ヘルスチェック)

主手順の完了後に「正しく完了したか」を確認するチェックリストです。以下を定義します。

確認観点確認方法合格基準
サービス稼働curl -sf http://localhost:8080/healthHTTP 200 が返ること
エラーログtail -n 50 /var/log/app/error.logERROR 行が増加していないこと
DB 接続アプリから DB へのクエリが成功することSELECT 1 が 1 ms 以内に返ること
監視アラートDatadog / CloudWatch でアラートが解消されること全アラートが Green

⑤ ロールバック手順

作業が失敗または期待と異なる結果になった場合に元の状態へ戻す手順です。 メインの手順と同様に番号付きで記載し、「何があればロールバックを実行するか(判断基準)」も明示します。

Markdown — ロールバック手順の書き方
## ロールバック手順

**実行判断基準:** 手順 3 の完了後 5 分経過してもヘルスチェックが通らない場合

### 1. フェイルオーバー前のノードを復帰させる
```bash
# 旧プライマリを再起動
ssh db-primary.internal "sudo systemctl restart postgresql"
```

### 2. スタンバイに戻す設定を適用する
```bash
psql -h db-primary.internal -U admin -c "SELECT pg_ctl_promote();"
# ※ 新プライマリ(元スタンバイ)がある状態でのみ実行
```

### 3. アプリの接続先を元に戻す
```bash
# 環境変数を旧プライマリに変更して再起動
export DB_HOST=db-primary.internal
sudo systemctl restart app-service
```

⑥ エラー対処(トラブルシューティング)

作業中によく発生するエラーとその対処法を FAQ 形式でまとめます。 過去の障害対応から「詰まったポイント」を抽出して掲載するのが最も効果的です。

症状 / エラーメッセージ原因対処法
FATAL: could not connect to the primary server プライマリが完全に停止している プライマリノードの状態を確認し、手順 3 へ進む
pg_promote() が返らない スタンバイが recovery モードでない pg_is_in_recovery() で状態を確認。t なら再試行
アプリが旧プライマリに接続し続ける DNS TTL が残っている アプリを再起動して接続プールをリセットする

⑦ 連絡先・エスカレーション

自己解決できない場合の連絡先を明記します。「誰に・何を伝えるか」のテンプレートも添えると実用的です。

エスカレーション先条件連絡方法
DBA チームフェイルオーバーが 10 分以内に完了しない場合Slack #oncall-dba / PagerDuty
インフラリードロールバックも失敗した場合電話(番号: 社内電話帳参照)
サービス責任者ダウンタイムが 30 分を超える場合Slack DM + メール

Python で実機情報を自動収集する

Runbook の「手順実行前状態」確認や「ヘルスチェック」を Python で自動化できます。

① システム情報の自動収集(psutil)

Python — CPU・メモリ・ディスク・プロセスを一括取得
"""
Runbook 実行前の事前確認用スクリプト。
psutil で CPU・メモリ・ディスク・プロセスを取得して表示する。
インストール: pip install psutil
"""
import psutil
from datetime import datetime

def system_snapshot() -> dict:
    cpu    = psutil.cpu_percent(interval=1)
    mem    = psutil.virtual_memory()
    disk   = psutil.disk_usage("/")
    uptime = datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S")

    return {
        "timestamp": datetime.now().isoformat(),
        "uptime_since": uptime,
        "cpu_percent":  cpu,
        "mem_total_gb": round(mem.total / 1024**3, 1),
        "mem_used_pct": mem.percent,
        "disk_total_gb": round(disk.total / 1024**3, 1),
        "disk_used_pct": disk.percent,
    }

def find_process(name: str) -> list[dict]:
    """指定プロセス名が動いているか確認する。"""
    result = []
    for proc in psutil.process_iter(["pid", "name", "status", "create_time"]):
        if name.lower() in proc.info["name"].lower():
            result.append({
                "pid":    proc.info["pid"],
                "name":   proc.info["name"],
                "status": proc.info["status"],
                "since":  datetime.fromtimestamp(proc.info["create_time"]).strftime("%H:%M:%S"),
            })
    return result

if __name__ == "__main__":
    snap = system_snapshot()
    print("=== システム状態スナップショット ===")
    for k, v in snap.items():
        print(f"  {k}: {v}")

    print("\n=== PostgreSQL プロセス確認 ===")
    procs = find_process("postgres")
    if procs:
        for p in procs:
            print(f"  PID {p['pid']} | {p['name']} | {p['status']} | 起動: {p['since']}")
    else:
        print("  ⚠️  PostgreSQL プロセスが見つかりません")

② ログの自動テール(障害時の確認用)

Python — ログファイルから ERROR/WARN を抽出して表示
"""
指定ログファイルの末尾 N 行から ERROR / WARN を抽出する。
Runbook のヘルスチェック・障害初動で使う。
"""
import re
from pathlib import Path
from collections import Counter

LOG_FILE = Path("/var/log/app/application.log")
TAIL_LINES = 200
PATTERN = re.compile(r"\b(ERROR|WARN|CRITICAL)\b", re.IGNORECASE)

def tail(path: Path, n: int) -> list[str]:
    """ファイルの末尾 n 行を返す。大きなファイルでも効率的。"""
    with path.open("rb") as f:
        f.seek(0, 2)
        size = f.tell()
        buf = bytearray()
        lines = 0
        pos = size - 1
        while pos >= 0 and lines < n:
            f.seek(pos)
            byte = f.read(1)
            if byte == b"\n" and pos != size - 1:
                lines += 1
            buf.extend(byte)
            pos -= 1
        return buf[::-1].decode("utf-8", errors="replace").splitlines()

def analyze_log(lines: list[str]) -> None:
    level_counter = Counter()
    print(f"{'レベル':<10} {'件数':>5}   メッセージ(最初の3件)")
    print("-" * 60)
    collected = {"ERROR": [], "WARN": [], "CRITICAL": []}

    for line in lines:
        m = PATTERN.search(line)
        if m:
            level = m.group(1).upper()
            level_counter[level] += 1
            if len(collected.get(level, [])) < 3:
                collected.setdefault(level, []).append(line.strip())

    for level, count in sorted(level_counter.items()):
        print(f"{level:<10} {count:>5}")
        for msg in collected.get(level, [])[:3]:
            print(f"           → {msg[:80]}")

if __name__ == "__main__":
    if not LOG_FILE.exists():
        print(f"ログファイルが見つかりません: {LOG_FILE}")
    else:
        lines = tail(LOG_FILE, TAIL_LINES)
        print(f"=== {LOG_FILE} 末尾 {TAIL_LINES} 行の解析結果 ===\n")
        analyze_log(lines)

③ HTTP ヘルスチェックの自動実行

Python — 複数エンドポイントのヘルスチェックを一括実行
"""
Runbook の確認手順で使うヘルスチェックスクリプト。
エンドポイント一覧に対して HTTP GET を実行して結果を出力する。
"""
import urllib.request
import urllib.error
import time

ENDPOINTS = [
    ("order-api   ", "http://localhost:8080/health"),
    ("payment-api ", "http://localhost:8081/health"),
    ("DB proxy    ", "http://localhost:5432/"),  # pg_proxy など
]
TIMEOUT = 5

def check(label: str, url: str) -> dict:
    start = time.monotonic()
    try:
        with urllib.request.urlopen(url, timeout=TIMEOUT) as resp:
            elapsed = (time.monotonic() - start) * 1000
            return {"label": label, "status": resp.status, "ms": round(elapsed, 1), "ok": True}
    except urllib.error.HTTPError as e:
        return {"label": label, "status": e.code, "ms": -1, "ok": False}
    except Exception as e:
        return {"label": label, "status": str(e)[:30], "ms": -1, "ok": False}

if __name__ == "__main__":
    print(f"{'サービス':<14} {'Status':>6}  {'応答(ms)':>9}  結果")
    print("-" * 45)
    all_ok = True
    for label, url in ENDPOINTS:
        r = check(label, url)
        icon = "✅" if r["ok"] else "❌"
        ms = f"{r['ms']} ms" if r["ms"] >= 0 else "timeout"
        print(f"{r['label']:<14} {str(r['status']):>6}  {ms:>9}  {icon}")
        if not r["ok"]:
            all_ok = False
    print()
    print("🟢 全サービス正常" if all_ok else "🔴 異常あり — Runbook のトラブルシューティングを参照")

Runbook のアンチパターン

アンチパターン問題対策
「適宜確認してください」深夜の担当者は判断できない合格基準を具体的な数値で書く
ロールバックがない失敗時に手動で判断する羽目になる必ずロールバック手順とトリガー条件を定義する
コマンドが抽象的環境差異で動かない実行例と期待出力を必ずセットにする
更新されない本番と Runbook が乖離するデプロイのたびに Runbook の動作確認を CI に組み込む
エスカレーション先が「担当者に連絡」誰に・どう連絡するか分からないSlack チャンネル名・PagerDuty サービス名まで明記する

まとめ

Runbook で定義すべき7要素

□ 概要・目的(何のための手順書か)
□ 対象読者と前提条件(権限・ツール・実行開始条件)
□ 手順(番号付き・コマンド+期待出力)
□ 確認手順(ヘルスチェックと合格基準)
□ ロールバック手順(判断基準と具体的コマンド)
□ エラー対処(症状→原因→対処の FAQ 形式)
□ 連絡先・エスカレーション(チャンネル・条件・伝えるべき内容)

Runbook は「書いたら終わり」ではありません。実際に手順を実行した担当者がフィードバックを書き込む文化と、定期的な Dry Run(演習)を組み合わせることで、常に機能するドキュメントになります。