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つの操作」を徹底します。 コマンドはコピー&ペーストで実行できる形式で記載し、期待される出力も添えます。
## 手順
### 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/health | HTTP 200 が返ること |
| エラーログ | tail -n 50 /var/log/app/error.log | ERROR 行が増加していないこと |
| DB 接続 | アプリから DB へのクエリが成功すること | SELECT 1 が 1 ms 以内に返ること |
| 監視アラート | Datadog / CloudWatch でアラートが解消されること | 全アラートが Green |
⑤ ロールバック手順
作業が失敗または期待と異なる結果になった場合に元の状態へ戻す手順です。 メインの手順と同様に番号付きで記載し、「何があればロールバックを実行するか(判断基準)」も明示します。
## ロールバック手順
**実行判断基準:** 手順 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)
"""
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 プロセスが見つかりません")
② ログの自動テール(障害時の確認用)
"""
指定ログファイルの末尾 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 ヘルスチェックの自動実行
"""
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(演習)を組み合わせることで、常に機能するドキュメントになります。