夜中2時の電話が怖くなくなった。本番障害3回で学んだインシデント対応の現実

3年のSRE経験で気づいた、教科書と実務のズレ。オンコール地獄から抜け出した秘訣と、実際に機能した検出ルールの作り方を率直に話します。

夜中2時の電話が怖くなってた時代

うちのチームは2023年、インシデント対応をようやく「ちゃんと」始めようとしていた時期だった。その前は、本番がぶっ壊れたら Slack に「あ、死んだ」って書いて、デプロイ履歴から犯人探しする…という正気とは思えない運用をやってた。

ある金曜の夜23時、API のレイテンシが突然 3秒から 15秒に跳ね上がった。当時の僕たちに「原因特定」という概念はなく、とりあえず前のバージョンにロールバックして終わり。月曜に「何が起きたの?」って聞かれても、誰も覚えていない。

3回目の本番障害の時、さすがにこれはマズいと思った。でも、インシデント対応の「正解」を求めて本や記事を読んでも、実務と合わないことばかり。今から実践して気づいたコツを、率直に話そう。

オンコール制度、最初は地獄。でも仕組みが変わると現実が変わる

うちが導入したオンコール体制は正直、最初は失敗だった。理由は簡単—フォーカスが「誰が対応するか」に集中していて、「何を検出するか」という前段階がスッカスカだったから。

2024年、AWS の CloudWatch + SNS で基本的なアラートは設定していたけど、実際に来るのは誤検知だらけ。CPU 80% で急いで起きたら「夜間バッチが走ってるだけ」みたいなことばっかり。地味にストレスだった。

そこで気づいたのが SLI/SLO の設計。これは「本当にヤバい状態」を定義することが全てなんだよね。テキトーなアラート閾値じゃなく、ビジネスインパクトベースで考える。

実装例としては、こんな感じで定めた:

メトリクス閾値検出間隔月のエラーバジェット
API Response Time (p99)500ms5分1時間
Database Connection Pool80%1分30分
Error Rate1%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 つだ:

  1. Impact を数値で書く—「ユーザー 0.3% 影響」と書くと「お、大した事ないな」と分かる
  2. Timeline は細かく—何分で気づけたのか、何分で対応できたのか が可視化される
  3. Root Cause と Underlying Issue を分ける—その場の修正と本当の対策が混ざらない
  4. 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
  1. 判定フロー:
    • RDS slow query → Step A
    • CPU高い → Step B
    • Connection pool → Step C
    • 不明 → DB チームページング

Step A:RDS N+1 Query

  1. Bastion 経由で SSH
  2. Slow query log を確認:
    SELECT * FROM mysql.slow_log LIMIT 10;
  3. クエリパターンを特定
  4. 一時的な修正:インデックス作成またはクエリ最適化
  5. アプリチームにコードレビュー依頼

Step B:DB CPU 高い

  1. 長時間実行クエリを強制終了:
    mysql -e "SHOW PROCESSLIST;" | grep Sleep | awk '{print $1}' | xargs -I {} mysql -e "KILL {};"
  2. 過去 30分 のデプロイ確認
  3. マッチしたら:ロールバック開始

Step C:Connection Pool 枯渇

  1. Pool 使用率確認:
    mysql -e "SHOW STATUS WHERE variable_name='Threads_connected';"
  2. 90% 超えてたら:max_connections を +20%
  3. アプリチームに接続漏れの可能性を通知

これを 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(解決時間)"

実感値としては、こんな感じで変わった:

  1. 検出精度:誤検知が月 500 件 → 月 30 件。信号対雑音比が全く違う
  2. 対応速度:初回対応を 平均 20 分 → 8 分 に短縮。Runbook 整備の効果が大きい
  3. 再発率:同じ原因の障害が 3 ヶ月内に 3 回 → 0 回 に。ポストモーテムアクションをちゃんとやったから

特に 3 番目が大事だ。「同じ障害が起きない」って、本当に気持ちいい。

まとめ

インシデント対応は、技術と人と仕組みの 3 つのバランスが全て。教科書的には、こう見える:

カテゴリ具体例
技術異常検知アルゴリズム、Runbook、IaC ベースの復旧
オンコール制度の設計、スキル育成、心理的安全性
仕組みSLO/SLI の定義、ポストモーテム文化、自動化による誤検知削減

うちが最初に失敗したのは「技術だけで解決」と思ってたこと。正気とは思えない本番運用から脱出するには、地道な工夫が必要だった。

特に、オンコール者のメンタルヘルスを保つことを意識したら、組織全体の対応品質が劇的に上がったのは意外だった。これは個人の経験値じゃなく、組織としてのインシデント対応能力が高まった証拠。やっぱり人が疲弊してちゃ、いい判断なんてできないんだよね。

次のアクション:

  • SLO を定義していないなら、最初は 1 つだけ。例えば「API p99 latency 500ms」
  • アラート 100 件/月以上なら、Context-aware alerts を試す
  • ポストモーテムは書くけど実装されないなら、Action Items の Assign & Deadline を厳守する

最後に、個人的には「オンコール当番」という制度自体は必要だと思ってる。本番環境を預かってる以上、誰かが起きてないといけない。ただし、それが個人の搾取になるなら、組織として何かが間違ってる。その境界線を引く作業が、実は一番難しいんだと気づいた。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事