夜中2時の電話が怖くなくなった。本番障害3回で学んだインシデント対応の現実
3年のSRE経験で気づいた、教科書と実務のズレ。オンコール地獄から抜け出した秘訣と、実際に機能した検出ルールの作り方を率直に話します。
夜中2時の電話が怖くなってた時代
うちのチームは2023年、インシデント対応をようやく「ちゃんと」始めようとしていた時期だった。その前は、本番がぶっ壊れたら Slack に「あ、死んだ」って書いて、デプロイ履歴から犯人探しする…という正気とは思えない運用をやってた。
ある金曜の夜23時、API のレイテンシが突然 3秒から 15秒に跳ね上がった。当時の僕たちに「原因特定」という概念はなく、とりあえず前のバージョンにロールバックして終わり。月曜に「何が起きたの?」って聞かれても、誰も覚えていない。
3回目の本番障害の時、さすがにこれはマズいと思った。でも、インシデント対応の「正解」を求めて本や記事を読んでも、実務と合わないことばかり。今から実践して気づいたコツを、率直に話そう。
オンコール制度、最初は地獄。でも仕組みが変わると現実が変わる
うちが導入したオンコール体制は正直、最初は失敗だった。理由は簡単—フォーカスが「誰が対応するか」に集中していて、「何を検出するか」という前段階がスッカスカだったから。
2024年、AWS の CloudWatch + SNS で基本的なアラートは設定していたけど、実際に来るのは誤検知だらけ。CPU 80% で急いで起きたら「夜間バッチが走ってるだけ」みたいなことばっかり。地味にストレスだった。
そこで気づいたのが SLI/SLO の設計。これは「本当にヤバい状態」を定義することが全てなんだよね。テキトーなアラート閾値じゃなく、ビジネスインパクトベースで考える。
実装例としては、こんな感じで定めた:
| メトリクス | 閾値 | 検出間隔 | 月のエラーバジェット |
|---|---|---|---|
| API Response Time (p99) | 500ms | 5分 | 1時間 |
| Database Connection Pool | 80% | 1分 | 30分 |
| Error Rate | 1% | 5分 | 2時間 |
重要なのは「エラーバジェット」を決めること。月 1時間 までなら障害があってもいい、という考え方だ。これを 30% 消費したら alert を鳴らす仕組みにした。
この違いが本当にデカい。アラートの鳴る数が 月200件 → 月8件 に落ちたし、その8件は全部「本当に起動する価値がある」ものになった。夜中に起きる回数が劇的に減ったのは、精神衛生上かなり良かった。
ポストモーテムの書き方。5W1H で時間溶かすな
最初、うちのポストモーテムはカオスだった。Timeline には「23:15 API slow / 23:45 rollback / 00:10 restored」だけ。Root Cause は「database connection pool exhausted」一行。Action Items は「monitoring を改善しよう」なんてふんわりしたまま。
2ヶ月後、同じ障害が 2回 起きた。「あ、これ対策されてないんだ」って気づいた時の絶望感、半端なかった。
今は、Google の SRE Book を参考に、構成をガラッと変えた。
## Incident Summary
- **Duration**: 23:15 - 00:45 (1h 30m)
- **Impact**: API p99 latency 500ms → 3.5s / ユーザー 0.3% に影響
- **Error Budget Consumed**: 月間バジェットの 15% 消費
## Timeline(詳細版)
23:14 - RDS connection pool アラート発火(75%)
23:15 - API チームオンコール者が対応開始(検出遅延 3 分)
23:18 - データベースチーム合流。接続状態の調査開始
23:25 - 原因特定:user search エンドポイントで N+1 クエリ(先週のデプロイで混入)
23:35 - 一時的な回避策:max_connections を +50
23:45 - デプロイをロールバック
23:58 - connection pool が正常化
00:10 - 復旧の動作確認完了
## Root Cause Analysis
- **直接原因**:user search エンドポイントの N+1 クエリ
- 期待:1 + ユーザー数 のクエリ
- 実際:1 + ユーザー数 + フレンド数 のクエリ
- **根本原因**:コードレビューで自動化されたクエリ分析がない
- **検出漏れの理由**:ロードテストが 100 同時実行(本番ピーク:2000)でしか検証してなかった
## 解決策
| 対策 | 期限 | 担当 | 優先度 |
|-----|------|------|--------|
| 短期:CI/CD にクエリ分析を組み込む | 2026-07-20 | @alice | P0 |
| 中期:ロードテスト基準を 5000 同時実行に引き上げ | 2026-07-25 | @bob | P1 |
| 長期:ステージング環境に APM 統合して slow query を自動検出 | 2026-07-18 | @charlie | P1 |
何が変わったか。重要なポイントは 4 つだ:
- Impact を数値で書く—「ユーザー 0.3% 影響」と書くと「お、大した事ないな」と分かる
- Timeline は細かく—何分で気づけたのか、何分で対応できたのか が可視化される
- Root Cause と Underlying Issue を分ける—その場の修正と本当の対策が混ざらない
- Action Items は必ず assign と deadline をセット—書くだけじゃ意味ない
この構成にしたら、2回目以降は同じ障害が起きなくなった。
自動検出。アラートを「減らす」が実は最難関
ここからが本当に難しい話だ。うちが 2025年に導入したのは、CloudWatch + Lambda + Slack 連携で、異常検知を自動化する仕組み。
# 異常検知 Lambda の実装例
import boto3
from datetime import datetime, timedelta
import json
cloudwatch = boto3.client('cloudwatch')
sns = boto3.client('sns')
def lambda_handler(event, context):
# 過去1時間のメトリクス取得
response = cloudwatch.get_metric_statistics(
Namespace='AWS/RDS',
MetricName='DatabaseConnections',
Dimensions=[{'Name': 'DBInstanceIdentifier', 'Value': 'prod-db'}],
StartTime=datetime.utcnow() - timedelta(hours=1),
EndTime=datetime.utcnow(),
Period=60,
Statistics=['Average', 'Maximum']
)
datapoints = response['Datapoints']
# 異常検知:標準偏差ベース
if len(datapoints) > 30:
values = [dp['Average'] for dp in datapoints]
mean = sum(values) / len(values)
variance = sum((x - mean) ** 2 for x in values) / len(values)
stddev = variance ** 0.5
latest = values[-1]
# 過去30分の平均 + 3σ を超えたら alert
threshold = mean + (3 * stddev)
if latest > threshold:
message = {
'text': f'🚨 DB Connection 異常検知',
'blocks': [
{
'type': 'section',
'text': {
'type': 'mrkdwn',
'text': f'現在値: {latest:.0f}\n閾値: {threshold:.0f}\n平均: {mean:.0f}'
}
}
]
}
sns.publish(
TopicArn='arn:aws:sns:ap-northeast-1:xxx:oncall-alerts',
Message=json.dumps(message)
)
return {'statusCode': 200, 'body': 'Alert sent'}
return {'statusCode': 200, 'body': 'No anomaly'}
悪くはないんだけど、本当の課題は違う場所にあった。導入1ヶ月で、オンコールの人は正気を失いかけた。
理由はシンプル:
- 夜間バッチが走る時間帯は「異常」と判定される
- 定時メンテナンス中も「異常」
- 新規ユーザー増加キャンペーン中も「異常」
結果、アラートが来ても誰も見なくなった。いわゆる Boy Who Cried Wolf 現象だ。
改善策は Context-aware alerts。つまり「何が正常か」をちゃんと定義すること。
# 改善版:Context を考慮した異常検知
def is_maintenance_window():
"""定時メンテナンス時間か判定"""
now = datetime.now()
# 毎週月曜 02:00-04:00 はメンテナンス
if now.weekday() == 0 and 2 <= now.hour < 4:
return True
return False
def get_baseline(metric_name, hours=24):
"""過去24時間の正常なベースラインを取得"""
response = cloudwatch.get_metric_statistics(
Namespace='Custom/Anomaly',
MetricName=f'{metric_name}_baseline', # 事前に記録した正常値
StartTime=datetime.utcnow() - timedelta(hours=hours),
EndTime=datetime.utcnow(),
Period=300,
Statistics=['Average']
)
return response['Datapoints']
def lambda_handler(event, context):
if is_maintenance_window():
return {'statusCode': 200, 'body': 'Maintenance window, skip'}
current_metric = get_current_metric('DatabaseConnections')
baseline = get_baseline('DatabaseConnections')
# baseline の 3σ を上回ったときだけ alert
mean = sum(dp['Average'] for dp in baseline) / len(baseline)
stddev = calculate_stddev(baseline)
if current_metric > mean + (3 * stddev):
send_alert(f'メトリクス: {current_metric}, 閾値: {mean + 3*stddev}')
これで誤検知は 8割 減った。正直、この改善は大きかった。
オンコール者の心折れないために実装した仕組み
技術的な話だけじゃなく、人の部分がめちゃ大事。オンコール制度も「仕組み」がないと地獄になる。
うちが 2025年に導入したのが、以下の 3 つの施策だ。
1. 予測可能なオンコール時間
人員A: 月・火・水(09:00-18:00)+ 木夜(18:00-翌09:00)
人員B: 木・金・土(09:00-18:00)+ 日夜(18:00-翌09:00)
人員C: 日・月・火(09:00-18:00)+ 水夜(18:00-翌09:00)
※ 夜間は 30分 以内に対応できる体制を常に 2名
これだけで「今週は誰が当番?」という混乱がなくなる。
2. オンコール手当 + 実績に基づく報酬
Base: 月額 3万円(オンコール参加手当)
Incident Response:
- 15分以内対応: +5,000円
- 15-30分: +3,000円
- 30分+: +1,000円
RCA Quality: ポストモーテムが実装につながったら +5,000円
「報酬」で解決するわけではないけど、「オンコールはコスト」という認識から「オンコールはスキル」に変わる。これは心理的に大きい。
3. 実行型 Runbook の整備
最後に、実装で一番大事なのが 実行型 Runbook。単なるドキュメントじゃなく、実際に動かすことを前提にした手順書だ。
## [Runbook] API Latency 高い
### Detection
- Alert: API p99 latency > 500ms for 5分
- Severity: SEV2
### Immediate Action(5分以内)
1. CloudWatch ダッシュボード確認:api-health-2026
2. 診断コマンド実行:
```bash
./scripts/incident-diagnosis.sh api
- 判定フロー:
- RDS slow query → Step A
- CPU高い → Step B
- Connection pool → Step C
- 不明 → DB チームページング
Step A:RDS N+1 Query
- Bastion 経由で SSH
- Slow query log を確認:
SELECT * FROM mysql.slow_log LIMIT 10; - クエリパターンを特定
- 一時的な修正:インデックス作成またはクエリ最適化
- アプリチームにコードレビュー依頼
Step B:DB CPU 高い
- 長時間実行クエリを強制終了:
mysql -e "SHOW PROCESSLIST;" | grep Sleep | awk '{print $1}' | xargs -I {} mysql -e "KILL {};" - 過去 30分 のデプロイ確認
- マッチしたら:ロールバック開始
Step C:Connection Pool 枯渇
- Pool 使用率確認:
mysql -e "SHOW STATUS WHERE variable_name='Threads_connected';" - 90% 超えてたら:max_connections を +20%
- アプリチームに接続漏れの可能性を通知
これを Confluence + Slack bot で検索可能にすると、初心者オンコール者でも 5分で対応できるようになる。
## 2026年のインシデント対応、この1年で変わったこと
```mermaid
xychart-beta
title インシデント検出から解決までの時間推移
x-axis [2024-01, 2024-04, 2024-07, 2024-10, 2025-01, 2025-04, 2025-07]
y-axis "時間(分)" 0 --> 50
line [25, 22, 18, 15, 12, 9, 6] title "MTTD(検出時間)"
line [45, 40, 32, 28, 22, 18, 14] title "MTTR(解決時間)"
実感値としては、こんな感じで変わった:
- 検出精度:誤検知が月 500 件 → 月 30 件。信号対雑音比が全く違う
- 対応速度:初回対応を 平均 20 分 → 8 分 に短縮。Runbook 整備の効果が大きい
- 再発率:同じ原因の障害が 3 ヶ月内に 3 回 → 0 回 に。ポストモーテムアクションをちゃんとやったから
特に 3 番目が大事だ。「同じ障害が起きない」って、本当に気持ちいい。
まとめ
インシデント対応は、技術と人と仕組みの 3 つのバランスが全て。教科書的には、こう見える:
| カテゴリ | 具体例 |
|---|---|
| 技術 | 異常検知アルゴリズム、Runbook、IaC ベースの復旧 |
| 人 | オンコール制度の設計、スキル育成、心理的安全性 |
| 仕組み | SLO/SLI の定義、ポストモーテム文化、自動化による誤検知削減 |
うちが最初に失敗したのは「技術だけで解決」と思ってたこと。正気とは思えない本番運用から脱出するには、地道な工夫が必要だった。
特に、オンコール者のメンタルヘルスを保つことを意識したら、組織全体の対応品質が劇的に上がったのは意外だった。これは個人の経験値じゃなく、組織としてのインシデント対応能力が高まった証拠。やっぱり人が疲弊してちゃ、いい判断なんてできないんだよね。
次のアクション:
- SLO を定義していないなら、最初は 1 つだけ。例えば「API p99 latency 500ms」
- アラート 100 件/月以上なら、Context-aware alerts を試す
- ポストモーテムは書くけど実装されないなら、Action Items の Assign & Deadline を厳守する
最後に、個人的には「オンコール当番」という制度自体は必要だと思ってる。本番環境を預かってる以上、誰かが起きてないといけない。ただし、それが個人の搾取になるなら、組織として何かが間違ってる。その境界線を引く作業が、実は一番難しいんだと気づいた。