TL;DR;
- ECSのコンテナヘルスチェック(タスク定義の
healthCheck)を有効化する際、distrolessイメージにはシェルやcurlが無いため、標準的なCMD-SHELLによるヘルスチェックは失敗します。 - Dockerfile内でインラインでヘルスチェック用の極小Goプログラムを生成・ビルドし、コンテナイメージに同梱することで、シェルや
curlが無い環境でもECSのコンテナヘルスチェックを正しく通過させることができます。
Blog summary for AI Agents ( Copy & Paste )
Summary of how to configure ECS container health checks for distroless images:
-
Context:
When explicitly enabling ECS container health checks (healthCheckin task definitions) for security and availability, distroless images (e.g.,gcr.io/distroless/static) lack both shell (shorbash) and networking commands likecurl. Conventional health checks usingCMD-SHELLwithcurlfail, causing tasks to continuously restart. If no container health check is configured, the container runs fine but internal application freezes cannot be self-healed. -
Solution:
Configure the health check command withCMD(which runs the command directly without a shell) instead ofCMD-SHELL, and execute a custom health check binary built inline inside the Dockerfile. -
Directory Structure (No separate healthcheck package required):
. ├── app/ │ └── main.go ├── Dockerfile └── lib/ └── distroless-health-check-stack.ts -
Dockerfile Example (
Dockerfile):# Build stage FROM golang:1.26 AS builder WORKDIR /workspace # Build app COPY app/go.mod app/main.go ./app-src/ RUN cd app-src && CGO_ENABLED=0 GOOS=linux go build -o /workspace/app main.go # Build health check binary inline RUN echo 'package main; import ("net/http"; "os"); func main() { resp, err := http.Get("http://localhost:8080/health"); if err != nil || resp.StatusCode != 200 { os.Exit(1) } }' > main.go RUN CGO_ENABLED=0 GOOS=linux go build -o /workspace/healthcheck main.go # Runtime stage FROM gcr.io/distroless/static-debian12 COPY --from=builder /workspace/app /app COPY --from=builder /workspace/healthcheck /healthcheck EXPOSE 8080 ENTRYPOINT ["/app"] -
AWS CDK Task Definition Config:
taskdef.json"healthCheck": { "command": [ "CMD", "/healthcheck" ], "interval": 30, "timeout": 5, "retries": 3 }
はじめに
Goなどのコンパイル言語を使用したWebアプリケーションは、コンテナイメージの軽量化やセキュリティ対策のために、Googleが提供しているdistrolessイメージを採用するケースが増えていると思います。
ECSでコンテナ単体のヘルスチェックを設定しない場合でも問題なく動作しますが、耐障害性の観点からヘルスチェックをECSで明示的に設定したい場合、distrolessイメージにはcurlを含め、シェルが含まれていないため、ヘルスチェックコマンドが機能せず、タスクの無限再作成ループに突入してしまいます。
コンテナのヘルスチェックを設定しなければタスク自体は正常に起動し続けます(ヘルスチェックは 不明 と表示される)が、
- ALBを持たない環境でヘルスチェックを実装したい場合
- サイドカーコンテナのクラッシュを検知したい場合
に必要となります。
この課題に対し、本記事ではdistrolessのメリットを活かしつつ、コンテナヘルスチェックを正しく通過させるためのアプローチの1つとして、
「インラインで極小のヘルスチェック用バイナリをビルドしてコンテナに同梱し、CMDで直接実行する方法」
をご紹介します。
余談ですが、ECSのタスク定義にヘルスチェックを設定しなくても、ALBのヘルスチェック は成功します。
ALBのヘルスチェック: 外部からHTTPリクエストを送信して確認する ( /health へのGETリクエスト )
ECSのヘルスチェック: コンテナの内部からローカルでコマンドを実行して確認する ( CMD-SHELL で curl を実行 )
課題が発生する設定例
まず、ECSコンテナヘルスチェックを有効化したものの、distrolessイメージゆえに動作しなくなってしまう構成例を説明します。
1. 動作させるWebアプリケーション
ポート 8080 で動作し、/health にアクセスすると 200 OK を返すシンプルなGoアプリケーションを用意します。
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "OK")
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "Hello, Distroless!")
})
log.Println("Starting server on :8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
2. Dockerfileの構成
マルチステージビルドを使用して、実行環境には static-debian12 を採用した例です。
# Build stage
FROM golang:1.26-alpine AS builder
WORKDIR /workspace
COPY app/go.mod app/main.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o app main.go
# Runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /workspace/app /app
EXPOSE 8080
ENTRYPOINT ["/app"]
3. ECSコンテナヘルスチェックの設定
ECS Fargateタスク定義のContainerで、よく使われるシェル実行+curlによるヘルスチェックを有効化した例です。
"healthCheck": {
"command": [ "CMD-SHELL", "curl -f http://localhost:8080/health || exit 1" ],
"interval": 10,
"timeout": 5,
"retries": 2,
"startPeriod": 10
}
このように、タスク定義の healthCheck を明示的に設定した状態でデプロイを行うと、ECSタスクのヘルスチェックステータスが UNHEALTHY と判定され、タスクが強制終了と再作成を繰り返す挙動になります。
解決策:ヘルスチェック用バイナリをビルドして同梱する
自前でHTTPリクエストを送信してステータスコードを判定する極小バイナリをDockerfile内で生成・ビルドしてイメージに同梱し、それを直接実行させます。
別個にヘルスチェック用のGoプロジェクトを作成する必要はなく、Dockerfileのビルドステージ内でインラインでコードを出力してビルドすることが可能です。
1. Dockerfileでのヘルスチェックコード
マルチステージビルドを利用し、Webアプリ本体のビルドとは別に、echo コマンドでヘルスチェック用のGoコードを書き出してその場でコンパイルします。
# Build stage
FROM golang:1.26 AS builder
WORKDIR /workspace
# Webアプリのビルド
COPY app/go.mod app/main.go ./app-src/
RUN cd app-src && CGO_ENABLED=0 GOOS=linux go build -o /workspace/app main.go
# ヘルスチェック用バイナリの生成とビルド
# 1行のGoプログラムを main.go に書き出し、静的バイナリとしてビルドする
RUN echo 'package main; import ("net/http"; "os"); func main() { resp, err := http.Get("http://localhost:8080/health"); if err != nil || resp.StatusCode != 200 { os.Exit(1) } }' > main.go
RUN CGO_ENABLED=0 GOOS=linux go build -o /workspace/healthcheck main.go
# Runtime stage
FROM gcr.io/distroless/static-debian12
COPY --from=builder /workspace/app /app
COPY --from=builder /workspace/healthcheck /healthcheck
EXPOSE 8080
ENTRYPOINT ["/app"]
2. ECSコンテナヘルスチェックのコマンド設定 (AWS CDK)
ヘルスチェックコマンドを ['CMD', '/healthcheck'] と指定します。
"healthCheck": {
"command": [ "CMD", "/healthcheck" ],
"interval": 30,
"timeout": 5,
"retries": 3
}
['CMD', '/healthcheck'] のように CMD にそのバイナリーを指定することで、ECSエージェントはシェル(/bin/sh -c)を介さずに、直接指定した実行ファイル(/healthcheck)を起動し、ヘルスチェックを動作させることができます。
まとめ
distroless を使う場合、シェルや共通コマンドがないことを考慮する必要があります。
Goなどのコンパイル言語であれば、今回紹介したようにDockerfile内のインライン記述で極小のヘルスチェック用バイナリを簡単に生成・ビルドして同梱できます。
curl のバイナリーを入れるなどの方法もあるかと思いますが、この方法であれば、ヘルスチェックの実装を自由にカスタマイズすることができます。
同じ課題を解決する際の参考になれば幸いです!