5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloud Run の Probe 設計:gRPC 未実装・Distroless・ポートなし Job、それぞれの代替策

5
Posted at

はじめに

Google Cloud で構築したサーバレスコンテナの Probe 設計を行った際の話です。
「gRPC サービスだから gRPC プローブでいいよね」と設計を始めたら、3 日後には全部作り直していました。😇

システム移行プロジェクトで Cloud Run のプロセス・ポート監視を設計したときの話です。最初は「各リソースにプローブを設定するだけ」のつもりでしたが、実装を掘り下げるたびに何かが出てきました。gRPC Health Check 未実装、Distroless でコマンド実行不可、Node.js Worker Pool は HTTP サーバなし、バッチ Job はポート自体がない。

この記事は、その 4 つの制約にぶつかるたびに何を判断したかの記録です。

先にまとめ

Cloud Run Service    → Startup + Liveness Probe(gRPC or TCP Socket or HTTP GET)
Cloud Run Worker Pool → Startup + Liveness Probe(HTTP GET が前提、ポートがなければ実装必要)
Cloud Run Job        → Probe なし、ジョブ失敗ログ監視で代替
Compute Engine (MIG) → MIG オートヒーリングヘルスチェック(本記事では詳細割愛)

プローブ種別は以下の基準で選びます。

状況 選択するプローブ種別
gRPC Health Checking Protocol が実装済み gRPC プローブ
gRPC サーバが動いているが HealthService 未登録 TCP Socket プローブ(gRPC ポート)
HTTP エンドポイント(/healthz 等)がある HTTP GET プローブ
ポートが存在しない(バッチ処理等) プローブなし → ログ監視で代替
distroless コンテナ Exec プローブ禁止(上記3択のいずれか)

設定値の設計ルールは以下の通りです。

Startup Probe:  failureThreshold × periodSeconds ≥ アプリの最大起動時間
Liveness Probe: failureThreshold × periodSeconds = 誤検知を避けつつ迅速に検知できる判定時間

Cloud Run のリソース種別と Probe の基礎

3 種類のリソースと Probe の適用可否

Cloud Run には Service / Worker Pool / Job の 3 種類があり、それぞれ Probe の適用方法が異なります。

リソース種別 特徴 Startup Probe Liveness Probe
Service HTTP リクエスト駆動。コンテナが常時稼働
Worker Pool Pub/Sub などのメッセージをプル処理。コンテナが常時稼働
Job 1 回限りの実行。完了後にコンテナが終了 △(ポートがあれば設定可能)

Startup Probe と Liveness Probe の役割分担

コンテナ起動
    │
    ├─ Startup Probe 開始(failureThreshold に達するまでトラフィック遮断)
    │       │
    │       ├─ 成功 → Liveness Probe 開始(稼働中の定期チェック)
    │       │               │
    │       │               ├─ 成功が続く → 正常稼働継続
    │       │               └─ failureThreshold 到達 → コンテナ再起動
    │       │
    │       └─ failureThreshold 到達 → コンテナ終了(FAILED)
    │
    └─ Startup Probe が通るまで Liveness Probe は開始されない

Startup Probe が通るまで Liveness Probe は動かない、というのは意外と見落とされやすい仕様です。起動に時間のかかる Java アプリケーションに Startup Probe を設定しないと、Liveness Probe が起動中のコンテナを「ハング」と誤判定して再起動ループに陥ります。

Cloud Run Service 編:gRPC 未実装の現実に向き合う

当初の設計

プロジェクトの gRPC バックエンドサービスは gRPC サーバ(port 9090)で公開していました。当然、最初は gRPC プローブを設計しました。

# 当初の設計(動かなかった)
startupProbe:
  grpc:
    port: 9090
  initialDelaySeconds: 10
  periodSeconds: 3
  failureThreshold: 5

gRPC プローブは、コンテナ内で gRPC Health Checking Protocolgrpc.health.v1.Health/Check RPC)を実装していることが前提です。

実装を確認したら未実装だった

pom.xml と Java ソースを調べた結果:

grep -r "grpc.health" ./app --include="*.java" --include="*.properties"
→ 0 件

grep -r "spring-boot-starter-actuator" ./app --include="pom.xml"
→ 0 件

grpc-services 依存が存在せず、HealthServiceImpl の登録もなし。gRPC プローブを設定しても、gRPC サーバは Health Check RPC に応答できないため、プローブは常に失敗します。

よく誤解されますが、「gRPC サーバが動いている」ことと「gRPC Health Checking Protocol に対応している」は別の話です。後者には明示的な実装が必要です。

TCP Socket プローブへの切り替え

gRPC Health Checking Protocol の実装はアプリ実装する必要があります。リリースまでの時間が限られていたため、アプリ変更なしで即時対応できる TCP Socket プローブに切り替えました。

# 変更後(TCP Socket に切り替え)
startupProbe:
  tcpSocket:
    port: 9090  # gRPC サーバが listen しているポートをそのまま使用
  initialDelaySeconds: 0
  periodSeconds: 10
  failureThreshold: 12  # 120 秒の起動窓
livenessProbe:
  tcpSocket:
    port: 9090
  initialDelaySeconds: 0
  periodSeconds: 15
  failureThreshold: 3  # 45 秒で再起動判定
  timeoutSeconds: 5

TCP Socket プローブは「ポートに接続できるか」しか確認しません。gRPC サーバプロセスが起動していてポートが開いている状態 = プロセス・ポート監視の要件を満たす、という判断です。

アプリレベルのヘルス(依存サービスへの接続状態など)まで監視したい場合は、将来的に gRPC Health Checking Protocol を実装したうえで gRPC プローブへ移行するのが本筋です。

Distroless コンテナの制約

使用しているコンテナイメージは gcr.io/distroless/java21-debian12:nonroot です。Distroless はシェルや診断ツールを含まないため、Exec プローブ(コマンド実行型)は使用できません。

# これはできない(distroless だとシェルがないので失敗する)
startupProbe:
  exec:
    command: ["curl", "-f", "http://localhost:9090/health"]

プローブ種別は必ず gRPC / TCP Socket / HTTP GET のいずれかを選んでください。

failureThreshold × periodSeconds の算出ロジック

「起動窓」の考え方

Startup Probe の設計で最も重要なのは、起動窓 = failureThreshold × periodSeconds がアプリの最大起動時間を上回っていることです。

項目 計算式
起動窓(秒) failureThreshold × periodSeconds
最大チェック回数まで initialDelaySeconds + failureThreshold × periodSeconds

当初の設定値を見直した際の Before/After です。

パラメータ Before After 理由
initialDelaySeconds 10 0 起動が早ければ即座に通過できるため、不要な待機をなくす
periodSeconds 3 10 細かすぎるチェックはコンテナ高負荷時に誤失敗を増やす
failureThreshold 5 12 起動窓を 25 秒 → 120 秒に拡大
起動窓 25 秒 120 秒 Java の実際の起動時間に合わせる

Java と Node.js で全然違う起動時間

同じプロジェクトに Java(Spring Boot)と Node.js の両方のサービスがあり、必要な起動窓が大きく異なりました。

Java(Spring Boot)の起動時間の構成要素:

  • JVM 起動
  • Spring Boot コンテキスト初期化
  • OpenTelemetry Agent の初期化
  • gRPC サーバの起動
  • Cloud Spanner 接続プールの初期化

推定起動時間:60〜120 秒(性能試験前のため実測値ではありません)。これに安全マージンを含めて 10 × 12 = 120 秒 の起動窓を設定しました。

一方、Node.js(monitoring Worker)の起動時間は実測値 300〜600ms。1 秒もかかりません。

# Node.js 系 Worker Pool の Startup Probe
startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5   # Java の数十秒とは桁違いに短い
  periodSeconds: 3
  failureThreshold: 5      # 15 秒の起動窓で十分
  timeoutSeconds: 3

Java と同じ設定を Node.js に適用すると、120 秒以上の起動窓を持つことになり、起動失敗の検知が大幅に遅延します。言語・フレームワークの起動特性に合わせて個別に設計する必要があります。

Liveness Probe は「誤検知との戦い」

Liveness Probe の failureThreshold × periodSeconds は、一時的な高負荷による応答遅延を誤検知として排除しつつ、真の異常(プロセスハング・ポート閉塞)を検知できる時間に設定する必要があります。

GCP のベストプラクティスでは failureThreshold: 3 が推奨されており、periodSeconds を調整して判定時間を決めます。

サービス periodSeconds 判定時間(積) 採用理由
API サービス(Java/gRPC) 15 45 秒 gRPC Health Check は軽量だが、Spanner 高負荷時の一時的遅延を考慮
フロントサービス(Node.js) 10 30 秒 Node.js はイベントループがフリーズすると即座に無応答になるため短め
長時間バッチ処理系 30 90 秒 処理中の一時的なスレッド占有による誤検知を防ぐ

Cloud Run Worker Pool 編:Node.js 無ポート問題

Worker Pool の特殊性

Cloud Run Worker Pool は Pub/Sub などのメッセージをプル処理するリソースです。HTTP リクエストを受け付けず、外部からポートを叩かれることを想定していません。

そのため、Probe の設計では「何を監視するか」の整理が必要でした。

Node.js Worker Pool に HTTP サーバがなかった問題

monitoring 系の Worker Pool は Node.js 実装で、Pub/Sub メッセージを受信して処理するだけのシンプルな構成です。ソースを確認したところ、HTTP サーバを起動していませんでした。

# 確認したこと
HTTP サーバ(express 等)の依存: なし
ポートのリスニング: なし
/healthz エンドポイント: なし

HTTP GET プローブを設定しようにも、叩く先がありません。

解決策として、/healthz エンドポイントの実装をアプリチームに依頼しました。要求仕様を以下のように整理して提示しています。

エンドポイント: GET /healthz
ポート: 8080
応答条件: Pub/Sub サブスクリプションへの接続が完了していること
成功レスポンス: HTTP 200(ボディ任意)
失敗レスポンス: HTTP 5xx
タイムアウト: 3 秒以内に応答

Pub/Sub 接続が完了する前に 200 を返すだけでは、「起動したがまだメッセージ処理できない」状態でトラフィックを受け入れることになります。接続完了後に 200 を返すことで、プローブが本当の意味で「使える状態になった」ことを確認できます。

Java 系 Worker Pool との設計の分岐

Java(Spring Boot)で動く Worker Pool はポート 8080 で HTTP サーバが起動していることを確認できました。こちらは Spring Actuator の導入状況次第で、HTTP GET(/actuator/health)か TCP Socket かを選択します。

# Java 系 Worker Pool(Spring Actuator 確認済みの場合)
startupProbe:
  httpGet:
    path: /actuator/health
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 9    # 90 秒の起動窓
livenessProbe:
  httpGet:
    path: /actuator/health
    port: 8080
  initialDelaySeconds: 0
  periodSeconds: 15
  failureThreshold: 3
  timeoutSeconds: 5

Cloud Run Job 編:Startup Probe は「使えるが使わない」という判断

バッチ処理系 Job の実態

プロジェクトには数十本の Cloud Run Job があります。実装を確認したところ:

# application.properties
spring.main.web-application-type=none
# gRPC も無効
grpc.server.port=-1

HTTP サーバなし・gRPC サーバなし。これは意図的な設計で、バッチ処理としてメモリを節約するために HTTP サーバを起動しないよう設定されていました。ポートが存在しないため、TCP Socket プローブも HTTP GET プローブも設定できません。

「Startup Probe を使えるようにアプリを変更する」は合理的か

「ではポートを開ければいいのでは」という議論もありましたが、以下の理由から推奨しませんでした。

観点 内容
Probe でないと補えないか ジョブ失敗ログ監視(既存)が起動失敗を含む全失敗シナリオを捕捉済み
コスト 全 Job にヘルスエンドポイントを追加 → テスト・デプロイの工数が発生
リスク 本来不要な HTTP サーバを起動することで、攻撃面が増える

ログ監視で起動失敗まで監視できる理由

「Startup Probe がないと起動失敗を検知できないのでは?」という懸念がありましたが、以下のシナリオはすべてジョブ失敗ログ監視(タスク失敗ログベースアラート)で捕捉できます。

起動失敗シナリオ コンテナの終了 ログ監視での検知
JVM 起動失敗(OOM、クラスパスエラー等) 非ゼロ exit ✅ 検知できる
Spring Context 初期化失敗(Bean エラー等) 非ゼロ exit ✅ 検知できる
イメージ pull 失敗 コンテナ未起動 ✅ 検知できる
起動中に OOM Kill カーネルが kill ✅ 検知できる
起動後にコンテナがハングして exit しない タイムアウト後に強制終了 timeoutSeconds 後に失敗として記録

唯一 Startup Probe でしか補えないケースは「起動後にポートが開いているか確認する」ですが、ポートのないバッチジョブにはそもそも監視対象のポートがありません。

バッチ処理系 Job は Startup Probe / Liveness Probe を設定せず、既存のログ監視で十分です。これは「使えるが使わない」という積極的な判断です。

まとめ

結局、どのリソースも「何のポートが開いているか」を先に確認することが出発点でした。ポートがなければプローブは設定できず、あっても Health Check の実装がなければ gRPC プローブは機能しない。distroless ならコマンドも叩けない。

次に同じ設計をするなら、まず実装を読んでから YAML を書きます。

  • gRPC サーバが動いていても Health Check 未実装なら TCP Socket で代替
  • distroless では gRPC / TCP Socket / HTTP GET の三択
  • failureThreshold × periodSeconds を起動窓として計算し、Java と Node.js は別設計
  • Worker Pool は HTTP サーバの有無を先に確認。なければ /healthz 実装が前提
  • バッチ Job はポートなし。ログ監視で起動失敗も含めてカバーできる
  • Cloud Run の自動再起動を前提に「2 回目の失敗でアラート」にする

最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、
幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。

お問合せ:https://gmo-connect.jp/contactus/

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?