distroless + gRPCサーバーで、コンテナの HEALTHCHECK を通す
はじめに
何の話か
コンテナで何かしらのサーバーが立っているときに、うまく動作しているかの確認のヘルスチェックでは、よく以下のような方法が取られます。
- コンテナ外部からコンテナに対してリクエストを送る
- ex.) ALB + ECS Fargateの構成において、ALBのヘルスチェック機能を用いるパターン
- コンテナ内部からリクエストを送る
- ex.) Dockerfileの
HEALTHCHECKを用いるパターン, ECSサービスのヘルスチェックを用いるパターン
- ex.) Dockerfileの
この記事では、2の方法についての話をします。
2の方法では、コンテナ内部からリクエストを送るため、コンテナ自身がヘルスチェック動作を実行するための依存関係を持っている必要があります。例えば、ヘルスチェックに curl を使いたい場合は、コンテナイメージに curl がインストールされている必要があります。
そのため、セキュリティや軽量化の観点などで distroless イメージをベースイメージとした場合、自身で依存関係を用意してあげる必要があります。
サンプルとして構築するもの
gRPCサーバーを、distroless イメージをベースイメージとしたコンテナで動かし、そのコンテナ内部からヘルスチェックを行う方法を考えます。
gRPCにおいてどのようなメソッドやメッセージでヘルスチェックを行うべきかは、こちらに記載があります。
サーバー自体は上記のprotoに従って構築します。今回は、Node.js + Fastify + Connectで構築しました(本題ではないのでここの細かい内容は省略します)。
成果物
成果物は以下のリポジトリにあります。
mutex-inc/zenn-chrg1001-docker-grpc-health-check
サーバーの準備
サーバーの実装
ここは本題ではないので軽くだけ触れます。
上で紹介した proto をそのまま定義します。
Buf CLIを使って、Node.jsのコードを生成します。
yarn buf:gen
サービスを実装してルーティングを行えば完了です。
ホスト環境での確認
まずホスト環境で正しくヘルスチェックできることを確認しましょう。
以下でサーバーを起動します。
yarn dev
今回はConnectを使用しているので、curl でも簡単にリクエストを送ることができます。
curl -X POST \
-H "Content-Type: application/json" \
--data "{}" \
--http2-prior-knowledge \
http://localhost:8080/grpc.health.v1.Health/Check
以下が返ってきたらOKです。
{"status":"SERVING"}
また、サーバー側にで以下のようなログが出ていることも確認しておきましょう。
2025-04-27T03:26:13.998Z - Health check called
コンテナ側の実装(本題)
コンテナからのヘルスチェック方法
前述の通り distroless イメージを使う場合は、自前で依存関係を用意する必要があるので、バイナリが用意されているものが最も楽に実装できます。
そこで今回は、grpc_health_probe を使うことにします。
$ grpc_health_probe -addr=localhost:8080
healthy: SERVING
バイナリが用意されているので、Dockerfile内でもすぐに使えます。
RUN GRPC_HEALTH_PROBE_VERSION=v0.4.13 && \
wget -qO/bin/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \
chmod +x /bin/grpc_health_probe
Dockerfileの実装
具体的なDockerfileの実装です。ここでは本題と関係のあるところのみ抽出します(Dockerfile全体はここ)。
実行時のイメージは distroless をベースとしたいため、 builder と runner のマルチステージでビルドします。
builder ステージ
builder ステージで、grpc_health_probe のバイナリをダウンロードし、実行権限をつけます。
今回はコンテナの実行環境に合わせて linux/arm64 用のバイナリをダウンロードしています。
あとは、node側のコードをビルドします。
runner ステージ
runner ステージでは、builder ステージからnodeのビルドファイルや grpc_health_probe のバイナリをコピーします。
最後に HEALTHCHECK を設定します。オプションは適当なものに変更してください(ドキュメント)。
実行
ビルドします。
docker build . -f Dockerfile -t docker-grpc-health-check-example:latest
コンテナを起動します。
docker run --rm --init --name docker-grpc-health-check-example -p 127.0.0.1:8080:8080 docker-grpc-health-check-example:latest
少し待つとコンテナ側に先ほどと同様なログが出てくるはずです。
2025-04-27T03:26:13.998Z - Health check called
コンテナのヘルスステータスについては、以下のように docker inspect でも確認できます。
docker inspect docker-grpc-health-check-example | jq -C '.[].State.Health'
# jq を使わない場合
docker inspect docker-grpc-health-check-example --format='{{json .State.Health}}'
Status が healthy であることを確認できます。
{
"Status": "healthy",
"FailingStreak": 0,
"Log": [
{
"Start": "2025-04-27T13:15:34.251428908+09:00",
"End": "2025-04-27T13:15:34.296695584+09:00",
"ExitCode": 0,
"Output": "status: SERVING\n"
},
]
}
注意点
HEALTHCHECK のコマンドは、exec形式で記載する
distroless に限らずですが、シェルの含まれていないイメージを利用する場合は、 CMD, RUN, ENTRYPOINT などのコマンドは、すべてexec 形式で記載する必要があります(shell 形式とexec 形式についてはこのあたり参考)。
# NG
CMD echo "hello world"
# OK
CMD ["echo", "hello world"]
つまり、 HEALTHCHECK で指定するコマンドも同様に exec 形式で記載する必要があります。
# NG
HEALTHCHECK CMD grpc_health_probe -addr=localhost:8080 || exit 1
# OK
HEALTHCHECK CMD ["grpc_health_probe", "-addr=localhost:8080"]
ここでの留意事項としては以下が挙げられます。
- コマンドの中で変数は使えない
-
成功した場合は
0, 失敗した場合は1を返すようなコマンドを実行する必要がある(そのような実行バイナリを用意する必要がある)
また、正しくない exec 形式(のような形)で書いた場合はすべて shell 形式として解釈されます。
HEALTHCHECK CMD ["grpc_health_probe", "-addr=localhost:8080"] || exit 1
# これは以下と同じように解釈される
HEALTHCHECK CMD "[\"grpc_health_probe\", \"-addr=localhost:8080\"]" || exit 1
このあたりのデバッグに少し手こずったのですが、こちらも docker inspect で確認することで解決しました。
docker inspect docker-grpc-health-check-example | jq -C '.[].Config.Healthcheck.Test'
[
"CMD-SHELL",
"[\"grpc_health_probe\", \"-addr=localhost:8080\"] || exit 1"
]
このように CMD-SHELL として解釈されていることがわかります。このとき、Health の Log には以下のように出力されます。
docker inspect docker-grpc-health-check-example | jq -C '.[].State.Health.Log'
[
{
"Start": "2025-04-27T13:42:01.126941676+09:00",
"End": "2025-04-27T13:42:01.184033831+09:00",
"ExitCode": -1,
"Output": "OCI runtime exec failed: exec failed: unable to start container process: exec: \"/bin/sh\": stat /bin/sh: no such file or directory: unknown"
}
]
正しく以下のように書いた場合は、きちんと CMD として解釈されます。
HEALTHCHECK CMD ["grpc_health_probe", "-addr=localhost:8080"]
[
"CMD",
"/bin/grpc_health_probe",
"-addr=:8080"
]
ビルドキャッシュに注意
HEALTHCHECK コマンドのデバッグで、ここだけ書き換えてビルドし直してもキャッシュが残っていると変更は反映されません。
デバッグ時は docker build で --no-cache を指定してあげたほうが良いでしょう。
docker build . -f Dockerfile -t docker-grpc-health-check-example:latest --no-cache
参考
Discussion