k8sのヘルスチェックとRailsでの実装例
対象読者
- これから k8s 上で Rails アプリケーションを運用し始める方
- k8s のヘルスチェックについて、ざっくり知りたい方
- ヘルスチェックエンドポイントの実装例を知りたい方
Health チェックの種類
k8s (EKS) で動作するRailsアプリケーションのヘルスチェックは以下の通りです。
- LivenessProbe
- RedinessProbe
- StartupProbe(今回は扱わない)
- ALB ヘルスチェック
それぞれどのようなヘルスチェックを実装したのか、なぜそうしたのかをまとめました。
LivenessProbe
LivenessProbe は、コンテナが動いていることを確認できればよいです。
なので Rails アプリケーションでは「プロセスは動いているが、アプリケーションが応答できない場合」に Liveness が失敗すれば良さそうです。
「それって ReadinessProbe だけでよくない?」 という気もしますが、Liveness が失敗した場合はコンテナの再起動を行ってくれるので、別で設定したほうが良いと思います。
なので Readiness は、HTTP リクエストをさばけるプロセスが生きてるかどうか? をチェックする用途として、今回は 200 を返すだけのエンドポイントを LivenessProbe に設定しました。
ちなみに Rails は標準で health チェックエンドポイントを実装してくれてます。
ただ実装を見てもらえれば分かる通り、このエンドポイントは処理できた場合すぐに 200, そうでない場合は 500 を返すだけで、その他のヘルスチェックは行っていないです。(というか異常系は 503 じゃないのね。。)
一応、コメントでは、依存するサービスが落ちてる場合は health チェックで失敗するのではなく、適切に対応できるように実装するように記載されてます。
ReadinessProbe
ReadinessProbe は アプリケーションが応答できる準備ができているかどうか? を確認できればよいです。
個人的にはアプリケーションが正常に応答できる == DB や Redis などの周辺アプリケーションも利用できると捉えて、周辺アプリケーションへの疎通確認も行うべきだと思います。
なので Readiness には DB, Redis への疎通確認を実装しました。
health チェック用の module
# frozen_string_literal: true
module Kubernetes
class DeepHealthChecker
class << self
def healthy?
db_connection_active? && redis_connection_active?
end
private
def db_connection_active?
ApplicationRecord.connection.execute('SELECT 1;')
true
rescue => e
false
end
def redis_connection_active?
redis_client.ping == 'PONG'
rescue => e
false
end
def redis_client
Redis.new(url: 'example.hoge')
end
end
end
end
health チェック用の controller
class DeepHealthzsController < ApplicationController
def show
if Kubernetes::DeepHealthChecker.healthy?
head :ok
else
head :service_unavailable
end
rescue => e
head :service_unavailable
end
end
DBの疎通確認は ApplicationRecord.connection.execute('SELECT 1;')
で行ってます。
ActiveRecord には ActiveRecord::Base.connection.active?
というメソッドもありますが、こちらは ping での確認となるので、今回は DB へクエリが実行できることまで確認するために SELECT 1;
しています。
また、このようなヘルスチェックは DeepHealthCheck と呼ばれることがあるそうなので、エンドポイントの名前も /deep_healthz
にしています。
そして、失敗時のハンドリングでは 503 service_unavailable を返却しています。
たとえば k8s では 200 ~ 400 番台以外を失敗とみなすため、500 を返しても良いのですが、レスポンスコードとしては 503 が適切だと思います。
Any code greater than or equal to 200 and less than 400 indicates success. Any other code indicates failure.
DeepHealth チェックに関する注意事項
Rails のコメントにもあった通り、依存サービスが落ちた場合に、自サービスもヘルスチェックに失敗するべきなのかどうか? は、プロダクトの要件ごとに考慮が必要です。
たとえば Redis を使用していないバックエンド API 用の Pod と、ユーザーがアクセスしてくる
Redis を使用している Web サーバーの Pod のヘルスチェック内容は別であるべきです。
k8s を使用するなら、 Deployment 事に別のヘルスチェックエンドポイントを利用できるので、Deployment, Pod の要件ごとにヘルスチェックエンドポイントを使い分けることが重要です。
参考記事
要約すると、『要はバランス』
The key takeaway from my Kubernetes tale isn't to shun deep health checks but to use them carefully. Balance is crucial; we need to weigh the benefits of thorough health checks against the potential for widespread system impacts. Learning from our mistakes and those of others is what makes us better developers and more resilient in the face of system complexity. I share my story, in the hope that you share yours too.
ALB のヘルスチェック
ALB のヘルスチェックはあくまで ALB とサービスの疎通確認として、deep_healthz ではなく healthz へアクセスするようにしました。
Readiness で適切にヘルスチェックできている場合、仮に DB 等が落ちた場合は Rediness が失敗→該当 Pod が Service から除外されます(つまり該当の Pod へのトラフィックは止まる)
なので、ALB ではそこまで確認せずに、あくまで疎通確認だけで良いかなと思います。
また、運用する ALB, Pod が多い場合はヘルスチェックの試行回数も増えて、サービスに負担をかける可能性もあるので、そのあたりも要検討ポイントです。
ヘルスチェックの標準化
レスポンスの標準化みたいな話は少し出たみたいですが現状進んでいなさそうです。
ここまでリッチに作る必要はない気もしますが、一度見てみると参考になるかもしれません。
まとめ
これから k8s x Rails でヘルスチェックを実装しようとする方の参考になれば幸いです。
参考資料
Discussion