🧑‍⚕️

distroless + gRPCサーバーで、コンテナの HEALTHCHECK を通す

に公開

はじめに

何の話か

コンテナで何かしらのサーバーが立っているときに、うまく動作しているかの確認のヘルスチェックでは、よく以下のような方法が取られます。

  1. コンテナ外部からコンテナに対してリクエストを送る
    • ex.) ALB + ECS Fargateの構成において、ALBのヘルスチェック機能を用いるパターン
  2. コンテナ内部からリクエストを送る
    • ex.) Dockerfileの HEALTHCHECK を用いるパターン, ECSサービスのヘルスチェックを用いるパターン

この記事では、2の方法についての話をします。

2の方法では、コンテナ内部からリクエストを送るため、コンテナ自身がヘルスチェック動作を実行するための依存関係を持っている必要があります。例えば、ヘルスチェックに curl を使いたい場合は、コンテナイメージに curl がインストールされている必要があります。

そのため、セキュリティや軽量化の観点などで distroless イメージをベースイメージとした場合、自身で依存関係を用意してあげる必要があります。

サンプルとして構築するもの

gRPCサーバーを、distroless イメージをベースイメージとしたコンテナで動かし、そのコンテナ内部からヘルスチェックを行う方法を考えます。

gRPCにおいてどのようなメソッドやメッセージでヘルスチェックを行うべきかは、こちらに記載があります。

サーバー自体は上記のprotoに従って構築します。今回は、Node.js + Fastify + Connectで構築しました(本題ではないのでここの細かい内容は省略します)。

成果物

成果物は以下のリポジトリにあります。

mutex-inc/zenn-chrg1001-docker-grpc-health-check

サーバーの準備

サーバーの実装

ここは本題ではないので軽くだけ触れます。

上で紹介した proto をそのまま定義します。

https://github.com/mutex-inc/zenn-chrg1001-docker-grpc-health-check/blob/main/proto/grpc/health/v1/check.proto

Buf CLIを使って、Node.jsのコードを生成します。

yarn buf:gen

サービスを実装してルーティングを行えば完了です。

https://github.com/mutex-inc/zenn-chrg1001-docker-grpc-health-check/blob/6b22f939203228f6d95c7f864082b54792e2b2d5/src/index.ts#L11-L32

ホスト環境での確認

まずホスト環境で正しくヘルスチェックできることを確認しましょう。

以下でサーバーを起動します。

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 を使うことにします。

https://github.com/grpc-ecosystem/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 をベースとしたいため、 builderrunner のマルチステージでビルドします。

builder ステージ

builder ステージで、grpc_health_probe のバイナリをダウンロードし、実行権限をつけます。

今回はコンテナの実行環境に合わせて linux/arm64 用のバイナリをダウンロードしています。

https://github.com/mutex-inc/zenn-chrg1001-docker-grpc-health-check/blob/6b22f939203228f6d95c7f864082b54792e2b2d5/Dockerfile#L28-L33

あとは、node側のコードをビルドします。

runner ステージ

runner ステージでは、builder ステージからnodeのビルドファイルや grpc_health_probe のバイナリをコピーします。

https://github.com/mutex-inc/zenn-chrg1001-docker-grpc-health-check/blob/6b22f939203228f6d95c7f864082b54792e2b2d5/Dockerfile#L97-L100

最後に HEALTHCHECK を設定します。オプションは適当なものに変更してください(ドキュメント)。

https://github.com/mutex-inc/zenn-chrg1001-docker-grpc-health-check/blob/66a92fadc6dc8a0a099c7fa52c9163d0d519d6c0/Dockerfile#L108-L109

実行

ビルドします。

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}}'

Statushealthy であることを確認できます。

{
  "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"]

ここでの留意事項としては以下が挙げられます。

また、正しくない 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 として解釈されていることがわかります。このとき、HealthLog には以下のように出力されます。

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

参考

https://github.com/GoogleContainerTools/distroless/issues/183

https://github.com/GoogleContainerTools/distroless/issues/319

https://zenn.dev/hsaki/books/golang-grpc-starting/viewer/healthcheck

mutex Tech Blog

Discussion