要約
はじめは、ヘルスチェックにcurl
コマンドを用い、Go言語のバイナリ実行のステージにdistroless
イメージを利用する設定にしていました。
ですが、distroless
イメージは極力コマンドなどを削った環境であり、curl
コマンドもインストールされていないようで、ヘルスチェックが行えませんでした。
対処法の候補はいくらかあるかと思いますが、私はdistroless
イメージの使用を辞め、alpine
イメージを使用するようにし、ヘルスチェックのコマンドもcurl
ではなくwget
を使うように変更しました。
HEALTHCHECK CMD wget --quiet --spider http://localhost:1323/ || exit 1
背景
元々のコンテナイメージ
普段Go言語のAPIサーバを構築する際、実行環境はDockerで構築しています。いままでは以下のようなDockerfileでGo言語のプログラムを実行していました。
FROM golang:1.22
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main main.go
HEALTHCHECK CMD curl -f http://localhost:1323/ || exit 1
CMD [ "./main" ]
サーバが起動しているのかを確認するために、ヘルスチェックをHEALTHCHECK CMD curl -f http://localhost:1323/ || exit 1
の行で設定しています。やっていることは、実際にcurlでリクエストを投げレスポンスが来るかを見ています。
このDockerfileの内容でビルドしたイメージをAWSのECRにプッシュし、ECSでデプロイするという方法を行なっていました。そのECSのタスク定義にも上記と同様のヘルスチェックコマンドを設定していました。
"healthCheck": {
"command": [
"CMD-SHELL",
"curl -f http://localhost:1323/ || exit 1"
]
}
この段階での問題点
この方法で問題視していたのは、コンテナイメージのサイズです。
先ほどもいったようにビルドしたコンテナイメージはAWSのECR上にプッシュしています。このECRですが、課金体系を見るとストレージ容量に応じた課金があります。したがって、イメージサイズが小さければそれだけコスト面で嬉しいです。
そこで、マルチステージビルドという手法を採用することで、最終的なイメージサイズを小さくしようということになりました。
マルチステージビルド
マルチステージビルドの概要について簡単にまとめます。
Go言語で言えば、プログラムをソースコードからビルドする段階と、ビルドしたバイナリを実行する段階をそれぞれ分け、ECRにはバイナリの実行環境のみアップロードするという方法です。
実行環境には最低限実行バイナリがあればよく、依存パッケージのソースコードなどは不要になります。それらを省くことで、その分だけイメージのサイズを小さくすることができます。
マルチステージビルドに変更してみた
マルチステージビルドを実装するために、参考にあげている記事なども参照しながら、以下のようにDockerfileを変更しました。
################## ビルドステージ ##################
FROM golang:1.22-alpine as builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main main.go
################## 実行ステージ ##################
FROM gcr.io/distroless/static-debian11
WORKDIR /app
# ビルドしたバイナリをコピー
COPY --from=builder /app/main .
HEALTHCHECK CMD curl -f http://localhost:1323/ || exit 1
CMD [ "./main" ]
ここで実行ステージに使用しているベースイメージはGoogleの手がけるdistroless
イメージというものです。必要最低限の依存パッケージやコマンドしか入っておらず、そのサイズが非常に小さくなっています。このdistroless
イメージの必要最低限のコマンドというのが、追々の問題につながりました。
このイメージで、ローカルで正常にコンテナを起動することはできました。Docker Desktopで見た時のイメージのサイズは、以前のイメージで999.98 MB
となっていたものが、マルチステージビルドを利用したイメージで10.03 MB
まで減りました。圧倒的です。ローカルで起動もできているので問題ないと思いECSにデプロイをしました。
ただデプロイしてみると、タスクの実行が止まってしまいます。色々ログを見ていたりすると、どうやらstatusがunhealthyとなっていることに気付きました。
なぜECSでのヘルスチェックが失敗したのか、ローカルでは動作したのか
statusがunhealthyとなっているので、やはり問題があるのはヘルスチェックの箇所です。ヘルスチェックを振り返ってみると、DockerfileでもECSのタスク定義でも、curl -f http://localhost:1323/ || exit 1
のようなcurl
を用いたコマンドをヘルスチェックに指定しています。
結論、原因はこのcurl
コマンドが、distroless
イメージにはインストールされていないということでした。
ローカルの環境で正常に動作していたのは、ローカルのdocker-compose
がstatus: unhealthy
の場合でも停止するわけではないからです。docker container ls
で見てみると、STATUS
がUnhealthy
のままでもコンテナが動作しています。逆にECSではstatus: unhealthy
なら異常だと判断して、このタスクを止めてしまっていました。
対処法を考える
対処法としてはいくつか考えられるのかと思いますが、私たちの中では以下のような選択肢を考えていました。
- ヘルスチェックの方法を変更する
- そもそもヘルスチェックを行わない
-
curl
以外のコマンドを候補に入れる
-
distroless
イメージにcurlをインストールする -
distroless
イメージ以外を使用する
ここから最終的に取った手段は、「distroless
イメージ以外を使用する」というものでした。厳密に言えばそれにプラスして、curl
以外のコマンドとしてwget
コマンドを利用するようにもしています。
なぜその対処法にしたか
まず「ヘルスチェックをそもそもしないようにする」というのは最終手段と考え、一旦は考慮から省いていました。
そしてcurl
以外の方法を使うについては、より具体的に次の2つの方法がありました。wget
のような類似のコマンドを利用するものと、Go言語やその他言語でヘルスチェック用のスクリプトを実装する方法です。
そのうちwget
は、curl
と同様にデフォルトではdistroless
イメージにインストールされていませんでしたので無理です。ヘルスチェック用のスクリプトを作るのは、できなくは無いけどヘルスチェックのためだけには新たにコードを書いて、ビルドなどの環境を整えるのはしんどいなぁといった気持ちでした。
distroless
イメージにcurl
をインストールするのは、例えば前段のビルドステージでcurl
のバイナリを用意しておき、それを実行ステージにコピーしてくるといった方法が考えられます。ただ、その用意してインストールするという手順がDockerfileに増えるので、内容が複雑になり好ましくは無いなぁーといった気持ちでした。
対処法
最終的には、まずdistroless
イメージを使用するのを諦めました。そもそもdistroless
イメージを選んだのも軽量だからであって、別の軽量イメージがあればそれで問題ありません。
そこで代わりに採用したのがAlpine Linux
のイメージです。distroless
イメージのように搭載パッケージを削り軽量化したLinux環境ですが、ある程度のコマンドは利用ができました。使えたもののうちの1つがwget
のコマンドです。curl
はこのalpineの中でも使えませんでした。
wget
コマンドは、Web上のコンテンツをダウンロードするようなコマンドです。ただ、オプションの指定によってcurl
のような疎通確認にも利用できるようでした。このwget
コマンドを用いてヘルスチェックするように設定を変更しました。
################## 実行ステージ ##################
- FROM gcr.io/distroless/static-debian11
+ FROM alpine:3.19
WORKDIR /app
# ビルドしたバイナリをコピー
COPY --from=builder /app/main .
- HEALTHCHECK CMD curl -f http://localhost:1323/ || exit 1
+ HEALTHCHECK CMD wget --quiet --spider http://localhost:1323/ || exit 1
CMD [ "./main" ]
同様にECSのタスク定義も次のように書き換えました。
"healthCheck": {
"command": [
"CMD-SHELL",
- "curl -f http://localhost:1323/ || exit 1"
+ "wget --quiet --spider http://localhost:1323/ || exit 1"
]
}
wget
コマンドのオプションについて、--quiet
オプションはwget
コマンドの出力を省くもの、--spider
オプションはダウンロードを行わないというオプションです。
これらの変更でECS上でも正常にタスクが動作し、ステータスもHEALTHY
の状態になることが確認できました。
ちなみに、alpine
イメージを使用した際のイメージサイズは15.19 MB
でした。distroless
イメージより大きくはなりますが、元のイメージサイズから大きく減少していて、本来の目的は果たせただろうという判断になりました。
終わりに
この記事を書くために改めて情報を整理していたら、alpine
はオススメしないという記事も見つけました。なんでもパフォーマンスが落ちたりするのだとか…。
詳細まで見られていませんが、内容を詳しく見てみて私自身も対応は考えたいなと思っています。この記事にも参考情報として載せておきます。
最後に、ここまでで作成してきたソースコードは、以下のリポジトリにあります。必要に応じて参照してください。
参考