SLO設計で2年失敗した僕が、ユーザージャーニーで運用を軌道に乗せた話
SLOを導入したのに誰も見ない、チームが納得しない。その原因は教科書的な設計にありました。2年の失敗を経て辿り着いた、ユーザー視点のSLO設計と実装方法を共有します。
SLOが機能しなかった理由と、本当に効く設計方法
先日、うちのチームで「またSLOが無視されてる」という状況に直面してしまった。導入から2年経つのに、エンジニアは相変わらずリリースのタイミングでSLOを気にしない。マネージャーは「SLOってそんなに大事?」みたいな顔をしている。その時に気づいたんだ——僕たちのSLO設計がそもそも間違ってたんだ、と。
なぜSLOが機能しなかったのか
ぶっちゃけ、最初のSLO設計は教科書通りだった。「99.9%の可用性を目指します」「レイテンシP99は200ms」みたいな数字を決めて、Prometheusで監視して…終わり。誰も見ない。チームが納得してない。
理由は単純だ。ユーザーの視点が欠けてたんだ。
うちのプロダクトは受注管理システム。営業チームが朝8時に使い始める。営業が「昨日の夜メール来た顧客情報を入力したい」って時に、システムが落ちていたら最悪だ。でも午後2時の落ちなら、営業はすでに別の営業ツール使ってデータ入力を進めてる。影響が全然違う。
そのユーザージャーニーを無視して「システム全体で99.9%」なんて決めてた。そりゃ機能するわけないよ。
# 昔のダメなSLO設定
apilatency:
slo: p99 < 200ms
description: "とにかく速い必要がある"
availability:
slo: 99.9%
description: "ほぼ常に動いてる状態"
現実はこんな単純じゃない。この設定の問題は、営業が実際に困る時間帯と、困らない時間帯の区別がないってこと。ユーザーからするとシステムの価値は時間帯で全く違う。
ユーザージャーニーベースのSLO設計
で、去年から試してるのが「ユーザージャーニー単位でSLOを分解する」という方法だ。
うちの営業管理システムだと、主要なカスタマージャーニーは3つ。営業へのインタビューとアクセスログの分析で、こんな風に分類した。
| ジャーニー | 時間帯 | ユーザー数 | ビジネス影響 | 優先度 |
|---|---|---|---|---|
| 朝の既存顧客情報検索・更新 | 08:00-12:00 | 50人 | 営業の工数ロスが直結 | 高 |
| 夕方の日報確認・承認 | 16:00-19:00 | 15人 | 次の日の活動に影響 | 中 |
| 夜間バッチ分析・エクスポート | 22:00-02:00 | 3人 | 数時間の遅延は許容可 | 低 |
重要なのは、それぞれのジャーニーで求められる信頼性が全く違うってことだ。正直、朝の営業フローが止まったら営業が怒るし会社の売上に響く。でも、夜間のバッチが30分遅延しても誰も困らない。なのに同じSLOで管理してたんだから、そりゃ誰も気にしなくなるよ。
これぞれに対して、SLIを設計する。
# ユーザージャーニー単位のSLI設計
journeys:
morning_data_entry:
name: "朝の営業データ入力フロー"
time_window: "08:00-12:00"
slis:
- api_availability:
definition: "GET /api/customers のHTTP 2xx応答率"
target: "99.95%"
rationale: "50人の営業が同時アクセス。1人が困ると営業効率-2%"
- search_latency:
definition: "GET /api/customers search p95レイテンシ"
target: "<500ms"
rationale: "営業マンの心理的許容は500ms程度。それ以上は『遅い』と感じる"
- update_consistency:
definition: "POST /api/customers/{id} の成功率"
target: "99.99%"
rationale: "営業データの喪失はビジネス損失。金銭的影響は大きい"
evening_approval:
name: "夕方の日報確認フロー"
time_window: "16:00-19:00"
slis:
- api_availability:
definition: "GET /api/reports のHTTP 2xx応答率"
target: "99.5%"
rationale: "15人が確認。完全に落ちても翌日に持ち越し可能。許容度高い"
- approval_latency:
definition: "POST /api/reports/{id}/approve p90レイテンシ"
target: "<1000ms"
rationale: "承認操作は頻度低い。多少遅くてもOK。UXは許容度高い"
batch_analysis:
name: "夜間バッチ分析フロー"
time_window: "22:00-02:00"
slis:
- batch_completion_time:
definition: "バッチジョブの完了時間"
target: "<30分"
rationale: "2時までに完了すれば朝の報告に間に合う。夜間は影響が小さい"
- data_export_success:
definition: "エクスポート成功率"
target: "95%"
rationale: "失敗してもS3に再実行ログがあれば対応可能"
これをやってから、チームの空気が変わった。営業が朝8時に「システムが遅い」って言ったら、これって本当に重要なSLIの違反なのか?ってすぐに判断できるようになった。
そして夜間のバッチ処理が5分遅れても「あ、これはSLO外の時間帯だから、緊急対応の優先度は低い」って判断できるようになった。スプリント計画も明確になったし、何より営業部門も「自分たちが大事な時間帯」を認識して、デバッグを手伝ってくれるようになった。地味だけど、これが本当に大きい変化だった。
コンポジットSLOで複数コンポーネントを管理
でもここからが本当の課題だ。各ジャーニーは複数のマイクロサービスで構成されてる。
朝の営業データ入力フロー(availability: 99.95%)を実現するには、下図の通り4つのコンポーネントが関係する。
graph TB
subgraph "Morning Data Entry Flow"
api["API Gateway<br/>target: 99.99%"]
auth["Auth Service<br/>target: 99.98%"]
db["PostgreSQL RDS<br/>target: 99.95%"]
cache["Redis Cache<br/>target: 99.9%"]
end
client["Salesforce App<br/>50 users 08:00-12:00"] -->|login| auth
auth -->|validate| api
api -->|query| cache
cache -->|miss| db
style api fill:#e1f5ff
style auth fill:#fff3e0
style db fill:#f3e5f5
style cache fill:#e8f5e9
ここで重要なのは「コンポジットSLO」の計算方法だ。シリーズ構成なら、各コンポーネントのAvailabilityを掛け算する。
総合availability = 99.99% × 99.98% × 99.95% × 99.9% = 99.82%
あ、99.95%には届かない。ダメじゃん。
そこで必要な各コンポーネントの信頼性を逆算する。総合99.95%を達成するには?
# Pythonで逆算
import math
target_slo = 0.9995 # 99.95%
num_components = 4
# 均等配分する場合
per_component = target_slo ** (1 / num_components)
print(f"Each component needs: {per_component * 100:.3f}%")
# Output: Each component needs: 99.987%
# ただし、実際には重要度で振る
# 重要度: Auth > API > DB > Cache
weights = {
'auth': 0.4, # 40%の負担
'api': 0.3, # 30%の負担
'db': 0.2, # 20%の負担
'cache': 0.1 # 10%の負担
}
# 加重配分で計算
required = {
'auth': target_slo ** weights['auth'],
'api': target_slo ** weights['api'],
'db': target_slo ** weights['db'],
'cache': target_slo ** weights['cache']
}
for service, slo in required.items():
print(f"{service}: {slo * 100:.4f}%")
# Output:
# auth: 99.998%
# api: 99.985%
# db: 99.970%
# cache: 99.901%
こうやって「Auth Serviceは4個ナインが必要」って決まる。その時点で、Auth Serviceのチームに「可用性が本当に重要。クラスタリングとフェイルオーバーは必須」って共有できる。
Cache(Redis)は3個ナイン(99.9%)で十分って判定できるから「こっちは若干ザルでもいい。オンメモリなので再起動も30秒。むしろ時々落ちても許容される仕様にしよう」って割り切れるんだ。
エラーバジェットをスプリント計画に組み込む
ここまで来ると、エラーバジェットが本当に機能し始める。
99.95% SLOということは、1ヶ月(720時間)のうち、許容される停止時間は?
停止時間 = 720時間 × (1 - 0.9995) = 720 × 0.0005 = 0.36時間 = 21.6分
1ヶ月でたった21分。秒数で表すと1296秒しかない。
これをスプリントに割り当てる。うちは2週間スプリントなので、2週間(336時間)だと約10分のエラーバジェット。
# Sprint 計画での使い方
week_1-2:
error_budget: 10.8分(9.7秒/日)
planned_deployments:
- auth_service_upgrade: "5分のダウンタイム予定"
- db_migration: "2分のロック予定"
remaining_budget: "3.8分"
implication: |
「あと3.8分しか猶予がない」
→ 大規模なリファクタリングはNG
→ バグ修正だけにフォーカス
→ テスト強化が必須
これを使ってスプリント開始時にプロダクト責任者とエンジニアが一緒に「このスプリント何やる?」を決める。正直、最初はこの制限が嫌だったけど、慣れてくるとこれって凄く合理的だなって気づく。
「機能開発Aに3日かけたい」→「でもエラーバジェット残り3分ですよ」→「あ、じゃあこっちのリスク低い機能開発にします」
こういう判断ができるようになる。営業からのプッシュもあるんだけど「朝のシステムが止まったら営業が困る」ってロジックが明確に説明できるから、納得が早い。
監視の自動化:Alerting Rules
設計ができたら、次は監視だ。Prometheus + Grafanaでアラート設定する。
# prometheus rules for SLI monitoring
groups:
- name: journey_sli_rules
interval: 30s
rules:
# Morning Data Entry Journey SLIs
- expr: |
rate(http_requests_total{endpoint="/api/customers",status=~"2.."}[5m])
/
rate(http_requests_total{endpoint="/api/customers"}[5m])
record: journey:morning_data:api_availability
labels:
journey: morning_data_entry
sli: api_availability
- expr: |
histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket{endpoint="/api/customers/search"}[5m])
)
record: journey:morning_data:search_latency_p95
labels:
journey: morning_data_entry
sli: search_latency
# Availability SLO Alert (4-week window)
- alert: MorningDataEntryAvailabilityBreach
expr: |
(
sum(rate(http_requests_total{endpoint=~"/api/customers.*",status=~"2.."}[5m]))
/
sum(rate(http_requests_total{endpoint=~"/api/customers.*"}[5m]))
) < 0.9995
for: 5m
labels:
severity: warning
journey: morning_data_entry
annotations:
summary: "Morning data entry journey availability is degrading"
dashboard: "https://grafana.internal/d/morning-journey"
runbook: "https://wiki.internal/runbooks/morning-availability"
# Error Budget Exhaustion Alert
- alert: MorningDataEntryErrorBudgetWarning
expr: |
(
1 - (
sum(rate(http_requests_total{endpoint=~"/api/customers.*",status=~"2.."}[30d]))
/
sum(rate(http_requests_total{endpoint=~"/api/customers.*"}[30d]))
)
) > 0.0004 # 99.96% = remaining budget very low
for: 5m
labels:
severity: critical
journey: morning_data_entry
annotations:
summary: "Monthly error budget nearly exhausted. No more production changes."
recommended_action: "Freeze feature deployments. Focus on stability."
Grafanaダッシュボードはこんな感じで可視化する。
xychart-beta
title "Morning Data Entry Availability - Monthly View"
x-axis [Week 1, Week 2, Week 3, Week 4]
y-axis "Availability %" 99.8, 99.99
line [99.92, 99.95, 99.98, 99.94]
line [99.95, 99.95, 99.95, 99.95]
このダッシュボードを毎日朝会で見る。「あ、Week 2は99.95%を達成した」「Week 3はちょっと調子いい」みたいにトレンドを追える。なんか地味だけど、データが目の前に出ると「あ、これ実際に数字で出てるんだ」ってリアル感が出る。
実装で気づいた落とし穴
2年間失敗しながら気づいたことをいくつか。
1. ユーザージャーニーの定義が難しい
ぶっちゃけ、営業がどこまでを1つのジャーニーと考えてるのか、正確には誰も知らない。営業マネージャーは「1日トータルで仕事できればいい」って言うし、営業担当者は「朝のデータ入力とお昼の顧客訪問の間に同期したい」って言う。その間にも細かいズレがある。
うちは結局、営業4人に詳しくインタビューして、ヒートマップで「何時に何をしてるのか」を可視化した。その時間帯×タスクの組み合わせを「ジャーニー」として定義した。数時間かかったけど、これなしには設計できなかったと思う。
2. SLIの測定方法の選択で本番爆死する可能性
たとえば「APIの応答時間P95」って書いたけど、これをどこで測る?
- アプリ側のHTTP Client(ネットワークレイテンシ含む)
- ロードバランサーの受信側(ネットワークレイテンシ含まず)
- アプリサーバー内部(キューイング時間を含まず)
どれを選ぶかで全然数字が変わる。営業が実際に体験するのは「アプリからAPIへのHTTP Clientの感覚」だから、アプリ側で測るべき。
実は最初、Nginxのログだけで測ってて、「あれ、99.95%達成できてる」って思ってたんだ。でも営業からは「朝9時いつも遅い」って言われてた。原因は、営業用PCのWiFiが朝の時間帯で混雑してて、HTTPリクエストの往路だけで200msロスしてたんだよ。Nginxログには100msしか記録されてない。完全にズレてた。
そこからは、Real User Monitoring(RUM)をブラウザに仕込んだ。営業PCのブラウザがJavaScriptでperformance.getEntriesByType('navigation')を送信して、アプリからサーバーまでの真の往復時間を測るようにした。これでようやく営業が感じるレイテンシと数字が一致した。
3. ジャーニー内のフェイルセーフ設計とSLOの矛盾
これが一番痛かった。
朝の営業データ入力フロー(availability 99.95%)を実現するために、キャッシュを導入した。Redisがキャッシュ層として入ったことで、DBへのクエリが減った。理論上は高可用性になるはずだった。
でもさ、キャッシュがあると「データの鮮度」問題が出てくるんだ。営業が朝に入力した情報がキャッシュから返ってきて「あれ、今朝入力したのに昨日のデータ?」って混乱させることになった。営業からの信頼がガタ落ちだ。
結局、「朝8時から12時の時間帯はキャッシュを積極的に使う(速さ重視)。12時から14時は5分ごとに無効化(鮮度重視)」みたいな時間帯別のポリシーにした。
面白いのは、この判定自体が「SLO管理」と「ユーザー体験」の綱引きだってこと。SLI設計の段階で「何を優先するのか」をちゃんと決める必要がある。ここを曖昧にすると、後で大変なことになる。
定着させるための工夫
2年間で学んだ「SLOをチームに定着させるコツ」もいくつか。
1. 週次の SLO ミーティング
毎週月曜朝30分「先週のSLO振り返り」をやる。
- 朝の営業データ入力フロー: 99.96% ✅ (target: 99.95%)
- 1件の遅延インシデント(16分のダウンタイムではなく、5分の高レイテンシ)
- 原因: Auth Serviceのデプロイロール中にコネクションプール枯渇
- 対策: Auth ServiceのCI/CDにヘルスチェック追加
- 残りのエラーバジェット: 3.2分
- 夕方の日報確認フロー: 99.68% ❌ (target: 99.5%)
- 昼休みに疎通テストのため意図的に10分ダウン取った
- 報告漏れでアラート出ず。エスカレーション
- 対策: オンコール向けに「計画停止」チェックボックスを追加
- 夜間バッチ分析: 100% ✅ (target: 95%)
- 余裕あり。次のスプリントで新機能追加OK
毎週この話をするから「SLOって机上の空論じゃなく、実際に達成・失敗が起こるんだ」ってリアルに感じるようになる。エンジニアも営業も同じテーブルで数字を見るから、自然と「朝のシステム大事なんだ」ってわかる。
2. インシデント根本原因分析(RCA)をSLI軸で実施
バグが出たり障害が起きたら、RCAをする。その時に「このインシデントはどのSLIを違反させたのか」を明確にする。
## Incident: Customer Search API レイテンシスパイク
### 影響を受けたSLI
- Journey: morning_data_entry
- SLI: search_latency_p95
- 影響時間: 09:15-09:45(30分)
- P95レイテンシ: 通常150ms → インシデント時2800ms
- SLO達成状況: 99.98% → 99.80%(0.18%のバジェット喪失)
### Root Cause
PostgreSQLのクエリプランがインデックスを使わず、フルテーブルスキャン
→ 営業データが10万件に増えると顕在化
→ 朝の同時アクセス(50人)でスパイク
### Timeline
09:10 - 1人の営業から「システム遅い」報告
09:12 - オンコールがAlertを受け取る(でも無視。「たまにあるでしょ」)
09:15 - 3人の営業から報告。オンコール動き始める
09:20 - PostgreSLOW QUERYログ確認。フルテーブルスキャン検出
09:25 - インデックス作成開始(CONCURRENTLY)
09:45 - インデックス完成。レイテンシ正常化
### 対策
1. クエリプランテスト: CI/CD段階でEXPLAIN ANALYZEを自動実行
2. SLI監視強化: search_latency_p95が200ms超過で即座にPagerDuty alert
3. データ成長予測: 今後3ヶ月の営業データ増加を予測→プロアクティブにインデックス追加
こうやってSLI軸でRCAをすると「このバグの本質」が見える。単なる「クエリ遅い」ではなく「朝のピーク時間帯にユーザー50人を支えられなかった」って理解できるんだ。次の対策も「インデックス追加」だけじゃなく「データ成長に備えて事前に対応」みたいに変わる。
3. エラーバジェットが底をつきそうになったら自動的に「デプロイ禁止」に
これは技術的な工夫。GitHub ActionsやGitLabのデプロイパイプラインに、SLO確認フェーズを入れた。
# .github/workflows/deploy.yml
- name: Check Error Budget
run: |
python scripts/check_error_budget.py
# Outputs:
# morning_data_entry: 1.2分 remaining (warn)
# evening_approval: 8.3分 remaining (ok)
# batch_analysis: 25.4分 remaining (ok)
#
# If any journey has <2min budget:
# → Exit with error code 1
# → Block merge to main
if: github.ref == 'refs/heads/main'
これを入れたとき、営業向けに「エラーバジェット残り1分の時は、機能追加のデプロイは自動ブロックされます」って説明した。最初は「ええ、なんで?」って反発があったけど、「朝の営業の効率が落ちるのを防ぐため」って説明すると「あ、そっか」ってなった。自動化されるってことは、ビジネス側も「これは本当に大事な制限なんだ」って理解してくれる。
正直な話
2年やってみて、SLI/SLOは「完璧な設計」なんて存在しないってわかった。ユーザーの行動は変わるし、ビジネスの優先度も変わる。
うちも去年「営業が夕方日報確認する」ってSLOを99.5%で設定してたけど、営業体制が変わって「営業マネージャーが朝に前日の日報を確認」に変わった。時間帯が8時から10時に変わったから、SLOの定義も変わった。
そういう時に大事なのは「SLOは柔軟に変更できる」って姿勢だと思う。むしろ「SLOが変わる = ビジネスの優先度が変わってる」のシグナルとして使えば、SLOはプロダクトマネジメントのツールにもなる。
あと、実装の難しさをなめちゃダメ。SLIを正確に測るって本当に大変だ。Real User Monitoringとか、サーバーサイドメトリクスとか、アラート設定とか…全部にハマりポイントがある。一個一個丁寧にやらないと、最後は「数字と現実がズレてる」ってなる。
まとめ
正直、SLI/SLO設計で本当に重要なのは、この3つだと思う。
1. ユーザージャーニー軸で分解する — システム全体の「99.9%」は無意味。営業が朝に何をしてるのかを理解してから、そのジャーニーごとにSLOを決める。営業が困る時間帯と困らない時間帯は全然違う。
2. コンポジットSLOで逆算する — 総合的なSLO達成には、どのコンポーネントがどれくらいの信頼性を必要とするのかを計算する。責任分担が明確になるし、何に力を入れるべきかも見える。
3. エラーバジェットを運用に組み込む — スプリント計画、デプロイルール、意思決定の基準として使う。机上の空論ではなく「実際に数字で制限される」経験が本当に大事。
次のアクション:
あなたのチームで試すなら、まずはこんな風にやってみたらどう?
- あなたのチームは、誰が、いつ、どうやってシステムを使ってる?それを図にまとめてから、ユーザージャーニーを3〜5個定義してみて。
- 現在のSLOが本当に意味のある数字か、営業やマネージャーに聞いてみる。もし誰も知らなかったら、設計をやり直す機会だ。
- SLI測定の方法を見直す。特に「どこで測ってるのか」。RUMを導入してユーザー視点のメトリクスを取れば、営業が感じる遅さと数字が一致する。
SLI/SLOは設計じゃなく「運用」のツールだ。完璧さより、回り始めることを優先しよう。