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