1
1
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Go言語のAPIサーバをマルチステージビルドでAWS ECSにデプロイしたら、ヘルスチェックが通らなかった

Posted at

要約

はじめは、ヘルスチェックにcurlコマンドを用い、Go言語のバイナリ実行のステージにdistrolessイメージを利用する設定にしていました。

ですが、distrolessイメージは極力コマンドなどを削った環境であり、curlコマンドもインストールされていないようで、ヘルスチェックが行えませんでした。

対処法の候補はいくらかあるかと思いますが、私はdistrolessイメージの使用を辞め、alpineイメージを使用するようにし、ヘルスチェックのコマンドもcurlではなくwgetを使うように変更しました。

HEALTHCHECK CMD wget --quiet --spider http://localhost:1323/ || exit 1

背景

元々のコンテナイメージ

普段Go言語のAPIサーバを構築する際、実行環境はDockerで構築しています。いままでは以下のようなDockerfileでGo言語のプログラムを実行していました。

Dockerfile
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のタスク定義にも上記と同様のヘルスチェックコマンドを設定していました。

ECSのタスク定義(抜粋)
"healthCheck": {
    "command": [
        "CMD-SHELL",
        "curl -f http://localhost:1323/ || exit 1"
    ]
}

この段階での問題点

この方法で問題視していたのは、コンテナイメージのサイズです。

先ほどもいったようにビルドしたコンテナイメージはAWSのECR上にプッシュしています。このECRですが、課金体系を見るとストレージ容量に応じた課金があります。したがって、イメージサイズが小さければそれだけコスト面で嬉しいです。

そこで、マルチステージビルドという手法を採用することで、最終的なイメージサイズを小さくしようということになりました。

マルチステージビルド

マルチステージビルドの概要について簡単にまとめます。

Go言語で言えば、プログラムをソースコードからビルドする段階と、ビルドしたバイナリを実行する段階をそれぞれ分け、ECRにはバイナリの実行環境のみアップロードするという方法です。

実行環境には最低限実行バイナリがあればよく、依存パッケージのソースコードなどは不要になります。それらを省くことで、その分だけイメージのサイズを小さくすることができます。

マルチステージビルドに変更してみた

マルチステージビルドを実装するために、参考にあげている記事なども参照しながら、以下のようにDockerfileを変更しました。

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-composestatus: unhealthyの場合でも停止するわけではないからです。docker container lsで見てみると、STATUSUnhealthyのままでもコンテナが動作しています。逆に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コマンドを用いてヘルスチェックするように設定を変更しました。

Dockerfile(抜粋)
################## 実行ステージ ##################
- 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のタスク定義も次のように書き換えました。

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はオススメしないという記事も見つけました。なんでもパフォーマンスが落ちたりするのだとか…。

詳細まで見られていませんが、内容を詳しく見てみて私自身も対応は考えたいなと思っています。この記事にも参考情報として載せておきます。

最後に、ここまでで作成してきたソースコードは、以下のリポジトリにあります。必要に応じて参照してください。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1