PCD 合格対策

セクション4:Google Cloud サービスとの統合

公式試験ガイド 042426版 — Section 4: Integrating applications with Google Cloud services

21%
出題比重
3
サブトピック
~10問
想定問数
広く浅く
出題傾向
🎯 このセクションの核心 アプリと GCP サービスの「つなぎ込み」を扱う。Cloud Client Libraries の堅牢なエラーハンドリング(指数バックオフ)と OpenTelemetry + Cloud Trace による可観測性が肝。広く浅く問われ、用語と典型パターンを覚えれば確実に得点できる。

4.1 データ・ストレージサービスとの統合

Cloud SQL 接続:Auth Proxy vs Connector

Cloud Run / GKE / GCE から Cloud SQL に接続する方法には、Auth Proxy(プロセス分離)Connector ライブラリ(プロセス内)の2つがある。

パターン A: Auth Proxy(プロセス分離) App コンテナ DB 接続 localhost:5432 Auth Proxy サイドカー TLS + IAM 認証 TLS Cloud SQL PostgreSQL / MySQL パターン B: Connector ライブラリ App コンテナ cloud-sql-python-connector プロセス内で TLS + 認証 外部プロセス不要 TLS Cloud SQL PostgreSQL / MySQL
図1: Auth Proxy(左)は別プロセス、Connector(右)はアプリ内ライブラリ。どちらも IAM 認証 + TLS 暗号化。

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-instancesCloud Run が自動でサイドカー注入
GKEAuth Proxy Operatorカスタムリソースで自動サイドカー
Python / Java / GoConnector ライブラリ(言語サポートあり)プロセス分離不要、コードシンプル
Compute EngineAuth Proxy(プロセス常駐)複数アプリで共有可能
Cloud SQL 接続の落とし穴
  1. Auth Proxy はコネクションプーリング機能を持たない。アプリ側で SQLAlchemy / HikariCP 等のプールを実装
  2. HA フェイルオーバー時に既存接続が切れる。再接続ロジック必須
  3. Public IP に直接接続はアンチパターン(パスワードのみで TLS なし可能性)
  4. Private IP + Auth Proxyでも Auth Proxy のプロセスは VPC に到達できる必要あり(Direct VPC Egress / Serverless VPC Access)
  5. port 3307 (CSQL Auth Proxy) と 5432/3306 を混同しない(Proxy は内部的に 3307 で API ゲートウェイに接続)
公式リファレンス Cloud SQL Auth Proxy / Cloud Run + Cloud SQL

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())
Firestore の制約と落とし穴
  1. ドキュメントサイズ上限 1MB。大きい値は GCS にオフロード
  2. 1 文書あたり 1秒に 1書き込み(hot doc 問題)。連番カウンタは複数文書に分散
  3. 複合クエリには複合インデックス必要。Console で自動生成または手動定義
  4. OR 条件は複数クエリでクライアント側マージ(Firestore は単一クエリ AND のみ)
  5. 配列の 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) の優先順位

ADC が認証情報を探す順序 ① 環境変数 GOOGLE_APPLICATION_CREDENTIALS = /path/to/service-account-key.json(ローカル開発・特殊ケース) ② gcloud auth application-default login ~/.config/gcloud/application_default_credentials.json(ローカル開発) ③ メタデータサーバ(GCP 上で実行中) 169.254.169.254/computeMetadata/... から SA token を自動取得 本番(Cloud Run / GKE / GCE / Functions)は ③ のみ使う = SA Key 不要! サービスに専用 SA を割り当てるだけ
図2: ADC は 環境変数 → gcloud → メタデータ の順で探す。本番では SA をサービスに割り当てるのが正解。

サービスごとのデフォルト SA

サービスデフォルト SA推奨
Compute EnginePROJECT_NUMBER-compute@developer.gserviceaccount.com必ず専用 SAを割り当て直す
Cloud RunCompute デフォルト SA(権限過多)--service-account明示指定
GKE Podノードのデフォルト SAWorkload Identityで KSA に紐付け
Cloud FunctionsApp Engine デフォルト SA関数ごとに専用 SA
SA Key を発行しないための代替手段
  1. ローカル開発: gcloud auth application-default login
  2. GCP 内サービス: 専用 SA を割り当て + メタデータ経由 ADC
  3. GKE Pod: Workload Identity
  4. 外部 (GitHub / AWS): Workload Identity Federation
  5. その他特殊(オンプレ): SA impersonation(短期トークン発行)

リトライと指数バックオフ — 公式推奨パターン

HTTP / gRPC コードとリトライ可否

HTTPgRPC code意味リトライ
400INVALID_ARGUMENT入力エラー
401UNAUTHENTICATED認証失敗
403PERMISSION_DENIED権限不足
404NOT_FOUND存在しない
408(DEADLINE_EXCEEDED)タイムアウト
409ABORTED衝突(楽観ロック失敗等)✅ 条件付き
429RESOURCE_EXHAUSTEDレート制限✅(必ずバックオフ)
500INTERNALサーバ内部エラー✅ 慎重に
502(INTERNAL)Bad Gateway
503UNAVAILABLE一時停止
504DEADLINE_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 問題。

❌ 単独運用以外は避ける

Equal Jitter

delay/2 + random(0, delay/2)。待ち時間に最低保証あり。

⚠️ Full より衝突しやすい

Decorrelated

前回の delay と独立にランダム。長期トレンドで均等に分散。

✅ AWS のドキュメントで推奨
リトライの致命的アンチパターン
  1. 4xx もリトライ → 永久に成功しない(408 / 429 を除く)
  2. ジッターなしの即時リトライ → Thundering Herd で下流崩壊
  3. 非冪等 API (POST) のリトライ → 二重課金・二重投稿。Idempotency Key で防御
  4. 無限リトライ → 一過性でない障害で無限ループ。deadline 必須
  5. Circuit Breaker なしのリトライ → 下流のさらなる過負荷を引き起こす

ページネーション・バッチング・キャッシング

ページネーションの 3 スタイル

Offset / Limit

SELECT * FROM users
LIMIT 20 OFFSET 100

SQL の古典。深いページで遅い(OFFSET の手前を全部スキャン)

⚠️ 大量データは不向き

Keyset / Seek

SELECT * FROM users
WHERE id > $last_id
ORDER BY id LIMIT 20

インデックスを活用、最速。ただしクライアント側で last_id 保持が必要。

✅ 自前 API でのベストパフォーマンス

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
キャッシュ Stampede(Thundering Herd)対策

キャッシュ 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 等にエクスポート可能。

アプリケーション OTel SDK spans / metrics / logs OTel Collector (オプション、推奨) Receiver: OTLP Processor: batch, filter Exporter: GCP, etc. バッファ・サンプリング・ 変換を一元化 Cloud Trace spans の可視化 Cloud Logging 構造化ログ Cloud Monitoring
図3: OTel のアーキテクチャ。アプリは OTel SDK で計装、Collector が中継、Cloud Trace/Logging/Monitoring に配信。

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 によるサービス境界の伝播

Service A (Cloud Run) trace-id T 生成 span S1 開始 HTTP リクエスト送信時に traceparent ヘッダ追加 RequestsInstrumentor が 自動付与 HTTP Request traceparent: 00-T-S1-01 tracestate: ... W3C 標準形式 Service B (Cloud Run) FlaskInstrumentor が ヘッダ解析 trace-id T で span S2 を作る 親 = S1 として
図4: W3C Trace Context(traceparent ヘッダ)で trace-id をサービス境界に伝播。Cloud Trace で全 spans が 1つの trace として可視化される。

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 を自動認識する。

特殊フィールド一覧

フィールド説明
severityDEBUG / INFO / NOTICE / WARNING / ERROR / CRITICAL / ALERT / EMERGENCY
messageメイン本文(テキスト)
logging.googleapis.com/traceprojects/PROJECT/traces/TRACE_ID 形式
logging.googleapis.com/spanId16 hex の span ID
logging.googleapis.com/trace_sampledtrue / 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")
Cloud Logging のクエリ例
# 特定ユーザーの 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日 等)。

公式リファレンス Structured logging / Cloud Trace setup

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 形式)

GCP サービス統合の事故は 「設定の見落とし」「リトライ戦略の不在」「可観測性の欠如」に起因します。Cloud SQL / Cloud Client Libraries / OpenTelemetry / Cloud Logging / Memorystore で頻発する事故パターンを紹介します。
CASE 4.1 Cloud SQL / コネクションプール SEV-CRITICAL

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時間後復旧。

▼ 復旧手順
  1. Cloud SQL のフラグで max_connections=500 に増やす(インスタンスサイズに応じた上限)
  2. アプリのプールサイズを pool_size=3, max_overflow=2 に削減
  3. または PgBouncer を Cloud Run のサイドカーで導入(接続プーリングを集約)
  4. max-instances を 50 に下げてリトライ余地を確保
▼ 予防策
  • 事前計算: max_instances × pool_size + 余裕分 ≤ max_connections × 0.7
  • PgBouncer / Cloud SQL Connection Pooling を Cloud Run サイドカーで導入
  • Cloud SQL の接続数モニタをダッシュボード化、80% で警告
  • 本番リリース前に max-instances 想定値で負荷試験
CASE 4.2 Cloud Client Libraries SEV-HIGH

「即時 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 違反警告。

▼ 復旧手順
  1. 自前リトライを削除し、Cloud Client Libraries の組み込みリトライに置換
  2. または tenacity 等の標準ライブラリで Full Jitter + 指数バックオフ
  3. Circuit Breaker パターンも追加(pybreaker)
▼ 予防策
  • 独自リトライ実装は禁止。必ずCloud Client Libraries / tenacity / Resilience4jを使用
  • 必ず指数バックオフ + Full Jitter(AWS / GCP 公式推奨)
  • Circuit Breaker で下流の連続障害時にリトライを止める
  • カオステストで下流障害時の自社挙動を検証
CASE 4.3 Cloud Logging SEV-MEDIUM

本番で 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 の無駄な支出。発見が遅れて累積。

▼ 復旧手順
  1. 本番ログレベルを INFO 以上に設定
  2. Log Router の Exclusion Filter で severity=DEBUG を破棄
  3. HTTP リクエストボディはログに残さない(PII 観点でも危険)
  4. 長期保管が必要なログは Sink で BigQuery / GCS にエクスポート
▼ 予防策
  • 環境変数 LOG_LEVEL=INFO を本番のデフォルトに、設定をハードコードしない
  • Cloud Logging の取り込み量メトリックに予算アラート(前週比 2倍で発火)
  • Exclusion Filter を予防的に整備(health-check、debug、verbose ライブラリログ)
  • 機密データ(PII、シークレット)をログに残さない Lint ルール
CASE 4.4 OpenTelemetry / Cloud Trace SEV-MEDIUM

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 の遅延」と分かったが、トレースで見れなかったために手作業のログ照合が必要だった。

▼ 復旧手順
  1. publisher で trace context を attributes に積む:attributes={"traceparent": header_value}
  2. subscriber で attributes から取り出し、OpenTelemetry の propagator で trace context を復元
  3. OpenTelemetry Pub/Sub Instrumentation を導入(自動化)
▼ 予防策
  • 非 HTTP 通信(Pub/Sub、Kafka、gRPC streaming)では明示的な Trace Context 伝播が必要と認識
  • opentelemetry-instrumentation-pubsub など各通信プロトコル向けの Instrumentation を導入
  • 分散トレース要件をシステム設計フェーズで決め、コードレビューで Context 伝播を確認
  • カオステストで「ある span が見えない」状態をテスト
CASE 4.5 Cloud Storage / Firestore SEV-MEDIUM

「全レコード取得 → メモリでフィルタ」のコード → 100万件で OOMKilled、Pod 強制終了

▼ 症状

レポート生成バッチが「メモリ不足で OOMKilled」され、レポート未生成。

▼ 原因
  • 開発時にデータ 1万件だったので list(bucket.list_blobs()) で全件メモリ展開
  • 本番で 100万件に成長 → メモリ 2GB 制限を超過 → OOMKilled
  • ページネーション iterator を活用していれば 100万件でも処理可能
▼ 影響

日次レポート 1日欠落。顧客報告の遅延。

▼ 復旧手順
  1. list(...) をやめて iterator で逐次処理:for blob in bucket.list_blobs(page_size=1000):
  2. 巨大データの集計はBigQuery にロードして SQL 集計に切り替え
▼ 予防策
  • すべての list API はiterator パターンで処理(メモリ効率○)
  • 負荷試験で「本番想定データ量」をテスト
  • 大量データの集計は BigQuery / Dataflow に外出し
  • OOMKilled の Pod に Cloud Monitoring アラート
CASE 4.6 Error Reporting / Cloud Monitoring SEV-MEDIUM

すべてのエラーを Slack に通知 → アラート疲労で重要エラーを見逃した

▼ 症状

「決済 API の認可エラー(CRITICAL)」が 3時間気付かれず放置。CS から「決済できない」の苦情多数。

▼ 原因
  • Error Reporting + Cloud Monitoring から Slack の #alerts チャンネルに全エラーを通知
  • 1日 500件以上のアラート(軽微なバリデーションエラー含む)
  • エンジニアは「常にミュート」状態になり、重要アラートも気づかなかった
  • Alert Fatigue(アラート疲労)の典型例
▼ 影響

3時間の決済不能。約 200 件の注文失敗。顧客対応 + 売上機会損失 約 $30K。

▼ 復旧手順
  1. Slack 通知を重要度別に分割: #alerts-critical、#alerts-warning、#alerts-info
  2. 軽微なエラー(4xx ユーザー入力エラー等)は Slack 通知ではなくダッシュボードのみ
  3. SLO ベースのアラート(Error Budget 消費率)に切り替え
  4. Critical アラートは PagerDuty / OnCall 通知に
▼ 予防策
  • アラートは「人間が行動を取る必要があるもの」に絞る
  • 重要度ごとに通知チャンネル / 経路を分ける(Critical=PagerDuty、Warning=Slack、Info=Dashboard)
  • SLO + Error Budget ベースのアラート設計(小さな失敗は budget で吸収)
  • アラート発火頻度を定期レビュー、ノイズを継続的に削減
CASE 4.7 Cloud Client Libraries / 決済 API SEV-CRITICAL

非冪等な決済 API を 503 で自動リトライ → 顧客に二重請求

▼ 症状

顧客から「同じ商品で 2回課金されている」の通報が複数件。決済代行 API のログを確認すると、同一注文の決済リクエストが 2回送信されていた。

▼ 原因
  • 決済 API(外部 SaaS)が一時的に 503 を返した
  • Cloud Client Libraries の組み込みリトライが「503 はリトライ可能」と判断し再送
  • しかし決済 API は非冪等。同じリクエストを 2回送ると 2回課金
  • 1回目の 503 レスポンスは「実は決済は成功していた」だった
▼ 影響

50件以上の二重課金。返金処理 + 顧客謝罪。決済代行のレピュテーション低下。

▼ 復旧手順
  1. 決済 API 呼び出しにIdempotency Key を付与(注文 ID をキーに)
  2. 決済 API がそのキーを認識し、同一キーの 2回目以降は前回結果を返す
  3. 5xx エラー時は即座にリトライせず、決済状況の確認 API でステータス再取得
▼ 予防策
  • 非冪等 API のリトライはIdempotency Key を必須化
  • POST API は基本的に冪等性を確保する設計(注文 ID + 確認ステップ)
  • 外部 API ドキュメントで「冪等性サポート」を必ず確認
  • 本番リリース前に「リトライ時の二重処理」のテストケースを追加
🎯 セクション4 事故ケースの学び GCP サービス統合の事故は「呼び出す側の責任」。リトライ / 冪等性 / 接続プール / Trace 伝播 / ログコスト / アラート設計 はすべてアプリ側で適切に実装する必要があります。試験では「API 呼び出しでのベストプラクティス」「分散トレースの計装」「ログコスト最適化」が頻出。

セクション4 試験戦略まとめ

🎯 21%を確実に取るためのポイント
  1. Cloud SQL Auth Proxy: --add-cloudsql-instances + Unix Socket、IAM 認証 + TLS
  2. ADC 優先順位: 環境変数 → gcloud → メタデータサーバ
  3. 指数バックオフ + ジッター: Full Jitter 推奨、Cloud Client Libraries 標準サポート
  4. リトライ対象: 5xx / 408 / 429 / ネットワーク。4xx は不可
  5. ページネーション: iterator が自動でページング
  6. OpenTelemetry + Cloud Trace: ベンダーニュートラル計装の標準
  7. W3C Trace Context: traceparent ヘッダで trace-id 伝播
  8. 構造化ログ: JSON + logging.googleapis.com/trace で Trace 連携
  9. Error Reporting: ERROR + スタックトレース → 自動グルーピング
  10. 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
🎉 全セクション学習完了 ここまで Section 1〜4 を学んだら、問題演習で実力を測り、模擬試験1模擬試験2で本番形式の練習を。70% 以上で受験本決め!
📘 基礎 (MD) 🔧 応用 (MD) 🎯 要点と暗記 (MD)
← セクション3 問題演習 →