Cloud Runのヘルス/レディネスチェックについて調べてみた

デプロイ時にサーバーの起動に失敗したり、デプロイ後に何かしらの理由で5xxを返し続けた場合にどうなるのかなどを調べてみました

本エントリはクラスメソッド Google Cloud Advent Calendar 2021の11日目の記事です。

Cloud Runではヘルスチェック(health check)やレディネスチェック(readiness check)に関する設定はありません。そのため、ヘルスチェック機能はないんだと考えていたんですが、指定したポートの疎通チェックはしてるということを社内で教えてもらいました。この辺りのことが気になったため、新しいリビジョンのデプロイ時にサーバーの起動に失敗したり、デプロイ後に何かしらの理由で5xxを返し続けた場合にどうなるのかなどを調べてみました。

まとめ

ドキュメントを調べたり、試してわかったことは次のとおりです。

  • 新しいリビジョンのデプロイをした場合
  • デプロイ後のインスタンスが作成される場合(スケールアウトなど)
    • 指定したポートの疎通確認が取れ次第、新しいインスタンスにリクエストが流されます
    • 疎通確認が取れない場合や疎通確認が取れる前にアプリケーションが終了した場合、新しいインスタンスが再作成されます
    • インスタンス数が0の状態で、リクエストが来た場合はインスタンスの作成が完了もしくは失敗するまで待機状態になります
      • その状態でインスタンスの作成に失敗した場合は、そのリクエストには503エラーが返されます
  • アプリケーションがエラーを連続で返す場合
  • アプリケーションが終了した場合
    • リクエストが来ている場合は、新しいインスタンスが起動されます

試してみる

Node.jsで書いたアプリケーションをCloud Runにデプロイし、いくつかのことを試すなかで挙動を確認します。

アプリケーションの内容を確認する

今回利用するアプリケーションはFastifyを利用した次のようなシンプルなAPIです。エンドポイントとしては、'world'とだけ返す/hello、エラーが発生する/error、アプリケーションが終了してしまう/crashがあります。デプロイ失敗時の挙動を見るためにサーバーの起動に50%の確率で失敗するようにしています。

const fastify = require("fastify")({
  logger: true,
});

fastify.get("/hello", () => {
  return "world";
});

fastify.get("/error", () => {
  throw new Error("error");
});

fastify.get("/crash", () => {
  process.exit(1);
});

if (Math.random() < 0.5) fastify.listen(8080, "0.0.0.0");

Cloud Runでサービスを作成する

このアプリケーションをCloud Runにデプロイします。gcloud run deploy --sourceでソースコードからのデプロイが可能なので、今回はこの機能を利用します。Dockerfileがないんですが、GoogleCloudPlatform/buildpacksを使ってビルドしてくれるようです。アプリケーションの起動にはnpm startが実行されるため、package.jsonstartスクリプトを定義しておく必要があります。今回はnode index.jsstartスクリプトとして定義しています。

次のコマンドを実行して、Cloud Runにサービスを作成し、ビルド&デプロイします。

gcloud run deploy health-check-test \
--source . \
--allow-unauthenticated \
--region asia-northeast1

gcloud run deployの各オプションについてはドキュメントをご確認ください。

実行してしばらくすると、デプロイが完了してデプロイされたサービスにアクセスできるURLが表示されます。

デプロイの様子をCloud Loggingから確認してみます。Cloud Runのページからもログが確認できますが、Cloud Loggingからのほうがより詳細に見れるので、今回はCloud Loggingから確認します。ログを見てみると、Ready condition status changed to Trueとあり、何かしらの確認が実行されたことが推測できます。ドキュメントを見てみると、起動時には4分以内にポートをリッスン(listen)する必要があるようです。

サービス作成時にアプリケーションの起動に失敗した場合は次のようにサービス作成に失敗します。

ログは次のようになります。先程とは違って、Ready condition status changed to Falseと書かれています。

新しいリビジョンを作成する

次は新しいリビジョンを作成し、様子をログで確認します。先程ビルドしたイメージをそのまま利用して、リビジョンを作成します。

gcloud run deploy health-check-test \
--image 'asia-northeast1-docker.pkg.dev/{プロジェクト名}/cloud-run-source-deploy/health-check-test' \
--allow-unauthenticated \
--region asia-northeast1

ログを見てみると、先ほどと同じ感じの内容が確認できます。ただ、リクエストがどのタイミングで新しいリビジョンに振り分けられるのかは分かりません。ドキュメントRequests are automatically routed as soon as possible to the latest healthy service revision.とあるので、ヘルスチェック後に新しいリビジョンへリクエストが振り分けられているようです。

エラーを連続で発生させる

アプリケーション内部もしくはコンテナが起動している環境などで何かしらのエラーが発生して、リクエストを正常に処理できずエラーを返し続ける状態になった場合を想定して、エラーを連続で発生させてみます。エラーが発生するエンドポイント/errorに対して連続でリクエストを投げます。 すると、インスタンスが終了し、新しいインスタンスが作られたようです。ログからは次のように500エラーと503エラー、新しいアプリケーションの起動ログが確認できます。

ドキュメントによると、20回以上5xxエラーが連続して返されると、インスタンスは終了させられるようです。また、503エラーも確認できましたが、ペイロードはThe request failed because the instance failed the readiness check.となっていました。インスタンスがない状態でリクエストが来ると、リクエストは待機状態になり、新しいインスタンスが作成されます。その状態で新しいインスタンスの作成に失敗すると、503エラーが返されるようです。

アプリケーションを終了させる

アプリケーションが終了されるエンドポイント/crashにリクエストを投げて、アプリケーションが終了した場合の挙動を確認します。/crashにリクエストを投げた直後は/crashリクエストのログ以外にログは出てきませんでした。その後、/heelo(helloのtypo)にリクエストを投げたあとのログは次のようになりました。

/crashによってアプリケーションが終了し、インスタンスも終了し、その後リクエストが新たに来たときにインスタンスが作成されるようです。今回はランダムでサーバーが起動するため、作成されたインスタンスもサーバー起動せずにすぐに終了しました。そのため、その時来ていたリクエストには503エラーが返されました。

サービスを削除する

最後にサービスを削除して、検証を終わります。

gcloud run services delete health-check-test

さいごに

ポートの疎通確認だったり、連続で5xxエラーを返した場合に自動でインスタンスをシャットダウンさせるなどの仕組みがあることがわかりました。ヘルスチェック先のエンドポイントや間隔などをカスタマイズできないことは不便に感じるかもしれませんが、必要最低限の機能は備わっています。また、Cloud Monitoringの稼働時間チェックを用いることで、特定のエンドポイントへリクエストを飛ばし、そのレスポンス内容に応じたアプリケーションのモニタリング、稼働率の確認やアラートの設定などが可能です。目的に応じて適切なサービスや機能を利用することが重要ですね。