🧟‍♀️

Cloud Runの有効なヘルスチェック方法

2024/02/06に公開

はじめに:導入にあたって

最近、Cloud Runのヘルスチェックを導入しました。
以前プレビュー段階でトライしていたのですが期待する効果が得られなかったため一旦見送っていました。最近(といってもかなり経ちますが)GAになったのでリトライしました。

導入前はuptime checksを使ってヘルスチェック用のエンドポイントへポーリングしていました。
ただこの方法は、アプリケーション全体が不調の時はキャッチできますが、一部のコンテナがゾンビのような状態で生存し続けるようなケースをキャッチすることは難しいです。

現在はコンテナ単位でチェックできるので、ヘルシーなコンテナだけでアプリケーションを構成することができるようになりました。

ヘルスチェックの種類

以下の2種類があり、今回どちらも設定しました。

起動プローブ

起動したコンテナへトラフィックを流して良いかチェックします。
HTTP、TCP、gRPCによるチェックが可能です。

ライブネスプローブ

コンテナ起動後、健康状態を定期的にチェックします。
HTTP、gRPCによるチェックが可能です。

エンドポイントの作成

ヘルスチェックでリクエストするエンドポイントを作成しました。

何を持ってコンテナがヘルシーだと言えるのか?はわりと深い問いですが、バックエンドのコンテナやデータベース単体では正常でもお互い接続できていないと全体としてヘルシーではないことは明らかなので、それを考慮して作成します。

以下のように、DB、ElasticSearch、Redisとの接続をチェックするエンドポイントにしました。

import { prismaClients } from './clients/prisma'
import { elasticSearchClients } from './clients/elasticSearch'
import { redisClients } from './clients/redis'

// only check that the backend and the service are connected
const checkDb = async () => {
  await prismaClients.users.findFirst()
}

const checkElasticSearch = async () => {
  await elasticSearchClients.searchUser('dummy')
}

const checkRedis = async () => {
  await redisClients.get('dummy')
}

router.get('/health', async (_req, res) => {
  await Promise.all([checkDb(), checkElasticSearch(), checkRedis()])
  res.status(200).send('All probe passed')
})

設定の考え方

最終的に以下の設定にしました。

spec:
  template:
    spec:
      containers:
        startupProbe:
          timeoutSeconds: 1
          periodSeconds: 1
          failureThreshold: 100
          httpGet:
            path: /health
            port: 8080
        livenessProbe:
          timeoutSeconds: 60
          periodSeconds: 60
          failureThreshold: 3
          httpGet:
            path: /health
            port: 8080

環境や構成により設定値はもちろん変わりますが、以下をベースに決めると良いです。

起動プローブ(startupProbe)

initialDelayはゼロが良いと思います。これはチェックを始めるまでの待機時間で、アプリケーションが起動に要する時間を設定すると良さそうですが、たとえば15秒に設定したとして、その後誰かが劇的に起動時間を改善してくれたとしても15秒待機してしまうのでこれはゼロで良いと思います。

チェック間隔のperiodSecondsや失敗とみなすまでのリトライ回数failureThresholdを使って、これ以上かかると起動失敗と判断しても良い時間を設定しましょう。

ライブネスプローブ(livenessProbe)

厳しく設定するとヘルシーなコンテナだけでアプリケーションを構成できますが、あまりに厳しいと重い処理中のヘルスチェックでtimeoutSecondsを連続で超えてしまいコンテナがシャットダウンしてしまうので、チューニングして適切な値を設定しましょう。

チューニング

導入当初は起動プローブのinitialDelayをゼロより大きくしていたので導入前と比べコンテナのスタートアップ時間が増えてしまったり、ライブネスプローブを厳しくしすぎて別のリクエスト処理中にコンテナがシャットダウンしてしまったりと、何度かチューニングして現在に至ります。

エンドポイントのアクセス制御

ヘルスチェックだけに使うエンドポイントのため、外部からのリクエストはWAFで拒否して不要な課金を抑えています。ちなみにuptime checksを使ってチェックしていた時は、GoogleからのIPアドレスかどうかチェックするミドルウェアを作ってアクセス制御していました。[1]

おわりに

これでヘルシーなコンテナだけでアプリケーションを構成できるようになりました。

改めて、ヘルシーなコンテナとは何だろうと考えてみました。
人間のヘルスチェックでは網羅的な診断項目があり、結果が基準値以内であれば問題なしと判定されるので、コンテナも同じように考えるとパフォーマンスが良い、エラーがない、外部サービスと接続できているとヘルシーと言えます。

診断項目が多いと人の場合も健診センターで待たされるように、コンテナのヘルスチェックに色々詰め込むと時間がかかって起動が遅くなるので、ここではコンテナが依存する外部との接続を確認する程度で、その他は別の手段でチェックするのが良いです。

余談ですが、ゾンビを撃退するだけでなく、ライフサイクルの最期はgraceful shutdownで綺麗にコンテナをシャットダウンしましょう![2]

脚注
  1. リクエスト元のIPがlistUptimeCheckIps()で返ってくるIPに含まれているかチェックしてました。サンプルコードはこちらを参照ください。 ↩︎

  2. SIGTERMをコンテナへ送ってDBの接続をクローズするなどクリーンアップタスクが実行できます。詳しくはこちらを参照ください。 ↩︎

コミューン株式会社

Discussion