セクション4:Google Cloud サービスとの統合
公式試験ガイド 042426版 — Section 4: Integrating applications with Google Cloud services
4.1 データ・ストレージサービスとの統合
Cloud SQL 接続:Auth Proxy vs Connector
Cloud Run / GKE / GCE から Cloud SQL に接続する方法には、Auth Proxy(プロセス分離)とConnector ライブラリ(プロセス内)の2つがある。
Cloud Run + Auth Proxy(推奨)
# Cloud Run へのデプロイで Cloud SQL を関連付け
gcloud run deploy my-service \
--image=us-central1-docker.pkg.dev/PROJECT/repo/app:v1 \
--add-cloudsql-instances=PROJECT:us-central1:my-instance \
--set-env-vars="INSTANCE_CONNECTION_NAME=PROJECT:us-central1:my-instance" \
--region=us-central1
# アプリ側: Unix Socket で接続
import psycopg2
conn = psycopg2.connect(
host=f"/cloudsql/{os.environ['INSTANCE_CONNECTION_NAME']}",
user="my-user",
password=db_password, # Secret Manager から取得
database="my-db"
)
Cloud SQL Connector(Python の例)
from google.cloud.sql.connector import Connector, IPTypes
import sqlalchemy
connector = Connector(ip_type=IPTypes.PRIVATE) # Private IP 推奨
def getconn():
return connector.connect(
"PROJECT:us-central1:my-instance",
"pg8000",
user="my-user",
password=db_password,
db="my-db",
enable_iam_auth=True # IAM 認証
)
engine = sqlalchemy.create_engine("postgresql+pg8000://", creator=getconn)
選択の指針
| シーン | 推奨 | 理由 |
|---|---|---|
| Cloud Run(標準) | Auth Proxy(--add-cloudsql-instances) | Cloud Run が自動でサイドカー注入 |
| GKE | Auth Proxy Operator | カスタムリソースで自動サイドカー |
| Python / Java / Go | Connector ライブラリ(言語サポートあり) | プロセス分離不要、コードシンプル |
| Compute Engine | Auth Proxy(プロセス常駐) | 複数アプリで共有可能 |
- Auth Proxy はコネクションプーリング機能を持たない。アプリ側で SQLAlchemy / HikariCP 等のプールを実装
- HA フェイルオーバー時に既存接続が切れる。再接続ロジック必須
- Public IP に直接接続はアンチパターン(パスワードのみで TLS なし可能性)
- Private IP + Auth Proxyでも Auth Proxy のプロセスは VPC に到達できる必要あり(Direct VPC Egress / Serverless VPC Access)
- port 3307 (CSQL Auth Proxy) と 5432/3306 を混同しない(Proxy は内部的に 3307 で API ゲートウェイに接続)
Firestore クライアントの典型パターン
from google.cloud import firestore
db = firestore.Client()
# 書き込み(自動 created/updated)
db.collection("users").document("u1").set({
"name": "Alice",
"age": 30,
"createdAt": firestore.SERVER_TIMESTAMP,
})
# トランザクション(単一文書 ACID)
@firestore.transactional
def update_balance(transaction, user_ref, amount):
snapshot = user_ref.get(transaction=transaction)
new_balance = snapshot.get("balance") + amount
transaction.update(user_ref, {"balance": new_balance})
user_ref = db.collection("users").document("u1")
update_balance(db.transaction(), user_ref, 1000)
# クエリ + ページング
query = db.collection("users").where("age", ">", 18).order_by("createdAt").limit(100)
docs = query.stream()
for doc in docs:
print(doc.id, doc.to_dict())
- ドキュメントサイズ上限 1MB。大きい値は GCS にオフロード
- 1 文書あたり 1秒に 1書き込み(hot doc 問題)。連番カウンタは複数文書に分散
- 複合クエリには複合インデックス必要。Console で自動生成または手動定義
OR条件は複数クエリでクライアント側マージ(Firestore は単一クエリ AND のみ)- 配列の
array-containsは1クエリに1つのみ
Pub/Sub クライアントライブラリの典型パターン
# Publish(バッチ + 非同期)
from google.cloud import pubsub_v1
from concurrent import futures
publisher = pubsub_v1.PublisherClient(
publisher_options=pubsub_v1.types.PublisherOptions(
enable_message_ordering=False,
flow_control=pubsub_v1.types.PublishFlowControl(
message_limit=1000,
byte_limit=10 * 1024 * 1024,
limit_exceeded_behavior=pubsub_v1.types.LimitExceededBehavior.BLOCK,
),
),
batch_settings=pubsub_v1.types.BatchSettings(
max_messages=100,
max_bytes=1024 * 1024,
max_latency=0.1,
),
)
topic_path = publisher.topic_path("PROJECT", "my-topic")
publish_futures = []
for msg in messages:
future = publisher.publish(topic_path, msg.encode("utf-8"), attribute_key="value")
publish_futures.append(future)
# 全部の publish の完了待ち
futures.wait(publish_futures, return_when=futures.ALL_COMPLETED)
# Subscribe(streaming pull)
from google.cloud import pubsub_v1
subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path("PROJECT", "my-sub")
flow_control = pubsub_v1.types.FlowControl(
max_messages=100, # 1度に受け取る最大メッセージ数
)
def callback(message):
try:
process(message.data)
message.ack() # 処理成功
except TransientError:
message.nack() # 即座に再配信
except Exception as e:
# 処理失敗 → ack せずデッドラインで自動再配信、または DLT へ
streaming_pull_future = subscriber.subscribe(
subscription_path, callback=callback, flow_control=flow_control)
with subscriber:
try:
streaming_pull_future.result(timeout=None)
except KeyboardInterrupt:
streaming_pull_future.cancel()
4.2 Google Cloud API の利用
Application Default Credentials (ADC) の優先順位
サービスごとのデフォルト SA
| サービス | デフォルト SA | 推奨 |
|---|---|---|
| Compute Engine | PROJECT_NUMBER-compute@developer.gserviceaccount.com | 必ず専用 SAを割り当て直す |
| Cloud Run | Compute デフォルト SA(権限過多) | --service-account で明示指定 |
| GKE Pod | ノードのデフォルト SA | Workload Identityで KSA に紐付け |
| Cloud Functions | App Engine デフォルト SA | 関数ごとに専用 SA |
- ローカル開発:
gcloud auth application-default login - GCP 内サービス: 専用 SA を割り当て + メタデータ経由 ADC
- GKE Pod: Workload Identity
- 外部 (GitHub / AWS): Workload Identity Federation
- その他特殊(オンプレ): SA impersonation(短期トークン発行)
リトライと指数バックオフ — 公式推奨パターン
HTTP / gRPC コードとリトライ可否
| HTTP | gRPC code | 意味 | リトライ |
|---|---|---|---|
| 400 | INVALID_ARGUMENT | 入力エラー | ❌ |
| 401 | UNAUTHENTICATED | 認証失敗 | ❌ |
| 403 | PERMISSION_DENIED | 権限不足 | ❌ |
| 404 | NOT_FOUND | 存在しない | ❌ |
| 408 | (DEADLINE_EXCEEDED) | タイムアウト | ✅ |
| 409 | ABORTED | 衝突(楽観ロック失敗等) | ✅ 条件付き |
| 429 | RESOURCE_EXHAUSTED | レート制限 | ✅(必ずバックオフ) |
| 500 | INTERNAL | サーバ内部エラー | ✅ 慎重に |
| 502 | (INTERNAL) | Bad Gateway | ✅ |
| 503 | UNAVAILABLE | 一時停止 | ✅ |
| 504 | DEADLINE_EXCEEDED | ゲートウェイタイムアウト | ✅ |
指数バックオフ + ジッターの実装
from google.api_core import retry
# Cloud Client Libraries の組み込みリトライ
my_retry = retry.Retry(
predicate=retry.if_exception_type(
google.api_core.exceptions.ServiceUnavailable, # 503
google.api_core.exceptions.DeadlineExceeded, # 504 / DEADLINE_EXCEEDED
google.api_core.exceptions.ResourceExhausted, # 429 / RESOURCE_EXHAUSTED
google.api_core.exceptions.Aborted, # 409 ABORTED
),
initial=1.0, # 初回 1秒
maximum=60.0, # 最大 60秒
multiplier=2.0, # 2倍ずつ
deadline=600.0, # 全体 10分まで
)
# クライアントメソッドに渡す
result = client.some_operation(request, retry=my_retry, timeout=30)
Full Jitter の自前実装(参考)
import random
import time
from typing import Callable, TypeVar
T = TypeVar("T")
def retry_with_jitter(
fn: Callable[[], T],
max_retries: int = 5,
initial_delay: float = 1.0,
max_delay: float = 60.0,
) -> T:
for attempt in range(max_retries):
try:
return fn()
except TransientError:
if attempt == max_retries - 1:
raise
# Full Jitter: random(0, min(max_delay, initial * 2^attempt))
delay = min(max_delay, initial_delay * (2 ** attempt))
sleep_time = random.uniform(0, delay)
time.sleep(sleep_time)
ジッター戦略の比較
No Jitter
delay × 2^attempt をそのまま待つ。複数クライアントが同時に再試行する Thundering Herd 問題。
Full Jitter
random(0, capped_delay)。クライアントを最も分散させる。AWS / GCP が推奨。
Equal Jitter
delay/2 + random(0, delay/2)。待ち時間に最低保証あり。
Decorrelated
前回の delay と独立にランダム。長期トレンドで均等に分散。
- 4xx もリトライ → 永久に成功しない(408 / 429 を除く)
- ジッターなしの即時リトライ → Thundering Herd で下流崩壊
- 非冪等 API (POST) のリトライ → 二重課金・二重投稿。Idempotency Key で防御
- 無限リトライ → 一過性でない障害で無限ループ。
deadline必須 - Circuit Breaker なしのリトライ → 下流のさらなる過負荷を引き起こす
ページネーション・バッチング・キャッシング
ページネーションの 3 スタイル
Offset / Limit
SELECT * FROM users
LIMIT 20 OFFSET 100
SQL の古典。深いページで遅い(OFFSET の手前を全部スキャン)
Cursor (nextPageToken)
response = client.list(
page_size=100, page_token=token)
token = response.next_page_token
GCP API の標準。サーバが次のトークンを返す。安定・スケーラブル。
Keyset / Seek
SELECT * FROM users
WHERE id > $last_id
ORDER BY id LIMIT 20
インデックスを活用、最速。ただしクライアント側で last_id 保持が必要。
Cloud Client Library の自動ページネーション
from google.cloud import storage
client = storage.Client()
bucket = client.bucket("my-bucket")
# パターン A: iterator が自動でページング(メモリ効率○)
for blob in bucket.list_blobs(page_size=100):
process(blob)
# パターン B: ページ単位で明示的に扱う
pages = bucket.list_blobs(page_size=100).pages
for page in pages:
print(f"Got a page of {len(list(page))} blobs")
for blob in page:
process(blob)
バッチング(Firestore の例)
# Firestore のバッチ書き込み(上限 500 文書)
batch = db.batch()
for i, doc in enumerate(docs):
if i > 0 and i % 500 == 0:
batch.commit() # 500 件ごとにコミット
batch = db.batch()
batch.set(db.collection("c").document(doc["id"]), doc)
batch.commit()
キャッシング (Cache-Aside パターン)
import json
import redis
cache = redis.Redis(host="MEMORYSTORE_IP", decode_responses=True)
TTL = 300 # 5分
def get_user(user_id: str) -> dict:
# 1. キャッシュを覗く
cached = cache.get(f"user:{user_id}")
if cached:
return json.loads(cached)
# 2. なければ DB から取得
user = db.collection("users").document(user_id).get().to_dict()
# 3. キャッシュに書く
cache.setex(f"user:{user_id}", TTL, json.dumps(user))
return user
キャッシュ Miss が同時多発 → 全クライアントが DB に殺到 → DB ダウン、を防ぐ:
- Probabilistic Early Expiration: TTL の前にランダムに再生成(XFetch アルゴリズム)
- Lock + Refresh: 1プロセスだけが再生成、他は古い値を返す(Redlock)
- Stale-While-Revalidate: TTL 切れでも古い値を返しつつ非同期に更新
4.3 トラブルシューティングと可観測性
OpenTelemetry → Cloud Trace 計装
OpenTelemetry (OTel) はベンダーニュートラルな計装フレームワーク。書いたコードを Cloud Trace、Datadog、NewRelic 等にエクスポート可能。
Python での OTel 自動計装
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.cloud_trace import CloudTraceSpanExporter
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.propagators.cloud_trace_propagator import CloudTraceFormatPropagator
from opentelemetry import propagate
# Tracer Provider
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(CloudTraceSpanExporter(project_id="PROJECT_ID"))
)
# W3C Trace Context(標準)+ GCP X-Cloud-Trace-Context(互換)
propagate.set_global_textmap(CloudTraceFormatPropagator())
# Flask / requests を自動計装
app = Flask(__name__)
FlaskInstrumentor().instrument_app(app) # 各エンドポイントが自動 span
RequestsInstrumentor().instrument() # requests ライブラリの呼び出しも自動 span
# 手動計装
tracer = trace.get_tracer(__name__)
@app.route("/orders/")
def get_order(id):
with tracer.start_as_current_span("get_order_details") as span:
span.set_attribute("order.id", id)
order = db.collection("orders").document(id).get()
span.set_attribute("order.total", order.get("total"))
return order.to_dict()
W3C Trace Context によるサービス境界の伝播
traceparent ヘッダの構造
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^ ^^
| | | |
| | | +-- flags (01 = sampled)
| | +-- parent-id (span-id, 16 hex)
| +-- trace-id (32 hex, リクエスト全体で同じ)
+-- version (00)
構造化ログ(Cloud Logging)
JSON で stdout に出力するだけで、Cloud Logging が severity / trace / labels / httpRequest を自動認識する。
特殊フィールド一覧
| フィールド | 説明 |
|---|---|
severity | DEBUG / INFO / NOTICE / WARNING / ERROR / CRITICAL / ALERT / EMERGENCY |
message | メイン本文(テキスト) |
logging.googleapis.com/trace | projects/PROJECT/traces/TRACE_ID 形式 |
logging.googleapis.com/spanId | 16 hex の span ID |
logging.googleapis.com/trace_sampled | true / false |
logging.googleapis.com/labels | ユーザー定義ラベル {key: value} |
logging.googleapis.com/insertId | 重複排除用 |
httpRequest | {requestMethod, requestUrl, status, latency, ...} |
Python での実装例(OTel と統合)
import json
import sys
from opentelemetry import trace
PROJECT_ID = "my-project"
def log_structured(severity: str, message: str, **kwargs):
span = trace.get_current_span()
ctx = span.get_span_context() if span else None
entry = {
"severity": severity,
"message": message,
**kwargs,
}
if ctx and ctx.is_valid:
entry["logging.googleapis.com/trace"] = f"projects/{PROJECT_ID}/traces/{ctx.trace_id:032x}"
entry["logging.googleapis.com/spanId"] = f"{ctx.span_id:016x}"
entry["logging.googleapis.com/trace_sampled"] = bool(ctx.trace_flags & 0x01)
print(json.dumps(entry), file=sys.stdout, flush=True)
# 使い方
log_structured("INFO", "User logged in", user_id="u1", method="oauth")
log_structured("ERROR", "Payment failed", order_id="o123",
error_code="CARD_DECLINED")
# 特定ユーザーの ERROR 以上を検索
severity>=ERROR
resource.type="cloud_run_revision"
resource.labels.service_name="my-service"
timestamp >= "2026-05-28T00:00:00Z"
jsonPayload.user_id = "u1"
# trace ID で全 logs を見る
trace="projects/my-project/traces/abc123..."
ログから Cloud Trace への遷移
構造化ログに logging.googleapis.com/trace を含めると、Cloud Logging UI から「View in Trace」ボタンが出現。1クリックで該当 trace の全 spans を可視化できる。
Cloud Logging のコスト最適化
Exclusion Filter
Log Router で「DEBUG ログを破棄」「health-check ログを破棄」等の条件でログを保存前に捨てる。Cloud Logging のストレージコストを直接削減。
Sink to BigQuery / GCS
長期保管必要なログを BigQuery / GCS にエクスポート。Cloud Logging 自体は短期(30日)保存に。BigQuery でクエリ、GCS で安価アーカイブ。
Bucket 別 retention
Log Bucket を複数作り、用途別に retention period を設定(重要ログ 1年、低価値ログ 7日 等)。
Error Reporting
同じスタックトレースを1つのインシデントにグルーピングし、メール / Slack / Pub/Sub に通知。
import logging
import sys
import traceback
import json
import google.cloud.logging
# Cloud Logging ハンドラ
client = google.cloud.logging.Client()
client.setup_logging()
# 未捕捉例外を自動キャプチャ
def excepthook(exc_type, exc_value, exc_tb):
logging.error(
"Uncaught exception",
extra={"json_fields": {"stack_trace": "".join(traceback.format_exception(exc_type, exc_value, exc_tb))}}
)
sys.excepthook = excepthook
# ERROR ログ + stack_trace が Error Reporting で集約される
try:
risky_operation()
except Exception:
logging.error(
"Payment processing failed",
exc_info=True, # 自動でスタックトレース付与
extra={"json_fields": {"order_id": "o123"}}
)
Cloud Profiler との関係
Cloud Trace
分散レイテンシの可視化。サービス境界の遅延箇所特定。
Cloud Profiler
関数単位の CPU/メモリ hotspot。「どの関数で時間/メモリを使っているか」を継続観測(< 1% オーバーヘッド)。
Error Reporting
未捕捉例外の集約・通知。同一スタックトレースを1インシデント化。
Cloud Monitoring
メトリクス・アラート・ダッシュボード。SLO / Error Budget。
🚨 事故ケース集(postmortem 形式)
Cloud Run の各インスタンスが DB プール 20 を持ち、max-instances=100 で接続数 2000 → Cloud SQL の max_connections に到達
セール開始 5分後、新規リクエストが 500 を返し始める。Cloud SQL のログに FATAL: sorry, too many clients already。
- 各 Cloud Run インスタンスでアプリ起動時に SQLAlchemy プール pool_size=20 を作成
- max-instances=100 のため、ピーク時に 100 × 20 = 2,000 接続が瞬時に開く
- Cloud SQL の
max_connections=100(デフォルト)に対して 20倍超過 - 結果、新規接続が拒否され、既存リクエストも応答不能
セール開始から 15分間、全注文不能。機会損失 約 $80K。緊急対応で 1時間後復旧。
- Cloud SQL のフラグで
max_connections=500に増やす(インスタンスサイズに応じた上限) - アプリのプールサイズを
pool_size=3, max_overflow=2に削減 - または PgBouncer を Cloud Run のサイドカーで導入(接続プーリングを集約)
- max-instances を 50 に下げてリトライ余地を確保
- 事前計算:
max_instances × pool_size + 余裕分 ≤ max_connections × 0.7 - PgBouncer / Cloud SQL Connection Pooling を Cloud Run サイドカーで導入
- Cloud SQL の接続数モニタをダッシュボード化、80% で警告
- 本番リリース前に max-instances 想定値で負荷試験
「即時 5回リトライ」の自前実装 → 下流サービス障害時に DDoS 化して連鎖障害
BigQuery が一時的に 503 を返した瞬間、自社の全 Cloud Run インスタンスが BigQuery を秒間 1万 RPS で叩き始め、復旧を阻害。Google Cloud サポートから「異常なクエリ量を検知」の連絡。
- 独自実装のリトライ:
for i in range(5): time.sleep(0.1); retry() - ジッターなし、間隔 0.1秒固定
- 全クライアントが同期して再試行 → Thundering Herd
- BigQuery が回復しようとしてもリトライ嵐で復旧不能
下流サービスの障害時間が 30秒だったところ、自社のリトライ嵐により 5分に延長。Google Cloud から quota 違反警告。
- 自前リトライを削除し、Cloud Client Libraries の組み込みリトライに置換
- または
tenacity等の標準ライブラリで Full Jitter + 指数バックオフ - Circuit Breaker パターンも追加(pybreaker)
- 独自リトライ実装は禁止。必ずCloud Client Libraries / tenacity / Resilience4jを使用
- 必ず指数バックオフ + Full Jitter(AWS / GCP 公式推奨)
- Circuit Breaker で下流の連続障害時にリトライを止める
- カオステストで下流障害時の自社挙動を検証
本番で DEBUG ログを出しっぱなし + 構造化ログのフィールド肥大 → ログ取り込み量が予算の 10倍
月末の Cloud Billing で「Cloud Logging」のコストが予算の 10倍。これまで $200/月だったのが $2,000/月に膨張。
- 新リリースで DEBUG ログ(DB クエリ全文、HTTP リクエストボディ)を出力する設定が混入
- 1リクエストあたり 5KB → 1日 100M リクエスト = 500GB/日
- Cloud Logging は取り込み量で課金($0.5/GB)
- Exclusion Filter も設定なしで、全量が課金対象
月 $1,800 の予算オーバー。3ヶ月で $5,400 の無駄な支出。発見が遅れて累積。
- 本番ログレベルを INFO 以上に設定
- Log Router の Exclusion Filter で
severity=DEBUGを破棄 - HTTP リクエストボディはログに残さない(PII 観点でも危険)
- 長期保管が必要なログは Sink で BigQuery / GCS にエクスポート
- 環境変数
LOG_LEVEL=INFOを本番のデフォルトに、設定をハードコードしない - Cloud Logging の取り込み量メトリックに予算アラート(前週比 2倍で発火)
- Exclusion Filter を予防的に整備(health-check、debug、verbose ライブラリログ)
- 機密データ(PII、シークレット)をログに残さない Lint ルール
Pub/Sub 経由のサービス間呼び出しで Trace Context 伝播が切れた → 分散トレースが追えない
本番障害時、Cloud Trace で「リクエスト全体の流れ」を追おうとすると、Pub/Sub 経由のサービス間で trace が切れて断片的にしか見えない。どこで遅延しているか特定に時間を要した。
- HTTP 経由の通信は
RequestsInstrumentorで自動 trace 伝播 - Pub/Sub 経由の通信は手動で attributes に traceparent を入れる必要がある
- publisher 側で trace context を attributes に積んでいなかった
- subscriber 側でも attributes から取り出して新しい trace の親に設定していなかった
障害原因特定に通常 30分のところ、4時間を要した。原因が「Pub/Sub の遅延」と分かったが、トレースで見れなかったために手作業のログ照合が必要だった。
- publisher で trace context を attributes に積む:
attributes={"traceparent": header_value} - subscriber で attributes から取り出し、
OpenTelemetry の propagatorで trace context を復元 - OpenTelemetry Pub/Sub Instrumentation を導入(自動化)
- 非 HTTP 通信(Pub/Sub、Kafka、gRPC streaming)では明示的な Trace Context 伝播が必要と認識
opentelemetry-instrumentation-pubsubなど各通信プロトコル向けの Instrumentation を導入- 分散トレース要件をシステム設計フェーズで決め、コードレビューで Context 伝播を確認
- カオステストで「ある span が見えない」状態をテスト
「全レコード取得 → メモリでフィルタ」のコード → 100万件で OOMKilled、Pod 強制終了
レポート生成バッチが「メモリ不足で OOMKilled」され、レポート未生成。
- 開発時にデータ 1万件だったので
list(bucket.list_blobs())で全件メモリ展開 - 本番で 100万件に成長 → メモリ 2GB 制限を超過 → OOMKilled
- ページネーション iterator を活用していれば 100万件でも処理可能
日次レポート 1日欠落。顧客報告の遅延。
list(...)をやめて iterator で逐次処理:for blob in bucket.list_blobs(page_size=1000):- 巨大データの集計はBigQuery にロードして SQL 集計に切り替え
- すべての list API はiterator パターンで処理(メモリ効率○)
- 負荷試験で「本番想定データ量」をテスト
- 大量データの集計は BigQuery / Dataflow に外出し
- OOMKilled の Pod に Cloud Monitoring アラート
すべてのエラーを Slack に通知 → アラート疲労で重要エラーを見逃した
「決済 API の認可エラー(CRITICAL)」が 3時間気付かれず放置。CS から「決済できない」の苦情多数。
- Error Reporting + Cloud Monitoring から Slack の
#alertsチャンネルに全エラーを通知 - 1日 500件以上のアラート(軽微なバリデーションエラー含む)
- エンジニアは「常にミュート」状態になり、重要アラートも気づかなかった
- Alert Fatigue(アラート疲労)の典型例
3時間の決済不能。約 200 件の注文失敗。顧客対応 + 売上機会損失 約 $30K。
- Slack 通知を重要度別に分割: #alerts-critical、#alerts-warning、#alerts-info
- 軽微なエラー(4xx ユーザー入力エラー等)は Slack 通知ではなくダッシュボードのみ
- SLO ベースのアラート(Error Budget 消費率)に切り替え
- Critical アラートは PagerDuty / OnCall 通知に
- アラートは「人間が行動を取る必要があるもの」に絞る
- 重要度ごとに通知チャンネル / 経路を分ける(Critical=PagerDuty、Warning=Slack、Info=Dashboard)
- SLO + Error Budget ベースのアラート設計(小さな失敗は budget で吸収)
- アラート発火頻度を定期レビュー、ノイズを継続的に削減
非冪等な決済 API を 503 で自動リトライ → 顧客に二重請求
顧客から「同じ商品で 2回課金されている」の通報が複数件。決済代行 API のログを確認すると、同一注文の決済リクエストが 2回送信されていた。
- 決済 API(外部 SaaS)が一時的に 503 を返した
- Cloud Client Libraries の組み込みリトライが「503 はリトライ可能」と判断し再送
- しかし決済 API は非冪等。同じリクエストを 2回送ると 2回課金
- 1回目の 503 レスポンスは「実は決済は成功していた」だった
50件以上の二重課金。返金処理 + 顧客謝罪。決済代行のレピュテーション低下。
- 決済 API 呼び出しにIdempotency Key を付与(注文 ID をキーに)
- 決済 API がそのキーを認識し、同一キーの 2回目以降は前回結果を返す
- 5xx エラー時は即座にリトライせず、決済状況の確認 API でステータス再取得
- 非冪等 API のリトライはIdempotency Key を必須化
- POST API は基本的に冪等性を確保する設計(注文 ID + 確認ステップ)
- 外部 API ドキュメントで「冪等性サポート」を必ず確認
- 本番リリース前に「リトライ時の二重処理」のテストケースを追加
セクション4 試験戦略まとめ
- Cloud SQL Auth Proxy:
--add-cloudsql-instances+ Unix Socket、IAM 認証 + TLS - ADC 優先順位: 環境変数 → gcloud → メタデータサーバ
- 指数バックオフ + ジッター: Full Jitter 推奨、Cloud Client Libraries 標準サポート
- リトライ対象: 5xx / 408 / 429 / ネットワーク。4xx は不可
- ページネーション: iterator が自動でページング
- OpenTelemetry + Cloud Trace: ベンダーニュートラル計装の標準
- W3C Trace Context:
traceparentヘッダで trace-id 伝播 - 構造化ログ: JSON +
logging.googleapis.com/traceで Trace 連携 - Error Reporting: ERROR + スタックトレース → 自動グルーピング
- Cloud Logging コスト削減: Exclusion + Sink
- 「Cloud Run → Cloud SQL の推奨接続」 → Cloud SQL Auth Proxy(
--add-cloudsql-instances) - 「503 が時々発生」 → 指数バックオフ + ジッター
- 「4xx のリトライ」 → 不正解(408 / 429 を除く)
- 「サービス境界の遅延を可視化」 → OpenTelemetry + Cloud Trace + Trace Context
- 「ログから Trace への遷移」 →
logging.googleapis.com/trace - 「大量データの list で OOM」 → iterator のページネーション
- 「未捕捉例外の自動通知」 → Error Reporting + 通知チャンネル
- 「本番でも継続プロファイリング」 → Cloud Profiler(< 1% オーバーヘッド)
- 「Cloud Logging のコスト削減」 → Exclusion Filter + Sink
- 「SA Key を発行しない」 → ADC + メタデータサーバ / Workload Identity / WIF