🐳

distrolessイメージのヘルスチェックのスタンダードがわからない件

に公開

これは何?

distroless イメージでヘルスチェックするときは大体こうするよね、というのがあまり見つけられなかったので http-health-probe というコマンドを公開しました。
https://github.com/dev-shimada/http-health-probe/tree/main
こうするといいよ、こうしてます、というのがあればぜひ教えて下さい。

背景

先日社内で distroless イメージを使っている方がヘルスチェックどうすればいいんだろう?と呟いていて、自分も世の中でどのように対応されているのか気になりました。

grpc の場合

grpc の場合は私は grpc-ecosystem/grpc-health-probe を使っています。
grpc サーバ側に connectrpc.com/grpchealth でヘルスチェックサービスを追加し、イメージに HEALTHCHECK を追加します。
このコマンドは go で実装されていて、ネイティブに実行できるため distroless でも他のステージからコピーして使うことができます。

curl を使おうとしてみる

まず curl を使ってみます。
dnf や apt などのパッケージマネージャは入っていないので、他のステージから持ってくる必要があります。
以下のようになりました。

FROM rust:1.85-bookworm AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
COPY . .
RUN <<EOF
if [ "${TARGETARCH}" = "arm64" ]; then
TARGET=aarch64-unknown-linux-musl
elif [ "${TARGETARCH}" = "amd64" ]; then
TARGET=x86_64-unknown-linux-musl
else
echo "Unsupported architecture: ${TARGETARCH}"
exit 1
fi
rustup target add ${TARGET}
cargo build --release --target ${TARGET}
mkdir -p /tmp/target
cp /workspace/target/${TARGET}/release/api /tmp/target/api
EOF

FROM rust:1.85-bookworm AS healthcheck

FROM gcr.io/distroless/base-debian12:latest
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY --chown=nonroot:nonroot --from=build /tmp/target/api /app/app
COPY --chown=nonroot:nonroot --from=healthcheck /usr/bin/curl /usr/bin/curl
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libcurl.so.4 /usr/lib/aarch64-linux-gnu/libcurl.so.4
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libz.so.1 /usr/lib/aarch64-linux-gnu/libz.so.1
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libnghttp2.so.14 /usr/lib/aarch64-linux-gnu/libnghttp2.so.14
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libidn2.so.0 /usr/lib/aarch64-linux-gnu/libidn2.so.0
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/librtmp.so.1 /usr/lib/aarch64-linux-gnu/librtmp.so.1
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libssh2.so.1 /usr/lib/aarch64-linux-gnu/libssh2.so.1
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libpsl.so.5 /usr/lib/aarch64-linux-gnu/libpsl.so.5
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libgssapi_krb5.so.2 /usr/lib/aarch64-linux-gnu/libgssapi_krb5.so.2
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libldap-2.5.so.0 /usr/lib/aarch64-linux-gnu/libldap-2.5.so.0
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/liblber-2.5.so.0 /usr/lib/aarch64-linux-gnu/liblber-2.5.so.0
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libzstd.so.1 /usr/lib/aarch64-linux-gnu/libzstd.so.1
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libbrotlidec.so.1 /usr/lib/aarch64-linux-gnu/libbrotlidec.so.1
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libunistring.so.2 /usr/lib/aarch64-linux-gnu/libunistring.so.2
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libgnutls.so.30 /usr/lib/aarch64-linux-gnu/libgnutls.so.30
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libhogweed.so.6 /usr/lib/aarch64-linux-gnu/libhogweed.so.6
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libnettle.so.8 /usr/lib/aarch64-linux-gnu/libnettle.so.8
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libgmp.so.10 /usr/lib/aarch64-linux-gnu/libgmp.so.10
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libkrb5.so.3 /usr/lib/aarch64-linux-gnu/libkrb5.so.3
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libk5crypto.so.3 /usr/lib/aarch64-linux-gnu/libk5crypto.so.3
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libcom_err.so.2 /usr/lib/aarch64-linux-gnu/libcom_err.so.2
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libkrb5support.so.0 /usr/lib/aarch64-linux-gnu/libkrb5support.so.0
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libsasl2.so.2 /usr/lib/aarch64-linux-gnu/libsasl2.so.2
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libbrotlicommon.so.1 /usr/lib/aarch64-linux-gnu/libbrotlicommon.so.1
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libp11-kit.so.0 /usr/lib/aarch64-linux-gnu/libp11-kit.so.0
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libtasn1.so.6 /usr/lib/aarch64-linux-gnu/libtasn1.so.6
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libkeyutils.so.1 /usr/lib/aarch64-linux-gnu/libkeyutils.so.1
COPY --chown=nonroot:nonroot --from=healthcheck /usr/lib/aarch64-linux-gnu/libffi.so.8 /usr/lib/aarch64-linux-gnu/libffi.so.8
HEALTHCHECK --interval=1s --timeout=1s --start-period=0s CMD ["/usr/bin/curl", "-s", "http://localhost:3000/"]
EXPOSE 3000
USER nonroot:nonroot
ENTRYPOINT [ "/app/app" ]

libcurl 以外にも意外と多くのライブラリに依存していることがわかりました。
distroless を使うならこういうことはしないのが無難かな?と思いました。
イメージサイズは 49MB 程度でした。

$ docker image inspect test | grep -i size
        "Size": 49039762,
$ 

curl の rust 版?の hurl の存在を知ったので試してみましたが、こちらもライブラリを集める必要がありました。(自分で build すれば静的リンクできるとは思いますが...)
https://github.com/Orange-OpenSource/hurl

つくったもの

シングルバイナリで動いて、簡単に持ってこれるのが重要かなと思いました。
grpc-health-probe を参考にフラグを設定し、最低限の機能のみが簡単に実装されています。
https://github.com/dev-shimada/http-health-probe/tree/main

使用例は README にも書いてありますが、以下のように使えます。

FROM rust:1.85-bookworm AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
COPY . .
RUN <<EOF
if [ "${TARGETARCH}" = "arm64" ]; then
TARGET=aarch64-unknown-linux-musl
elif [ "${TARGETARCH}" = "amd64" ]; then
TARGET=x86_64-unknown-linux-musl
else
echo "Unsupported architecture: ${TARGETARCH}"
exit 1
fi
rustup target add ${TARGET}
cargo build --release --target ${TARGET}
mkdir -p /tmp/target
cp /workspace/target/${TARGET}/release/api /tmp/target/api

INSTALL_DIR=/tmp
VERSION=v0.1.1
curl --silent --location https://github.com/dev-shimada/http-health-probe/releases/download/${VERSION}/http-health-probe_${TARGETOS}_${TARGETARCH}.tar.gz | tar xvz -C ${INSTALL_DIR} --one-top-level=http-health-probe_${TARGETOS}_${TARGETARCH}
EOF

FROM gcr.io/distroless/base-debian12:latest
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY --chown=nonroot:nonroot --from=build /tmp/target/api /app/app
COPY --chown=nonroot:nonroot --from=build /tmp/http-health-probe_${TARGETOS}_${TARGETARCH}/http-health-probe /bin/http-health-probe
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s CMD ["/bin/http-health-probe", "--addr=:3000"]
EXPOSE 3000
USER nonroot:nonroot
ENTRYPOINT [ "/app/app" ]

go install の場合はもっとシンプルです。

FROM rust:1.85-bookworm AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
COPY . .
RUN <<EOF
if [ "${TARGETARCH}" = "arm64" ]; then
TARGET=aarch64-unknown-linux-musl
elif [ "${TARGETARCH}" = "amd64" ]; then
TARGET=x86_64-unknown-linux-musl
else
echo "Unsupported architecture: ${TARGETARCH}"
exit 1
fi
rustup target add ${TARGET}
cargo build --release --target ${TARGET}
mkdir -p /tmp/target
cp /workspace/target/${TARGET}/release/api /tmp/target/api
EOF

FROM golang:bookworm AS healthcheck
RUN <<EOF
go install github.com/dev-shimada/http-health-probe@latest
EOF

FROM gcr.io/distroless/base-debian12:latest
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY --chown=nonroot:nonroot --from=build /tmp/target/api /app/app
COPY --chown=nonroot:nonroot --from=healthcheck /go/bin/http-health-probe /bin/http-health-probe
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s CMD ["/bin/http-health-probe", "--addr=:3000"]
EXPOSE 3000
USER nonroot:nonroot
ENTRYPOINT [ "/app/app" ]

イメージサイズは 45MB 程度でした。

$ docker image inspect test | grep -i size             
        "Size": 44974274,
$

Discussion