インシデントレスポンス — 顧客の本番環境で火を消す
午前2時、スマホが震えた
ソウタの枕元でスマホが振動した。午前2時13分。PagerDuty からのアラートだった。
[SEV-1] Nextera Financial - Arclight AI Platform - API Response Time > 30s, Error Rate 47%
ソウタは半年前に SaaS スタートアップ TechNova から、AI プラットフォーム企業 Arclight AI の FDE チームに転職したばかりだった。Nextera Financial は金融業界の大手顧客で、Arclight AI の推論 API を本番環境のリスク分析システムに組み込んでいる。
「落ち着け。まず状況を確認するんだ」
自分に言い聞かせながら、ソウタはラップトップを開いた。Slack の #incident-nextera チャンネルには既にメッセージが流れていた。
カイ (2:14 AM): @ソウタ 起きてるか。Nextera の本番が燃えてる。君が一番 Nextera の構成を知ってるから、一緒に対応しよう。War Room に入ってくれ。
カイは Arclight AI の FDE チームリードだ。10年以上のインシデント対応経験を持ち、どんな障害でも冷静さを失わない。ソウタにとって初めての本番 SEV-1 だった。
WARNING
FDE にとってインシデント対応は「自社プロダクトの障害」ではなく「顧客のビジネスの障害」だ。顧客の売上、信頼、エンドユーザーへの影響を常に意識する必要がある。
FDE がインシデントで発揮する固有の強み
ソウタが War Room の Zoom に接続すると、カイが既にタイムラインを整理していた。
「ソウタ、FDE がインシデント対応で求められる理由を覚えてるか?」
カイは障害の真っ只中でも、教えることを忘れなかった。
プロダクト知識の深さ
FDE はコードベースを熟知している。API のどのエンドポイントが何をしているか、内部のキューイング構造、レート制限の仕組み——通常のサポートエンジニアでは持ち得ない深度の知識がある。
ベンダーエンジニアリングへの直接アクセス
FDE はコアエンジニアリングチームに即座にエスカレーションできる。チケットを切って待つのではなく、Slack で直接コアチームの担当者を呼び出せる。
顧客コンテキストの理解
ソウタは Nextera Financial の環境構成を知っていた。推論 API をどのように呼び出しているか、どのモデルバージョンを使っているか、カスタム設定が何か——この知識がデバッグの速度を決定的に変える。
# ソウタは Nextera の構成を頭に入れていた
# config/customers/nextera_financial.yml に相当する知識
nextera_config = {
api_version: "v2.3",
model: "risk-analysis-v4",
max_concurrent_requests: 500,
timeout_seconds: 10,
custom_preprocessing: true, # 独自の前処理パイプライン
region: "ap-northeast-1",
dedicated_inference_pool: "nextera-pool-01"
}INFO
FDE の価値は「技術力 x 顧客コンテキスト」の掛け算にある。どちらか一方だけでは、インシデント対応の速度と精度は出せない。
重大度(Severity)分類
「まず重大度を確認しよう」とカイが言った。「今回は SEV-1 だ。全ユーザーに影響が出ている。」
インシデントの重大度は対応のスピードとリソース投入量を決定する。Arclight AI では以下の4段階で分類していた。
| 重大度 | 定義 | 初動時間 | エスカレーション | 対応体制 |
|---|---|---|---|---|
| SEV-1 | サービス全停止、全ユーザー影響、売上損失 | 15分以内 | VP + コアチーム即時 | War Room 常設 |
| SEV-2 | 主要機能停止、一部ユーザー影響 | 30分以内 | マネージャー通知 | 担当チーム集合 |
| SEV-3 | 軽微な機能障害、回避策あり | 4時間以内 | チームリード判断 | 担当者対応 |
| SEV-4 | 表示崩れ等、業務影響なし | 翌営業日 | 不要 | 通常チケット |
# app/models/incident.rb
class Incident < ApplicationRecord
enum :severity, {
sev1: 1, # 全面停止
sev2: 2, # 主要機能停止
sev3: 3, # 軽微な障害
sev4: 4 # 外観・軽微
}
enum :status, {
detected: 0,
acknowledged: 1,
investigating: 2,
identified: 3,
mitigating: 4,
resolved: 5,
postmortem: 6
}
belongs_to :customer
belongs_to :incident_commander, class_name: "User"
has_many :timeline_entries, dependent: :destroy
has_many :action_items, dependent: :destroy
validates :title, :severity, :customer, presence: true
def sla_deadline
base = detected_at || created_at
case severity
when "sev1" then base + 15.minutes
when "sev2" then base + 30.minutes
when "sev3" then base + 4.hours
when "sev4" then base + 24.hours
end
end
def sla_breached?
acknowledged_at.nil? && Time.current > sla_deadline
end
endWARNING
SEV-1 の SLA は 15 分。寝ぼけている暇はない。PagerDuty のアラートが鳴ったら、15分以内に状況確認と初動を完了させる必要がある。
エスカレーションの判断
エスカレーションの2つの経路
カイが War Room で説明した。「エスカレーションには2つの軸がある。上に上げるか、横に広げるかだ。」
階層エスカレーションは、マネジメントチェーンを上に辿る。リソースの追加投入や顧客への公式コミュニケーションが必要な場合に使う。
機能エスカレーションは、専門チームに横展開する。データベースの問題ならインフラチーム、推論モデルの問題なら ML チームに依頼する。
エスカレーションのトリガー
「いつエスカレーションするかの判断基準を持っておけ」とカイが言った。
# app/services/escalation_evaluator.rb
class EscalationEvaluator
TIME_LIMITS = { sev1: 30.minutes, sev2: 2.hours, sev3: 8.hours }.freeze
def initialize(incident)
@incident = incident
end
def should_escalate?
time_exceeded? || error_rate_exceeded? ||
affected_users_exceeded? || vip_customer_requested?
end
def escalation_reason
reasons = []
reasons << "SLA時間超過" if time_exceeded?
reasons << "エラー率閾値超過" if error_rate_exceeded?
reasons << "影響ユーザー数超過" if affected_users_exceeded?
reasons << "VIP顧客からの要求" if vip_customer_requested?
reasons.join(", ")
end
private
def time_exceeded?
limit = TIME_LIMITS[@incident.severity.to_sym]
limit && !@incident.resolved? && (Time.current - @incident.detected_at > limit)
end
def error_rate_exceeded?
@incident.customer.current_error_rate_percent > 50.0
end
def affected_users_exceeded?
@incident.affected_user_count > 1000
end
def vip_customer_requested?
@incident.customer.vip? && @incident.customer_escalation_requested?
end
endソウタは Nextera のダッシュボードを見た。エラー率 47%、影響ユーザー推定 3,200 人。複数のトリガーに該当していた。
Palantir の Delta/Echo チーム構造
「ここからが FDE の真骨頂だ」とカイが続けた。「Palantir の Delta/Echo 構造を使う。」
Palantir が編み出したインシデント対応の組織構造は、技術的な問題解決とコミュニケーション管理を明確に分離する。
Delta チーム(技術チーム)
Delta はデバッグと修正に集中する。ログを読み、コードを追い、仮説を立てて検証する。外部とのコミュニケーションは一切行わない。
Echo チーム(戦略チーム)
Echo は顧客コミュニケーション、社内への状況共有、経営層への報告を担う。Delta が技術に没頭できる環境を作る。
FDE は両方を橋渡しする
「普通のエンジニアは Delta だけ、普通のサポートは Echo だけ。FDE は両方の言語を話せる」とカイが言った。
ソウタは Delta として技術調査に入りつつ、顧客の技術責任者への説明も求められていた。FDE だからこそ、「今何が起きていて、いつ直る見込みか」を技術的根拠とともに伝えられる。
# app/models/incident_role.rb
class IncidentRole < ApplicationRecord
enum :role_type, {
incident_commander: 0, # 全体統括
delta_lead: 1, # 技術チームリード
delta_member: 2, # 技術調査メンバー
echo_lead: 3, # コミュニケーションリード
echo_member: 4, # コミュニケーション担当
fde_bridge: 5 # FDE(Delta/Echo 橋渡し)
}
belongs_to :incident
belongs_to :user
def can_update_customer?
echo_lead? || echo_member? || fde_bridge?
end
def can_deploy_fix?
delta_lead? || delta_member? || fde_bridge?
end
endINFO
Delta/Echo の分離により、技術者が顧客対応に時間を取られて調査が遅れる「割り込み地獄」を防げる。FDE はこの両方を状況に応じて切り替えられる稀有な存在だ。
体系的デバッグ手法
カイがソウタに問いかけた。「よし、Delta モードだ。最初に何をする?」
「ログを見ます」
「違う。最初にやるのは再現だ。」
Step 1: 再現(Replication)
問題を自分の手で再現する。再現できなければ、何を直しているのかわからない。
# bin/incident_reproduce.rb — 再現テストスクリプト
customer = Customer.find_by!(slug: "nextera-financial")
api_client = ArclightApi::Client.new(customer:, environment: :production)
10.times do |i|
start = Time.current
begin
api_client.predict(model: "risk-analysis-v4", input: sample_risk_payload)
puts "[#{i + 1}] 成功 - #{(Time.current - start).round(2)}s"
rescue ArclightApi::TimeoutError, ArclightApi::ServerError => e
puts "[#{i + 1}] 失敗 - #{(Time.current - start).round(2)}s - #{e.class}"
end
end
# => 10回中6回タイムアウト、2回サーバーエラー。再現率80%結果は明白だった。10回中6回がタイムアウト、2回がサーバーエラー。再現率80%。
Step 2: 分離(Isolation)
「再現できた。次は範囲を絞る。他の顧客でも起きてるか?」
# 他の顧客の API レスポンスタイムを確認
aws cloudwatch get-metric-statistics \
--namespace "ArclightAI/API" \
--metric-name "ResponseTime" \
--dimensions Name=Customer,Value=nextera-financial \
--start-time "2026-06-28T16:00:00Z" \
--end-time "2026-06-28T17:15:00Z" \
--period 300 \
--statistics Average Maximum \
--region ap-northeast-1他の顧客のレスポンスタイムは正常だった。問題は Nextera Financial に限定されている。さらに絞り込む——推論プール nextera-pool-01 に問題がある。
Step 3: 分割統治(Divide and Conquer)
システムを半分に切って、問題がどちら側にあるか特定する。
Step 4: 仮説検証(Hypothesis Testing)
「仮説: Nextera 専用推論プールの GPU ノードでモデルキャッシュがエビクションされ、毎回コールドロードが発生している」
# app/services/inference_pool_diagnostics.rb
class InferencePoolDiagnostics
def initialize(pool_id)
@pool_id = pool_id
@cw = Aws::CloudWatch::Client.new(region: "ap-northeast-1")
end
def check_model_cache_hit_rate
latest = fetch_metric("ModelCacheHitRate", ["Average"])
{ cache_hit_rate: latest&.average&.round(2), healthy: latest&.average.to_f > 80.0 }
end
def check_gpu_memory_usage
latest = fetch_metric("GPUMemoryUsage", ["Average", "Maximum"])
{ avg_usage: latest&.average&.round(2), max_usage: latest&.maximum&.round(2) }
end
private
def fetch_metric(name, stats)
resp = @cw.get_metric_statistics(
namespace: "ArclightAI/Inference", metric_name: name,
dimensions: [{ name: "PoolId", value: @pool_id }],
start_time: 2.hours.ago, end_time: Time.current,
period: 300, statistics: stats
)
resp.datapoints.max_by(&:timestamp)
end
end
# 実行結果
diag = InferencePoolDiagnostics.new("nextera-pool-01")
diag.check_model_cache_hit_rate # => { cache_hit_rate: 12.3, healthy: false }
diag.check_gpu_memory_usage # => { avg_usage: 97.8, max_usage: 99.2 }ビンゴだった。キャッシュヒット率が 12%——通常は 95% 以上あるべき値だ。GPU メモリ使用率が 97% を超えていた。
「原因がわかりました」とソウタが報告した。「昨日のデプロイで Nextera のカスタム前処理パイプラインが追加のメモリを消費するようになり、GPU メモリが逼迫してモデルキャッシュがエビクションされています。」
修正と緩和
カイが頷いた。「よくやった。じゃあ緩和策を打とう。恒久対策は後だ。まず火を消す。」
# 緩和策: 推論プールの GPU メモリ上限を一時的に拡大
# bin/incident_mitigate.rb
# Step 1: 推論プールのスケールアウト
ecs_client = Aws::ECS::Client.new(region: "ap-northeast-1")
ecs_client.update_service(
cluster: "arclight-inference",
service: "nextera-pool-01",
desired_count: 4 # 2 → 4 にスケールアウト
)
# Step 2: カスタム前処理のメモリ制限を設定
config_client = ArclightConfig::Client.new
config_client.update_pool_config(
pool_id: "nextera-pool-01",
preprocessing_memory_limit_mb: 2048 # 制限なし → 2GB に制限
)
# Step 3: モデルキャッシュのウォームアップ
warmup_client = ArclightApi::WarmupClient.new
warmup_client.warmup_model(
pool_id: "nextera-pool-01",
model: "risk-analysis-v4"
)
puts "緩和策を適用しました。メトリクスの回復を監視します。"5分後、エラー率が 47% から 3% に低下した。レスポンスタイムも正常値に戻った。
「Nextera の技術責任者に状況を報告してくれ」とカイが言った。「これが Echo の仕事だ。」
ソウタは顧客向けの状況報告を書いた。技術的な根本原因を、ビジネスインパクトの文脈で説明する——FDE ならではの仕事だった。
"砂利道から舗装道路へ" フィードバックループ
翌日、カイがソウタに話しかけた。
「昨夜の対応は手動でやったよな。次に同じことが起きたらどうする?」
「同じ手順を……あ、自動化すべきですね。」
「そうだ。FDE のインシデント対応は、プロダクト改善のインプットになる。これを"砂利道から舗装道路へ"と呼んでいる。」
今回のインシデントから生まれたプロダクト改善:
- 手動修正: ソウタが深夜に手動でスケールアウトとキャッシュウォームアップを実行
- Runbook 文書化: 手順を Confluence に記録
- 自動化: GPU メモリ逼迫時の自動スケールアウトスクリプトを作成
- プロダクト化: メモリ使用量の自動監視とオートスケーリングを推論プラットフォームの機能として実装
INFO
FDE が現場で繰り返し行う手動作業は、プロダクトの機能ギャップを示している。インシデントは「バグ」ではなく「プロダクトへのフィードバック」として扱う。
ポストモーテムを書く
インシデントが解決した翌日、カイがソウタに言った。
「さて、一番大事な仕事が残ってる。ポストモーテムだ。」
ポストモーテムの原則
ポストモーテムは非難ではなく学習のために書く。「誰が悪いか」ではなく「システムのどこが弱かったか」を明らかにする。
5 Whys による根本原因分析
「なぜ」を5回繰り返して、表面的な原因から根本原因にたどり着く。
- なぜ API がタイムアウトした? → 推論のレスポンスが遅かった
- なぜ推論が遅かった? → モデルが毎回コールドロードされていた
- なぜコールドロードされた? → GPU メモリ不足でキャッシュがエビクションされた
- なぜ GPU メモリが不足した? → カスタム前処理がメモリ制限なしで動作していた
- なぜメモリ制限がなかった? → 前処理パイプラインにリソース制限の仕組みがなかった
根本原因: カスタム前処理パイプラインにリソースガバナンスが未実装だった
タイムライン形式のポストモーテム
# app/services/postmortem_builder.rb
class PostmortemBuilder
def initialize(incident)
@incident = incident
end
def generate
{
title: @incident.title,
severity: @incident.severity,
duration: format_duration,
timeline: build_timeline,
root_cause: @incident.root_cause_description,
five_whys: @incident.five_whys_entries.order(:level).map { |e|
{ level: e.level, question: e.question, answer: e.answer }
},
action_items: @incident.action_items.map { |item|
{ action: item.description, owner: item.assignee.name,
priority: item.priority, deadline: item.due_date }
}
}
end
private
def build_timeline
@incident.timeline_entries.order(:occurred_at).map do |entry|
{ time: entry.occurred_at.strftime("%H:%M JST"),
event: entry.description, actor: entry.user&.name || "System" }
end
end
def format_duration
seconds = @incident.resolved_at - @incident.detected_at
"#{(seconds / 3600).floor}時間#{((seconds % 3600) / 60).floor}分"
end
endソウタが書いたポストモーテムの Action Items:
| アクション | 担当 | 優先度 | 期限 | チケット |
|---|---|---|---|---|
| 前処理パイプラインにメモリ制限を実装 | MLチーム 田中 | P0 | 7/5 | ENG-4521 |
| GPU メモリ使用率の閾値アラート追加 | インフラ 佐藤 | P0 | 7/3 | OPS-1287 |
| 推論プールの自動スケーリング実装 | プラットフォーム 鈴木 | P1 | 7/12 | ENG-4522 |
| Nextera 向け Runbook を更新 | FDE ソウタ | P1 | 7/1 | FDE-892 |
| 前処理リソース制限の設計ドキュメント作成 | MLチーム 田中 | P2 | 7/10 | ENG-4523 |
WARNING
Action Items に担当者と期限がないポストモーテムは意味がない。「改善する」ではなく「誰が」「いつまでに」「何を」するかを明記する。
AWS モニタリングパイプラインの構築
インシデントの教訓を受けて、ソウタは Nextera 環境のモニタリングを強化した。
CloudWatch カスタムメトリクスの送信
# app/services/monitoring/cloudwatch_reporter.rb
class Monitoring::CloudwatchReporter
def initialize
@client = Aws::CloudWatch::Client.new(region: "ap-northeast-1")
@namespace = "ArclightAI/CustomerMetrics"
end
def report_api_latency(customer_slug:, endpoint:, latency_ms:)
put_metric("APILatency", [
{ name: "Customer", value: customer_slug },
{ name: "Endpoint", value: endpoint }
], latency_ms, "Milliseconds")
end
def report_error_rate(customer_slug:, error_count:, total_count:)
rate = total_count.positive? ? (error_count.to_f / total_count * 100) : 0.0
put_metric("ErrorRate", [
{ name: "Customer", value: customer_slug }
], rate, "Percent")
end
private
def put_metric(metric_name, dimensions, value, unit)
@client.put_metric_data(
namespace: @namespace,
metric_data: [{
metric_name: metric_name, dimensions: dimensions,
timestamp: Time.current, value: value, unit: unit
}]
)
end
endCloudWatch Alarms の設定
# lib/tasks/monitoring/setup_alarms.rake
namespace :monitoring do
desc "顧客環境のCloudWatch Alarmsを設定"
task setup_alarms: :environment do
cloudwatch = Aws::CloudWatch::Client.new(region: "ap-northeast-1")
sns_arn = ENV.fetch("PAGERDUTY_SNS_TOPIC_ARN")
Customer.active.find_each do |customer|
# エラー率アラーム(5分平均が10%超過 × 2回連続でトリガー)
cloudwatch.put_metric_alarm(
alarm_name: "#{customer.slug}-error-rate-high",
namespace: "ArclightAI/CustomerMetrics",
metric_name: "ErrorRate",
dimensions: [{ name: "Customer", value: customer.slug }],
statistic: "Average", period: 300,
evaluation_periods: 2, threshold: 10.0,
comparison_operator: "GreaterThanThreshold",
alarm_actions: [sns_arn]
)
# P99 レイテンシアラーム(5秒超過 × 3回連続)
cloudwatch.put_metric_alarm(
alarm_name: "#{customer.slug}-latency-high",
namespace: "ArclightAI/CustomerMetrics",
metric_name: "APILatency",
dimensions: [{ name: "Customer", value: customer.slug }],
statistic: "p99", period: 300,
evaluation_periods: 3, threshold: 5000.0,
comparison_operator: "GreaterThanThreshold",
alarm_actions: [sns_arn]
)
end
end
endインシデント自動起票
# app/services/incident_auto_creator.rb
class IncidentAutoCreator
def self.from_cloudwatch_alarm(alarm_message)
parsed = JSON.parse(alarm_message)
customer = Customer.find_by!(slug: extract_customer_slug(parsed))
incident = Incident.create!(
title: "#{customer.name}: #{parsed['AlarmDescription']}",
severity: determine_severity(parsed),
status: :detected, customer: customer,
detected_at: Time.current, source: "cloudwatch_alarm"
)
incident.timeline_entries.create!(
occurred_at: Time.current,
description: "CloudWatch Alarm トリガー: #{parsed['AlarmName']}",
entry_type: :automated
)
PagerdutyNotifier.trigger(
severity: incident.severity, summary: incident.title,
source: "ArclightAI Monitoring", component: customer.slug
)
incident
end
private
def self.determine_severity(data)
case data.dig("Trigger", "MetricName")
when "ErrorRate" then data.dig("Trigger", "Threshold") >= 50 ? :sev1 : :sev2
when "APILatency" then data.dig("Trigger", "Threshold") >= 30_000 ? :sev1 : :sev2
else :sev3
end
end
def self.extract_customer_slug(data)
data.dig("Trigger", "Dimensions")&.find { |d| d["name"] == "Customer" }&.dig("value")
end
endエピローグ — 火を消した夜に学んだこと
ポストモーテムを書き上げたソウタに、カイが声をかけた。
「初めての SEV-1、どうだった?」
「正直、最初は手が震えました。でも、手順があると落ち着けますね。重大度を分類して、エスカレーションの判断基準を持って、Delta/Echo で役割を分けて、体系的にデバッグする。フレームワークがあるだけで、パニックにならずに済みました。」
カイが微笑んだ。「そうだ。インシデント対応は度胸じゃない。プロセスだ。そしてこの経験が、FDE を普通のエンジニアから分ける。」
ソウタは振り返った。TechNova で Rails を書いていた頃、障害は怖いものだった。しかし FDE として顧客の本番環境で火を消す経験は、恐怖を能力に変えてくれた。
「あと、もう一つ大事なことがある」とカイが付け加えた。「今夜の対応で、Nextera の技術責任者から Slack で直接メッセージが来てたぞ。"ソウタさんの対応のおかげで安心できました"って。」
顧客の信頼は、機能のデモや提案書では築けない。深夜2時に一緒に火を消した経験が、最も強固な信頼関係を作る。それが FDE のインシデントレスポンスだ。
INFO
インシデントは FDE にとって最大の信頼構築の機会でもある。冷静に、体系的に、誠実に対応することで、顧客との関係は障害前より強くなる。