SLO設計で2年失敗し続けた僕が、チームに本当に定着するまでの実装記録
「SLO作ったのに誰も見てない」「アラートが鳴っても誰も動かない」——そんな経験、ありませんか? 同じ沼にハマった僕が、ユーザージャーニー起点の設計で抜け出すまでをリアルに書きました。
SLI/SLO設計で2年失敗し続けた僕が、チームで本当に機能するSLOを作るまでの記録
正直に言う。最初の1年は完全に迷走してた。
SLOを設定したはいいものの、誰もそれを見てないし、アラートは毎週鳴ってるのに誰も改善アクションを取らない。「SLOを設定する」ことが目的になっちゃってて、実際の信頼性向上には全然繋がってなかった。
インシデント対応の最新ベストプラクティス2026を読んでくれた方は「そういえばあの記事でSLOの話が出てたな」と思ってもらえるかもしれない。あの記事ではインシデント対応との連携に触れたけど、今回はもっと手前の「SLOを設計して定着させるまで」の話に絞っていく。
それと、SLO設計で2年間失敗し続けた僕が、ようやく運用が回り始めた話という記事も以前書いたんだけど、あれは概念的な整理が多かった。今回はもっと実装レベルの話を中心に据える。
なぜうちのSLOは機能しなかったのか
振り返ると、失敗の原因はほぼ決まったパターンに収束してた。
「可用性99.9%」という数字だけ決めてた問題がいちばんデカい。「とりあえず99.9%にしよう」という感じで決めるやつ。根拠がないから、実際に99.9%を下回っても「まあ障害が起きたんだからしょうがないよね」という空気になる。エラーバジェットが意味を持たない。
もう一つはSLIの測定ポイントが実態に合ってなかったこと。LBのヘルスチェックを通過してるかどうかをAvailabilityのSLIにしてたんだが、当然ながらLBが正常でもアプリが壊れてることはある。ユーザーが「遅い」「エラーになる」と感じているのに、SLOは「達成してます」という謎の状況が続いた。
この問題意識を持ってから、2025年末あたりからチームのSLO設計を根本から見直した。以下はその実録だ。
ユーザージャーニーベースのSLI設計
今うちがやってるアプローチは、最初にユーザーが「このサービスは快適に使えている」と感じる状態を定義することから始める。技術的な指標から入らない。
例えばうちのサービスで言うと、「検索して結果が3秒以内に返ってきた」「注文完了画面が正常に表示された」みたいなユーザーアクション単位でSLIを考える。これをCritical User Journey(CUJ)と呼ぶ。
# ユーザージャーニーのマッピング例
CUJ1: 商品検索〜結果表示
- SLI: 検索APIのP99レイテンシ < 3000ms かつ 5xx率 < 0.1%
- 重要度: Critical(ここが壊れると離脱直結)
CUJ2: カート追加〜注文完了
- SLI: 注文APIの成功率 > 99.5%、P99レイテンシ < 5000ms
- 重要度: Critical
CUJ3: マイページ表示
- SLI: ページ表示成功率 > 99.0%、P99レイテンシ < 5000ms
- 重要度: High(ただしCUJ1,2より優先度低)
この整理をすると自然と「どのSLIが落ちたら本当にヤバいのか」がチームで共通認識になる。これが意外と重要で、以前は全部のアラートが同じ重みで届いてたせいで、本当に大事なやつが埋もれてた。
flowchart TD
A[ユーザーアクション定義] --> B[Critical User Journey特定]
B --> C[CUJ1: 商品検索]
B --> D[CUJ2: 注文フロー]
B --> E[CUJ3: マイページ]
C --> F[SLI: 検索API レイテンシ/可用性]
D --> G[SLI: 注文API 成功率/レイテンシ]
E --> H[SLI: ページ表示 成功率]
F --> I[SLO設定]
G --> I
H --> I
I --> J[エラーバジェット計算]
J --> K[アラートポリシー]
J --> L[リリース判断基準]
コンポジットSLOと現実的な数値設定
SLIが複数になってくると、「個々のSLIは全部達成してるけど、ユーザー体験としてはどうなの?」という疑問が出てくる。そこで最近採用したのがコンポジットSLOだ。
複数のSLIを組み合わせて、「このシステムはトータルとして信頼できるか」を一つの数値で表現する。
# コンポジットSLO計算の実装例(Pythonで概念確認)
from dataclasses import dataclass
from typing import List
@dataclass
class SLI:
name: str
current_value: float # 0.0 ~ 1.0
slo_target: float
weight: float # CUJの重要度による重み
def calculate_composite_slo(slis: List[SLI]) -> dict:
"""
重み付きコンポジットSLOを計算する
全SLIがSLO達成していればgood、
どれかが閾値を割ったら重みに応じてcomposite値を下げる
"""
total_weight = sum(sli.weight for sli in slis)
weighted_achievement = 0.0
results = []
for sli in slis:
achievement_rate = min(sli.current_value / sli.slo_target, 1.0)
weighted_achievement += achievement_rate * (sli.weight / total_weight)
results.append({
"name": sli.name,
"current": f"{sli.current_value * 100:.3f}%",
"target": f"{sli.slo_target * 100:.3f}%",
"achieved": sli.current_value >= sli.slo_target
})
return {
"composite_score": weighted_achievement,
"composite_achieved": weighted_achievement >= 0.995, # 99.5%を複合目標に
"individual_results": results
}
# 実際に動かしてみると
slis = [
SLI("search_api_availability", 0.9992, 0.999, weight=3.0), # 重要度高
SLI("order_api_success_rate", 0.9988, 0.9995, weight=5.0), # 最重要
SLI("mypage_availability", 0.9979, 0.990, weight=1.0), # 重要度低
]
result = calculate_composite_slo(slis)
print(f"Composite Score: {result['composite_score']:.4f}")
print(f"Composite Achieved: {result['composite_achieved']}")
for r in result['individual_results']:
status = '✅' if r['achieved'] else '❌'
print(f"{status} {r['name']}: {r['current']} (target: {r['target']})")
# 実行結果
Composite Score: 0.9987
Composite Achieved: True
✅ search_api_availability: 99.920% (target: 99.900%)
❌ order_api_success_rate: 99.880% (target: 99.950%)
✅ mypage_availability: 99.790% (target: 99.000%)
このアウトプットを見て気づくのは、「order_api_success_rateが個別には未達なのに、コンポジットは達成」という状況だ。正直まだチームで議論中で、個別SLIが落ちたときの扱いをどうするかは好みが分かれるかもしれない。うちは「コンポジットSLOは組織レポート用、個別SLIは開発チームのアクション用」として使い分けることにした。
次に、SLO数値そのものの設定方法について。よく聞かれるのが「どのアプローチで目標値を決めるか」なんだが、状況によってかなり変わる。
| SLO設定パターン | 適用場面 | メリット | デメリット |
|---|---|---|---|
| 過去実績から逆算 | 既存サービス改善時 | 現実的な目標になりやすい | 現状追認になりやすい |
| ユーザー調査ベース | 新規サービス立ち上げ | ユーザー視点で設定できる | 調査コストが高い |
| ビジネス要件から算出 | SLA契約がある場合 | ビジネス整合性が取れる | 技術的実現可能性の確認が必要 |
| 競合分析ベース | 市場競争力維持が目的 | 業界水準との比較ができる | 競合の実態が不透明なことも |
| 自前実装(Prometheus + Grafana) | SREチームにPrometheus知見がある場合 | 柔軟性が高い、コスト安 | 運用コストがかかる |
うちが採用したのは「過去実績から逆算 + ビジネス要件の最低ライン確認」の組み合わせだ。過去3ヶ月のP99レイテンシとエラー率を出して、それを若干改善した値をSLOにする。「99.99%にしよう!」みたいな夢物語を設定しないのが大事で、達成不可能なSLOはただのノイズになる。個人的には、最初は少し緩めに設定して「達成できた」という成功体験をチームで積む方が長続きすると思ってる。
エラーバジェット消費とリリース判断の自動化
SLOを設定して終わりにしがちだけど、実際に価値を生むのはエラーバジェットをどう使うかだ。
エラーバジェットの考え方はシンプルで、「SLOが99.9%なら、30日間で43.2分は落ちていい」ということ。このバジェットを使いながら新機能をリリースするか、バジェットが尽きそうなら信頼性改善に集中するか、という判断軸になる。
xychart-beta
title "エラーバジェット消費率(過去30日)"
x-axis [Day1, Day5, Day10, Day15, Day20, Day25, Day30]
y-axis "消費率 (%)" 0 --> 100
line [2, 8, 15, 31, 45, 67, 72]
bar [2, 8, 15, 31, 45, 67, 72]
Day15あたりで消費率が急に跳ね上がってるのがわかる。実際にこういうグラフが見えると「この日に何があったんだっけ?」という会話がすぐ始まるので、ダッシュボードに出す価値がある。
うちのチームでは、Prometheusのrecording rulesでエラーバジェット消費率を常時計算して、それをGrafanaのダッシュボードに出してる。実際の設定はこんな感じだ。
# prometheus/rules/slo_rules.yaml
groups:
- name: slo_error_budget
interval: 60s
rules:
# 検索API可用性SLI(30日ウィンドウ)
- record: sli:search_api_availability:ratio_rate30d
expr: |
1 - (
sum(rate(http_requests_total{job="search-api", status=~"5.."}[30d]))
/
sum(rate(http_requests_total{job="search-api"}[30d]))
)
# エラーバジェット残量(0.0 ~ 1.0)
- record: slo:search_api:error_budget_remaining
expr: |
(
sli:search_api_availability:ratio_rate30d - 0.999
) / (1 - 0.999)
# バーンレート(1時間)
- record: slo:search_api:burn_rate_1h
expr: |
(
1 - sli:search_api_availability:ratio_rate1h
) / (1 - 0.999)
- name: slo_alerts
rules:
# 高速バーンアラート(Page worthy)
- alert: ErrorBudgetBurnRateHigh
expr: |
slo:search_api:burn_rate_1h > 14.4
for: 2m
labels:
severity: page
annotations:
summary: "エラーバジェット消費が急速すぎます"
description: "現在のバーンレートが継続すると1時間以内にバジェット枯渇します"
# 低速バーンアラート(Ticket worthy)
- alert: ErrorBudgetBurnRateMedium
expr: |
slo:search_api:burn_rate_6h > 6.0
for: 15m
labels:
severity: warning
annotations:
summary: "エラーバジェット消費ペースに注意"
description: "現在のペースが継続すると6時間以内にバジェット枯渇します"
バーンレートの14.4という数値、初見だとなんだこれってなるよね。これは「1時間でバジェット全体の1/72を使い切ってしまうレート」で、このペースが続くと72時間(3日)でバジェット枯渇するという意味だ。2hウィンドウと1hウィンドウを組み合わせるMulti-windowアラートにするとFalse Positiveがかなり減るので、本番ではそっちを推奨する。
flowchart LR
subgraph 監視スタック
P[Prometheus\nRecording Rules] --> G[Grafana\nDashboard]
P --> AM[AlertManager]
end
subgraph アラートフロー
AM -->|severity: page| PD[PagerDuty\n即時対応]
AM -->|severity: warning| SL[Slack\n#sre-alerts]
end
subgraph リリース判断フロー
G --> EB{エラーバジェット\n残量確認}
EB -->|残量 > 50%| R1[通常リリース可]
EB -->|残量 10〜50%| R2[慎重にリリース\nロールバック準備必須]
EB -->|残量 < 10%| R3[リリース凍結\n信頼性改善優先]
end
このリリース判断フローを入れたことで、「なんか不安だけどリリースしちゃおう」みたいな感覚的な判断がなくなった。マジで助かった。特にリリース直前に「エラーバジェット残量どのくらい?」って聞けるようになったのが、地味にチームの文化を変えたと思ってる。
2026年時点でのSLO設計ツール選定
自前でPrometheusとGrafanaを組み合わせる方法を書いたけど、最近はSLO管理に特化したツールも成熟してきた。うちのチームで検討したものを比較してみる。
| ツール | 強み | 弱み | 向いてるケース |
|---|---|---|---|
| Nobl9 | SLO専用で機能が豊富、ビジネス影響の可視化が得意 | コストが高め | エンタープライズ向け |
| Sloth | Prometheus用SLO生成ツール、設定がシンプル | 機能は最低限 | Prometheusがすでにある場合 |
| OpenSLO + Coralogix | 宣言的なSLO定義が書ける | 新しくてエコシステムが成熟途中 | 将来的な標準化を意識したい場合 |
| Datadog SLOs | 既存Datadogユーザーならすぐ使える | Datadogロックイン | すでにDatadog使ってる場合 |
| 自前実装(Prometheus + Grafana) | 柔軟性が高い、コスト安 | 運用コストがかかる | SREチームにPrometheus知見がある場合 |
CloudWatch vs Datadog 2026の記事でも触れてるけど、モニタリングスタック全体との相性で選ぶのが現実的だ。うちは既存のPrometheusスタックを活かしてSlothを追加導入した。
Slothの設定例を載せておく。これが地味に気に入ってる。YAMLで宣言的に書けるので、SLOの定義がコードとして残るのがいい。
# sloth/slos.yaml
apiVersion: sloth.slok.dev/v1
kind: PrometheusServiceLevel
metadata:
name: search-api-slos
namespace: monitoring
spec:
service: "search-api"
labels:
team: "platform"
env: "production"
slos:
- name: "requests-availability"
objective: 99.9
description: "検索APIの可用性SLO"
sli:
events:
errorQuery: |
sum(rate(http_requests_total{job="search-api",code=~"(5..)"}
[{{.window}}]))
totalQuery: |
sum(rate(http_requests_total{job="search-api"}
[{{.window}}]))
alerting:
name: SearchAPIHighErrorRate
labels:
category: "availability"
annotations:
runbook: "https://wiki.internal/runbooks/search-api"
pageAlert:
labels:
severity: critical
ticketAlert:
labels:
severity: warning
- name: "requests-latency"
objective: 95.0 # リクエストの95%が3秒以内に完了する
description: "検索APIレイテンシSLO(P95 < 3s)"
sli:
events:
errorQuery: |
sum(rate(http_request_duration_seconds_bucket{
job="search-api",
le="3"
}[{{.window}}]))
totalQuery: |
sum(rate(http_request_duration_seconds_count{
job="search-api"
}[{{.window}}]))
# Slothでルールを生成
sloth generate -i slos.yaml -o /etc/prometheus/rules/slo_generated.yaml
# 生成されたルールの確認
cat /etc/prometheus/rules/slo_generated.yaml | head -50
これを実行すると、Multi-windowのrecording rulesとアラートルールが自動生成される。手書きのPrometheusルールと比べてヒューマンエラーが大幅に減った。正直、これを知る前に手書きで運用してた時期が恥ずかしくなるくらい楽になった。
SLOを文化として根付かせるために大事だったこと
技術的な実装の話はここまでにして、最後は「どうやってチームに定着させるか」の話をしたい。これ、技術よりずっと難しかった。
うちが実際にやって効果があったのは次の3つだ。
週次でエラーバジェットレビューをやる。 スプリントレビューのついでに5分でいいから「今週のエラーバジェット消費どうだった?」を確認する時間を作った。最初は「また数字の話か」みたいな空気だったけど、3ヶ月続けたら自然と「あのデプロイのあとバジェット削れたよね」という会話が生まれてきた。
SLOを根拠にリリースを止めた体験を一度作る。 これが一番インパクトあった。エラーバジェット残量が8%のときに「今はリリース止めましょう」と言って、実際に止めた。当然最初は抵抗があったけど、その後信頼性が回復してバジェットが戻ってからリリースしたら「これが機能するんだ」とチームが理解してくれた。「一度だけ止める」経験が全部を変えたと思ってる。
ポストモーテムにSLO影響を必ず記載する。 インシデント対応の最新ベストプラクティス2026でも書いたポストモーテムのフォーマットに「このインシデントでエラーバジェットが何%消費されたか」という項目を追加した。これだけでインシデントとSLOが頭の中で繋がるようになる。
正直まだ完璧ではないし、エラーバジェットポリシーの細部(どのタイミングでリリース凍結を解除するか、など)は今もチームで議論してる。ただ、1年前と比べると「SLOは飾り」という雰囲気は消えた。みなさんのチームではSLOの数字、ちゃんと生きてますか?
pie title SLO未達の主な原因分類(うちのチーム過去1年)
"インフラ起因" : 28
"デプロイ起因" : 35
"依存サービス起因" : 22
"設定ミス" : 10
"その他" : 5
デプロイ起因が35%というのは最初見たとき衝撃だった。「デプロイ前にもっと検証しましょう」ではなく「デプロイ方法を変えよう」という発想の転換が必要で、Canaryリリース導入につながった。こういう議論ができるようになったのはSLOで数値化できてるからだと思ってる。
まとめ
SLI/SLO設計を2年かけてチームに根付かせた経験を振り返ると、技術的な正しさよりも「チームが意思決定に使えるか」が全てだと思う。
今回の要点をまとめると以下の5つだ。
-
SLIは技術指標ではなくユーザーアクションから設計する。 「サーバーが起動してるか」ではなく「ユーザーが成功体験を得られたか」を測る。CUJマッピングから始めよう
-
コンポジットSLOで「サービス全体の健康状態」を一元化する。 個別SLIの達成状況と組み合わせることで、チームが見るべき指標がシンプルになる
-
エラーバジェットをリリース判断に組み込む。 「なんとなく不安だからリリース見送り」ではなく「バジェット残量10%以下なのでリリース凍結」という客観的な基準が文化を変える
-
Slothなどの宣言的SLOツールでPrometheusルールの手書きをやめる。 ヒューマンエラーが減るし、SLOの定義がコードとして残る
-
週次バジェットレビューを習慣化する。 月1回の振り返りでは遅い。週単位で消費状況を確認することで、問題が大きくなる前に対処できる
次のアクションとして、まず自チームのサービスで最も重要なCUJを1つ特定して、そのSLIを今の監視スタックで計算できるか確認してみてほしい。1つ動いたら全体が見えてくる。完璧なSLO設計を最初から目指すより、「1つ動くSLO」を持つことが圧倒的に大事だ。