B システム設計/インフラ — shipping-rate-estimator 外部配送会社API依存の信頼性設計(GKE Autopilot Sidecar Container(Envoy) タイムアウト×サーキットブレーカー×Outlier Detection × アプリ側フォールバック(全国一律送料) × Cloud Monitoring SLO(request-based SLI) × マルチウィンドウ・マルチバーンレートアラート × エラーバジェットに基づくCI/CDリリース凍結ゲート)(MOps 販促システムチーム チェックアウト全停止インシデント Bad→Good)

2026-09-01 (Day 148) 火曜 B: システム設計/インフラ ★★★★☆ GKE Sidecar Container / Envoy Circuit Breaker Cloud Monitoring SLO / Error Budget

概要

⏱️

Sidecar Container(Envoy) でタイムアウトを一元管理

アプリコードに書かず、GKE 1.29+ stableのSidecar Containerに持たせる。接続1秒・リクエスト2秒を外部APIのSLA(p99 2秒)に合わせて設定し、応答待ちがCloud Runの同時実行枠を埋め尽くすのを防ぐ。

🔌

サーキットブレーカー × Outlier Detection

劣化した外部APIを叩き続けて負荷を増幅させる側に回らない。circuit_breakersとoutlier_detection(連続5xxで一時的に切り離す)で追い打ちを構造的に止める。

🛟

ビジネス承認済みフォールバックをコードに反映

「外部API障害時は全国一律送料へ切り替えてでもチェックアウトを継続する」という既承認方針が実装されていなかった。DEFAULT_FLAT_RATE_JPYで正常系として扱う。

📉

エラーバジェットをリリース判断に接続

SLO/SLIを定義し、マルチウィンドウ・マルチバーンレートアラートで検知精度を上げ、エラーバジェット残量が閾値未満ならCI/CDが非緊急デプロイを自動でブロックする。

問題

ECサイト MOps 販促システムチームが運用する配送料金見積もりサービス(shipping-rate-estimator)は、チェックアウトフロー内で外部配送会社の料金見積もりAPI(サードパーティ、SLA 99.5%)を同期呼び出しし、購入者に正確な送料を提示している。

先週起きたインシデント: 外部配送会社側で障害が発生し、応答時間が平常時150msから最大8秒まで劣化する状態が40分間続いた。shipping-rate-estimatorはこの外部呼び出しにタイムアウトを設定しておらず、Cloud Runの同時実行枠(concurrency)が応答待ちのリクエストで埋まり尽くした結果、送料計算と無関係な他のチェックアウトリクエストまで一切受け付けられなくなり、40分間チェックアウト機能全体が停止した。実際にはビジネス側は「外部APIが落ちた場合は全国一律送料にフォールバックしてでもチェックアウトは止めない」という方針を既に承認していたが、この方針はコードに反映されていなかった。

問題点を全て洗い出し、GKE Autopilot Sidecar Containers(Envoyによるタイムアウト×サーキットブレーカー×Outlier Detection)× アプリケーション側フォールバック × Cloud Monitoring SLO(request-based SLI) × マルチウィンドウ・マルチバーンレートアラート × エラーバジェットに基づくリリース凍結ゲート × バックオフ+ジッター付きリトライを考慮したBad→Goodリファクタリングを行ってください。

制約・前提条件

  • GKE Autopilot 1.30+(Sidecar Containers は stable 機能)、Terraform 1.8+(google プロバイダー 5.x)、リージョン asia-northeast1
  • 外部配送会社APIのSLA: 稼働率99.5%、p99レイテンシ2秒(達成できない時間帯がある前提で設計すること)
  • チェックアウトサービス全体のSLO目標: 可用性99.9%/月(社内合意済み)。送料計算はクリティカルパスの一部だが、外部API障害時は全国一律送料へフォールバックしてでも継続するというビジネス判断が既に下されている
  • フォールバック送料の差額は自社が吸収する方針(チェックアウト全停止による機会損失よりはるかに小さい損失として経営判断済み)
  • 監視基盤: Cloud Monitoring(SLO機能)+ DataDog Agent v7 併用
期待する回答形式: 問題点の列挙(番号付き)+ 改善後構成(GKE Sidecar Container Envoy設定・Terraform SLO/アラート・Pythonフォールバックロジック)+ エラーバジェットに基づくリリース凍結運用フロー(設計意図の説明)

悪い構成 (Before)

この構成には 7つの設計上の問題 が隠れています。見つけてみてください。
現行実装(抜粋)
# shipping-rate-estimator/app/checkout.py
import requests

def get_shipping_rate(cart: Cart) -> ShippingRate:
    # 問題①: タイムアウト未設定(デフォルトで無限待機)
    # 問題⑦: 即時リトライ、バックオフ・ジッターなし
    for attempt in range(3):
        try:
            resp = requests.post(
                "https://carrier-api.example.com/v1/estimate",
                json={"items": cart.items, "zip": cart.zip_code},
            )
            resp.raise_for_status()
            return ShippingRate(amount=resp.json()["amount"])
        except requests.RequestException:
            continue  # 問題②: サーキットブレーカーなし、失敗しても叩き続ける
    # 問題③: 全リトライ失敗時、フォールバックせずそのまま例外を上げる
    raise ShippingRateUnavailableError()

# terraform/checkout.tf(抜粋)
# 問題④: SLO/SLIが未定義、「どこまで悪化したら問題か」の客観基準がない
resource "google_cloud_run_v2_service" "shipping_rate_estimator" { ... }

# 問題⑤: 静的閾値アラートのみ(じわじわ進行する劣化を検知できない)
resource "google_monitoring_alert_policy" "naive_5xx_alert" {
  conditions {
    condition_threshold {
      comparison      = "COMPARISON_GT"
      threshold_value = 5
      duration        = "60s"
    }
  }
}
# 問題⑥: エラーバジェットの消費状況がCI/CDのデプロイ判断と未接続
問題点サマリー(7点)
1タイムアウト未設定 — 外部劣化がCloud Run同時実行枠を埋め尽くし他リクエストまで巻き添え
2サーキットブレーカー不在 — 劣化した外部APIを叩き続け負荷を増幅
3フォールバック未実装 — 承認済みの全国一律送料切替が使われない
4SLO/SLI未定義 — 悪化判断が感覚依存
5静的閾値アラートのみ — slow burnを検知できない
6エラーバジェットとリリース判断が未接続 — 障害中も新規デプロイ継続
7即時リトライ — バックオフ・ジッターなくthundering herd誘発

ヒント(段階的開示)

ヒント1 — 方向性
問題は3層に分類できる。(1) 外部依存の異常が自サービスへ伝播することを防ぐ層がない — タイムアウト・サーキットブレーカー・フォールバックのいずれも実装されておらず、外部APIの劣化がそのまま自サービスの全面停止に増幅されている。健全なサービスは「依存先が壊れても自分は生き延びる」設計を持つ必要がある。(2) 「悪い」をどう定義するかが決まっていない — SLO/SLIが存在せず、アラートも「5xxが5件を超えたら」という感覚的な静的閾値のみ。これでは瞬間的なノイズに過敏に反応する一方、閾値未満でじわじわ進行する劣化(slow burn)を見逃す。(3) 悪化を検知した後の運用対応が仕組み化されていない — エラーバジェットがどれだけ残っているかが可視化されず、障害の最中でも新規リリースが止まらない。これら3層を、GKE AutopilotのSidecar Containers(Envoyによる回路遮断)× Cloud Monitoring SLO × エラーバジェットゲートで埋めることを考えてみてください。
ヒント2 — アプローチ
  • 問題①: 外部API呼び出しにタイムアウト未設定 → 外部APIのSLA(p99レイテンシ2秒)を踏まえ、接続タイムアウト1秒・リクエストタイムアウト2秒を明示する。アプリコードに直接書くより、Sidecar ContainerのEnvoyプロキシに持たせるとアプリコード非依存で一元管理できる
  • 問題②: サーキットブレーカーが存在しない → Envoyのcircuit_breakers(max_pending_requests/max_requests)とoutlier_detection(連続5xxでホストを一時的に切り離す)を設定し、劣化した外部APIへの追い打ちを止める
  • 問題③: フォールバックが未実装 → サーキットがオープンした場合、アプリ側は例外をキャッチしてDEFAULT_FLAT_RATE_JPY(全国一律送料の名前付き定数)を返し、フォールバック使用をメトリクス・構造化ログで可視化する
  • 問題④: SLO/SLIが未定義 → google_monitoring_sloでチェックアウトのrequest-based SLI(成功 or フォールバック成功を「good」、タイムアウトで失敗したもののみ「bad」)を定義し、月間99.9%のSLO目標を明文化する
  • 問題⑤: 静的閾値アラートのみ → Google SREワークブックのマルチウィンドウ・マルチバーンレート(短時間窓での急激なバーン + 長時間窓でのslow burn)を組み合わせたアラートポリシーに置き換える
  • 問題⑥: エラーバジェットとリリース判断が未接続 → CI/CDパイプライン(Argo Workflows / GitHub Actions)にデプロイ前ゲートを追加し、Cloud MonitoringのSLOバーンレートを問い合わせてエラーバジェット残量が閾値未満なら非緊急リリースをブロックする
  • 問題⑦: 即時リトライでバックオフ・ジッターがない → tenacity等で指数バックオフ+ジッター、最大リトライ回数を制限し、リクエストタイムアウト予算内に収める設計にする
ヒント3 — コードの骨格
# Sidecar Container: Envoyによるタイムアウト・サーキットブレーカー・Outlier Detection
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shipping-rate-estimator
spec:
  template:
    spec:
      containers:
        - name: app
          image: shipping-rate-estimator:latest
          # アプリはlocalhost:15001(Envoy sidecar)経由で外部APIを呼ぶ
        - name: envoy-sidecar
          restartPolicy: Always   # GKE 1.29+ stable: Sidecar Containers
          image: envoyproxy/envoy:v1.30-latest
          # clusters: carrier_api
          #   connect_timeout: 1s
          #   circuit_breakers.thresholds: [{max_pending_requests: 20, max_requests: 20}]
          #   outlier_detection: {consecutive_5xx: 3, base_ejection_time: 30s}

# Cloud Monitoring SLO(request-based SLI)のスケルトン
resource "google_monitoring_slo" "checkout_availability" {
  service      = google_monitoring_service.checkout.service_id
  slo_id       = "checkout-availability"
  goal         = 0.999
  rolling_period_days = 30
  request_based_sli {
    good_total_ratio {
      good_service_filter  = "... AND (status=\"OK\" OR fallback_used=\"true\")"
      total_service_filter = "..."
    }
  }
}

問題点分析(7点)

#問題点分類改善方法
1タイムアウト未設定で他リクエストまで巻き添え伝播遮断層Envoy sidecarで接続1s/リクエスト2s明示
2サーキットブレーカー不在で外部負荷を増幅伝播遮断層circuit_breakers+outlier_detection
3フォールバック未実装伝播遮断層DEFAULT_FLAT_RATE_JPYへの正常系フォールバック
4SLO/SLI未定義で悪化判断が感覚依存定義層google_monitoring_slo request-based SLI
5静的閾値アラートのみでslow burn検知不能定義層マルチウィンドウ・マルチバーンレート
6エラーバジェットとリリース判断が未接続運用自動化層CI/CDデプロイ前ゲート
7即時リトライでthundering herd誘発運用自動化層tenacity指数バックオフ+ジッター

障害伝播フロー図 — Bad vs Good の変換

Bad(変更前)— タイムアウト・サーキットブレーカーなし 外部配送会社APIが8秒まで応答劣化 問題①: タイムアウト未設定で無限に応答を待つ 問題⑦: 即時リトライでさらに外部負荷を上乗せ Cloud Runの同時実行枠が応答待ちで埋まる 問題②: サーキットブレーカーがなく劣化APIを叩き続ける 送料計算と無関係な他リクエストまで受付不能に 問題③: フォールバックがなくチェックアウト全体がエラー チェックアウト機能が40分間全停止 承認済みの全国一律送料フォールバックが未使用 送料計算以外の購入もすべて止まる 静的閾値アラート(5xx>5件/1分)のみ 問題④⑤: SLO未定義・slow burnを検知できない 対応開始判断が感覚に依存し初動が遅れる 構造的なリスク 問題⑥: エラーバジェットとCI/CDリリース判断が未接続 障害中でも新規デプロイが自動的に続行されリスク増大 次に外部APIが劣化しても同じ全停止が再発する × 外部依存の異常が自サービスへそのまま伝播 × 「悪い」の客観基準(SLO)が存在しない × エラーバジェットが運用対応に接続されていない Good(変更後)— 伝播遮断 × SLO × エラーバジェットゲート Envoy Sidecar: connect_timeout 1s / request timeout 2s 修正①: 外部SLA(p99 2秒)に合わせて明示的に設定 修正⑦: tenacityで指数バックオフ+ジッター、最大2回 外部APIが劣化 → circuit_breakers/outlier_detectionが作動 修正②: 連続5xx=3でホストを30秒切り離し、追い打ちを遮断 他のチェックアウトリクエストは正常に処理継続 DEFAULT_FLAT_RATE_JPYへフォールバックしチェックアウト継続 修正③: ビジネス承認済み方針をコードに反映、正常系として処理 バジェット健全 バジェット枯渇 通常デプロイ許可 SLOバーンレートを毎回CI/CDで確認 修正④⑤⑥: SLO定義+マルチバーンレート 非緊急リリース凍結 incident-fixのみ許可 修正⑥: CI/CDゲート SLOバーンレートアラートで初動を早期化 修正⑤: 1h窓/14.4x + 6h窓/6x のマルチウィンドウ判定 急性障害・slow burnの両方を検知しつつ誤報を抑制 ✓ 外部依存の異常が自サービスへ伝播しない ✓ SLO/SLIが「悪い」の客観基準になっている ✓ エラーバジェットがリリース判断に接続されている 修正

模範解答

Before — タイムアウト・CBなしの直接呼び出し
# 問題①②: タイムアウトもサーキットブレーカーもなく
# 外部APIへ直接requests.post()するのみ
resp = requests.post(
    "https://carrier-api.example.com/v1/estimate",
    json={"items": cart.items, "zip": cart.zip_code},
)
After — GKE Sidecar Container(Envoy)
# k8s/shipping-rate-estimator-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shipping-rate-estimator
  namespace: mops
spec:
  replicas: 3
  template:
    spec:
      containers:
        # ── メインアプリ ──
        - name: app
          image: asia-northeast1-docker.pkg.dev/mops/checkout/shipping-rate-estimator:v2
          env:
            - name: CARRIER_API_PROXY
              value: "http://localhost:15001"  # Envoy経由
          resources:
            requests: { cpu: "250m", memory: "256Mi" }

        # ── Envoy Sidecar(修正①②: GKE 1.29+ stable)──
        - name: envoy-sidecar
          restartPolicy: Always   # sidecar指定
          image: envoyproxy/envoy:v1.30-latest
          volumeMounts:
            - name: envoy-config
              mountPath: /etc/envoy
          resources:
            requests: { cpu: "100m", memory: "128Mi" }
      volumes:
        - name: envoy-config
          configMap: { name: envoy-carrier-proxy-config }
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: envoy-carrier-proxy-config
  namespace: mops
data:
  envoy.yaml: |
    static_resources:
      listeners:
        - name: listener_15001
          address: { socket_address: { address: 0.0.0.0, port_value: 15001 } }
          filter_chains:
            - filters:
                - name: envoy.filters.network.http_connection_manager
                  typed_config:
                    route_config:
                      virtual_hosts:
                        - name: carrier_api_vh
                          domains: ["*"]
                          routes:
                            - match: { prefix: "/" }
                              route:
                                cluster: carrier_api
                                timeout: 2s   # 修正①: リクエストタイムアウト
      clusters:
        - name: carrier_api
          connect_timeout: 1s               # 修正①: 接続タイムアウト
          type: LOGICAL_DNS
          load_assignment:
            cluster_name: carrier_api
            endpoints:
              - lb_endpoints:
                  - endpoint: { address: { socket_address: { address: carrier-api.example.com, port_value: 443 } } }
          circuit_breakers:                  # 修正②: サーキットブレーカー
            thresholds:
              - priority: DEFAULT
                max_pending_requests: 20
                max_requests: 20
                max_retries: 2
          outlier_detection:                  # 修正②: Outlier Detection
            consecutive_5xx: 3
            base_ejection_time: 30s
            max_ejection_percent: 100
# shipping-rate-estimator/app/checkout.py
"""チェックアウト時の配送料金見積もりを行うモジュール。

外部配送会社APIの障害時は、ビジネス側が承認済みの
全国一律送料フォールバックへ切り替え、チェックアウトを継続させる。
"""
import logging
import httpx
from tenacity import (
    retry, stop_after_attempt, wait_exponential_jitter, retry_if_exception_type,
)

logger = logging.getLogger(__name__)

DEFAULT_FLAT_RATE_JPY = 660       # 修正③: 障害時フォールバック送料(全国一律・経営承認済み)
CARRIER_API_TIMEOUT_SEC = 2.0     # 修正①: 外部APIのp99 SLAに合わせたアプリ側タイムアウト
MAX_RETRY_ATTEMPTS = 2            # 修正⑦: 過剰リトライによるthundering herdを防ぐ上限


@retry(
    stop=stop_after_attempt(MAX_RETRY_ATTEMPTS),
    wait=wait_exponential_jitter(initial=0.2, max=1.0),  # 修正⑦: 指数バックオフ+ジッター
    retry=retry_if_exception_type(httpx.HTTPError),
    reraise=True,
)
def _call_carrier_api(cart: Cart) -> float:
    """Envoy sidecar(localhost:15001)経由で外部配送会社APIを呼ぶ。

    Args:
        cart: 見積もり対象のカート。

    Returns:
        配送料金(円)。

    Raises:
        httpx.HTTPError: サーキットオープン中・タイムアウト・5xx等の失敗時。
    """
    with httpx.Client(base_url="http://localhost:15001", timeout=CARRIER_API_TIMEOUT_SEC) as client:
        resp = client.post("/v1/estimate", json={"items": cart.items, "zip": cart.zip_code})
        resp.raise_for_status()
        return resp.json()["amount"]


def get_shipping_rate(cart: Cart) -> ShippingRate:
    """カートの配送料金を取得する。外部API障害時はフォールバックする。

    Args:
        cart: 見積もり対象のカート。

    Returns:
        ShippingRate: 送料と、フォールバックを使用したかどうかのフラグを含む。
    """
    try:
        amount = _call_carrier_api(cart)
        return ShippingRate(amount=amount, fallback_used=False)
    except httpx.HTTPError as exc:
        # 修正③: チェックアウトを失敗させず、承認済みフォールバックへ切り替える
        logger.warning(
            "shipping_rate_fallback_used",
            extra={"reason": str(exc), "cart_id": cart.id, "fallback_amount": DEFAULT_FLAT_RATE_JPY},
        )
        SHIPPING_RATE_FALLBACK_COUNTER.inc()  # DataDog/Cloud Monitoringへ送出するカスタムメトリクス
        return ShippingRate(amount=DEFAULT_FLAT_RATE_JPY, fallback_used=True)
resource "google_monitoring_service" "checkout" {
  service_id   = "checkout"
  display_name = "Checkout (shipping-rate-estimator含む)"
}

# 修正④: request-based SLI(フォールバック成功も「good」に含める=
#         ビジネス判断上、チェックアウトが完走すればSLOは満たされているとみなす)
resource "google_monitoring_slo" "checkout_availability" {
  service             = google_monitoring_service.checkout.service_id
  slo_id              = "checkout-availability"
  display_name        = "Checkout Availability (fallback許容)"
  goal                = 0.999          # 月間99.9%
  rolling_period_days = 30
  request_based_sli {
    good_total_ratio {
      good_service_filter  = "resource.type=\"cloud_run_revision\" AND (metric.label.response_code_class=\"2xx\" OR jsonPayload.fallback_used=\"true\")"
      total_service_filter = "resource.type=\"cloud_run_revision\""
    }
  }
}

# 修正⑤: マルチウィンドウ・マルチバーンレートアラート(Google SREワークブック準拠)
resource "google_monitoring_alert_policy" "checkout_slo_fast_burn" {
  display_name = "checkout SLO fast burn (1h窓, 14.4x)"
  conditions {
    display_name = "burn_rate > 14.4 over 1h AND 5m"
    condition_threshold {
      filter          = "select_slo_burn_rate(\"${google_monitoring_slo.checkout_availability.id}\", \"3600s\")"
      comparison      = "COMPARISON_GT"
      threshold_value = 14.4
      duration        = "0s"
    }
  }
  combiner = "OR"
  notification_channels = [google_monitoring_notification_channel.pagerduty.id]
}

resource "google_monitoring_alert_policy" "checkout_slo_slow_burn" {
  display_name = "checkout SLO slow burn (6h窓, 6x)"
  conditions {
    display_name = "burn_rate > 6 over 6h"
    condition_threshold {
      filter          = "select_slo_burn_rate(\"${google_monitoring_slo.checkout_availability.id}\", \"21600s\")"
      comparison      = "COMPARISON_GT"
      threshold_value = 6
      duration        = "0s"
    }
  }
  combiner = "OR"
  notification_channels = [google_monitoring_notification_channel.slack.id]
}
# .github/workflows/deploy-shipping-rate-estimator.yaml(抜粋)
jobs:
  error-budget-gate:
    runs-on: ubuntu-latest
    outputs:
      budget_remaining_pct: ${{ steps.check.outputs.budget_remaining_pct }}
    steps:
      - id: check
        run: |
          # Cloud Monitoring SLO APIからエラーバジェット残量を取得
          REMAINING=$(gcloud monitoring slo describe checkout-availability \
            --service=checkout --format="value(errorBudget.remainingPercent)")
          echo "budget_remaining_pct=$REMAINING" >> "$GITHUB_OUTPUT"

  deploy:
    needs: error-budget-gate
    # 修正⑥: エラーバジェット10%超、またはincident-fixタグ付きのみ通常デプロイを許可
    if: needs.error-budget-gate.outputs.budget_remaining_pct > 10 || contains(github.event.head_commit.message, 'incident-fix')
    runs-on: ubuntu-latest
    steps:
      - run: echo "デプロイ実行"

ポイント解説

1依存先の異常が自サービスへ「伝播」する経路を先に断つ — タイムアウト・サーキットブレーカー・フォールバックは3点セットで機能する。タイムアウトだけでは「遅い外部APIを叩き続ける」問題が残り、サーキットブレーカーだけでは「オープンした後どう振る舞うか」が空白のままになる。3つを揃えて初めて「依存先が壊れても自分は生き延びる」設計になる。
2SLI/SLOは「ビジネス上の成功」を反映するように定義する — 今回のSLIは「HTTP 2xx」ではなく「2xx またはフォールバック成功」を good としている。これは、ビジネス側が既に「フォールバックでもチェックアウトが完了すれば許容範囲」と判断しているためで、SLIの定義そのものがビジネス判断の反映であるべきという点が重要。
3マルチウィンドウ・マルチバーンレートアラートは、検知速度と誤検知率の両方を改善する — 短い窓(1時間)だけだとノイズに弱く、長い窓(6時間)だけだと検知が遅い。両方を組み合わせることで、急性の障害とじわじわ進行する劣化の両方を拾いながら誤報を抑える、Google SREワークブック標準のパターンになる。
4エラーバジェットは「使い切ったら止まる予算」として運用して初めて意味を持つ — SLOを定義しただけでは数値が可視化されるだけで、行動には結びつかない。エラーバジェット残量をCI/CDのデプロイゲートに接続することで、「信頼性が悪化している最中に新しいリスクを積み増さない」という判断が自動化・仕組み化される。

実務への応用

shipping-rate-estimatorに限らず、MOps 販促システムチームが外部SaaS・外部APIに依存する箇所(決済代行、住所検証API、クーポン外部連携等)は同様に「タイムアウト未設定 × フォールバック未実装」のリスクを抱えている可能性が高い。今回のSidecar Container(Envoy)によるタイムアウト・サーキットブレーカー・Outlier Detectionはアプリコードを一切変更せずインフラ層だけで適用できるため、既存サービスへの後付け導入コストが低く、横展開しやすい。また、エラーバジェットに基づくリリース凍結ゲートは、shipping-rate-estimatorだけでなくチェックアウト全体のSLOに対して汎用化しておけば、将来的に他の依存サービス(在庫確認API・クーポン適用API)を追加する際も同じゲートで守れる。

SLIに「フォールバック成功」を含めるかどうかは、ビジネス側との合意が前提になる。技術者だけの判断で「フォールバックすれば成功扱い」と決めてしまうと、実際にはフォールバック送料による損失が積み重なっているにも関わらずSLO上は「健全」に見えてしまう危険がある。SHIPPING_RATE_FALLBACK_COUNTERのようなフォールバック使用率メトリクス自体を別途監視し、一定割合を超えたら経営層へエスカレーションする運用も併せて設計しておくべきである。

今日のまとめ

今回のインシデントの本質は「外部APIが落ちた」ことではなく、外部APIの異常を自サービス内で吸収する設計が一切なかったことにある。 タイムアウト・サーキットブレーカー・フォールバックという3点セットで依存先の異常の伝播を断ち、SLO・マルチバーンレートアラート・エラーバジェットゲートで「どこまで悪化したら何をするか」を客観的な数値に落とし込むことで、次に外部APIが劣化してもチェックアウト全体が止まる事態を構造的に防げる。

次のステップ

  • 発展問題: フォールバック使用率(SHIPPING_RATE_FALLBACK_COUNTER / 全リクエスト数)自体に対しても閾値ベースのアラートを追加し、「サーキットブレーカーが正しく機能してチェックアウトは止まっていないが、外部API障害が長時間続いてフォールバック送料による損失が積み上がっている」状態を検知する設計を検討せよ。損失額をリアルタイムで概算し、一定額を超えたらインシデント宣言する仕組みも合わせて考えること
  • 参考: GKE Autopilot Sidecar Containers / Envoy circuit_breakers・outlier_detection / Cloud Monitoring SLO(request-based SLI)/ Google SRE Workbook(Multiwindow, Multi-Burn-Rate Alerts)/ tenacity(指数バックオフ+ジッター)

自己評価(あとで記入)