ENECHANGE所属のエンジニア id:tetsushi_fukabori こと深堀です。
この記事を執筆している日にMLBのアーロン・ジャッジ選手がシーズン62本目のホームランを打ってアメリカンリーグのシーズンホームラン記録を塗り替えたことが伝えられました。
メジャーリーグの長い歴史に新たな1ページが加わった日に同席したと思うと、なんとなく気分が盛り上がりますね。
今回は「既存アプリケーション基盤のコンテナ化プロジェクト」でアプリケーションのコンテナ化、具体的にはDockerfileの作成で四苦八苦した内容を振り返ります。
このテーマ自体はDockerfileを書いている人なら何度も見た内容だと思いますが、実際に困ったので整理しておいて損はないと思い書いています。
同じ苦労をする人が少しでも減れば嬉しいです。
この記事を届けたい人
Dockerfileを書いた人で「buildはできるけどrunすると何故かプロセスが(正常)終了し、原因がわからない」人です。
私自身がそうなりました。
私の場合は
- コンテナをローカルで起動するとRailsアプリがデーモンとして立ち上がってくれるはずのコンテナが終了コード0で正常終了する
- ログ(標準出力・標準エラー出力)には何も怪しい表示が出ない
- (当然ながら)AWS ECS上ではコンテナが即終了してタスク起動に失敗する
という状態になりました。
トラブル事例
私の事例です。
(あとから見直すとこの失敗状態を公開するのはやや恥ずかしいですね。)
以下のようなDockerfileとentrypoint.shを作成しました。
# Dockerfile FROM docker.io/ruby:2.7.1-slim-buster # 諸々のビルド ENTRYPOINT ["/bin/bash", "-c", "${APP_ROOT}/.container/web/entrypoint.sh"] CMD ["/bin/bash", "-c", "bundle exec pumactl start -F ${APP_ROOT}/.container/web/pumaconf.rb"]
# entrypoint.sh #!/usr/bin/env bash set -e bundle exec rails db:migrate exec "$@"
この状態でコンテナを起動するとコンテナは即時正常終了します。特にエラーのログは出ません。
今見直すと「あーそれはそうね」となりますね。
当時は大ハマリしていました。
この事象の原因はENTRYPOINT
とCMD
の振る舞いです。
加えて以下のようなポイントが影響を及ぼしました。
- Dockerfile内で環境変数の展開を含むコマンドを実行したかった
- シェルスクリプト内での
$@
の振る舞いを正しく理解しないまま他のプロダクトから動いているentrypoint.shをコピペしてしまった
DockerfileにおけるENTRYPOINT
とCMD
ではDockerfileに於いてENTRYPOINT
とCMD
とは何でしょうか。
これらはDockerfileに記述できる命令(instruction)です。
日本語と英語それぞれのdockerのドキュメントから引用すると、それぞれの命令は以下のことを行ってくれます。
ENTRYPOINT
CMD
日英ともに同じことを示しています。
内容を読むといずれも「実行時」「実行」をキーワードとしていて、「コンテナ実行時に何を行うか」を指定していると読み解けます。
では複数の命令が同じ「コンテナ実行時に何を行うか」を指定する、というのはどういうことでしょうか?
Dockerfileに両方を指定する、または両方を指定しない場合はどうなるのでしょうか?
(以降の議論ではdocker run
の引数としてコマンドを与えてCMD
を上書きしたり、--entrypoint=
オプションでENTRYPOINT
を上書きする場合については簡単のため省略しています。あくまでDockerfile内で指定されたENTRYPOINT
とCMD
がどう振る舞うかについての整理ということをご了承下さい。)
ENTRYPOINT
とCMD
の相互作用
ENTRYPOINT
とCMD
はどちらも実行時に何を行うかの指定に使える命令ですので、相互作用があります。
相互作用の内容はこちらも公式ドキュメント(日本語、英語)にまとまっているのですが、日英で記載内容に差分があります。
実験したところ実際のふるまいと一致しているのは英語版の方だったので日本語ドキュメントの修正PRを送りました。マージされたら嬉しいです。
相互作用は以下のとおりです。
それぞれの命令で記載された内容はコンテナ内で実行されるコマンドとして考えるとわかりやすいので、実行されるコマンドであるという意味で$
記号始まりの形式で書いています。
(ただし後述するように記述形式によっては実行されるコマンドだと理解すると誤るポイントもあるので、あくまでわかりやすさのためのイメージだと理解して下さい。)
また、固定で入る/bin/sh -c
は灰色、ENTRYPOINT由来のコマンド・引数は赤色、CMD由来のコマンド・引数は緑色にそれぞれ色付けしています。
この表を読むためにはexec形式とshell形式の説明が必要ですので、次で説明します。
ENTRYPOINT
とCMD
のexec形式とshell形式
exec形式とshell形式はどちらもコマンドの実行を記述する形式ですが、大きく「コマンドとしてのシェルを実行するか」どうかが異なります。
最も大きな影響があるとすると環境変数の展開かと思います。
exec形式では環境変数は展開されませんが
# Dockerfile FROM docker.io/ruby:2.7.1-slim-buster ENTRYPOINT ["echo", "$HOME"]
$ docker container run sample:1
$HOME
shell形式ではシェルが実行されシェルによって環境変数が展開されます。
# Dockerfile FROM docker.io/ruby:2.7.1-slim-buster ENTRYPOINT echo $HOME
$ docker container run sample:1 /root
exec形式で環境変数を展開したければシェル自体を実行してその引数としてコマンドを与えるよう記述することが可能です。
# Dockerfile FROM docker.io/ruby:2.7.1-slim-buster ENTRYPOINT ["/bin/sh", "-c", "echo $HOME"]
$ docker container run sample:1 /root
上記の振る舞いはENTRYPOINT
とCMD
どちらも共通です。
なお、DockerのドキュメントではENTRYPOINT
とCMD
どちらもexec形式が推奨(ENTRYPOINT、CMD)されています。
このように複数の形式があることを理解した上で表を見直すと、一部気になるところが出てくると思います。
まずexec形式では実行時には
$ /bin/sh -c [exec形式で与えた文字列]
の形式で実行され、shell形式では
$ [shell形式で与えた文字列]
のような形式で実行されます。
ENTRYPOINT
のみ、CMD
のみの行・列を参照していただければそうなっているとわかると思います。
次にENTRYPOINT
とCMD
どちらも存在する場合、CMD
で与えた文字列はENTRYPOINT
で与えた文字列の引数になっています。
ただしENTRYPOINT
がexec形式の場合に限って、です。ENTRYPOINT
がshell形式の場合はCMD
で与えた文字列は無視されます。
このようにENTRYPOINT
とCMD
はどちらも存在する場合に非対称性があり、非対称性は記述形式に左右される部分が存在するというのが大きなポイントです。
トラブル事例は何が悪かったのか?
トラブル事例は以下のような思考の順序で作られました。
ENTRYPOINT
とCMD
が併存するDockerfileを既存プロジェクトからコピペして利用しようENTRYPOINT
とCMD
で実行したい命令に環境変数を含めて展開したいので、exec形式でシェルにわたすかshell形式で記述しよう- Dockerの推奨はexec形式なので
["/bin/sh", "-c", "[環境変数を含む命令]"]
の形式で記述しよう ENTRYPOINT
の命令の引数としてCMD
で与えた文字列が渡されるのでENTRYPOINT
で実行するentrypoint.shの中で引数を参照($@
)してexec
コマンドで実行しよう
しかしこの結果作られた以下のDockerfileは実行時に以下のように実行されます。
# Dockerfile FROM docker.io/ruby:2.7.1-slim-buster # 諸々のビルド ENTRYPOINT ["/bin/bash", "-c", "${APP_ROOT}/.container/web/entrypoint.sh"] CMD ["/bin/bash", "-c", "bundle exec pumactl start -F ${APP_ROOT}/.container/web/pumaconf.rb"]
# docker runの結果は以下とコマンド実行と同じ $ /bin/bash -c "${APP_ROOT}/.container/web/entrypoint.sh" "/bin/bash" "-c" "bundle exec pumactl start -F ${APP_ROOT}/.container/web/pumaconf.rb"
この場合において/bin/bash -c
で実行されるentrypoint.shからは以降の引数は参照できません。
簡易なスクリプトを作ってentrypoint.sh内から$@
$*
$0
$1
$2
などを参照するとわかるのですが、ここで参照できるのは$0
がentrypoint.shであることだけです。
# Dockerfile FROM docker.io/ruby:2.7.1-slim-buster COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/bin/sh", "-c", "/entrypoint.sh"] CMD ["/bin/sh", "-c", "echo $HOME"]
#!/bin/bash echo "\$@ : $@" echo "\$* : $*" echo "\$0 : $0" echo "\$1 : $1" echo "\$2 : $2" exec "$@"
$ docker container run sample:1 $@ : $* : $0 : /entrypoint.sh $1 : $2 :
つまりトラブル事例ではCMD
で与えた命令はentrypoint.shには引き渡されず、最後のコマンドの実行は以下のように動きます。
$ exec "$@" #=> exec ""と同じ #=> なにも実行せず正常終了
これにより「なんのログも出さず正常終了するコンテナ」が生まれてしまっていたのでした。
どう対処したか
ENTRYPOINT
及びCMD
で実行したい命令には環境変数が必要である、という前提を守ると自然と取れる手段が限定されます。
ENTRYPOINT
をexec形式で/bin/bash -c
を呼び出す形にするとCMD
の命令がentrypoint.shから参照できません(トラブル事例そのもの)ENTRYPOINT
をshell形式で実行するとCMD
の命令は無視されます(Dockerfileの仕様)
この時点でENTRYPOINT
とCMD
の併存はできなくなります。
どちらか1つに絞る必要が出てきますが、今回のコンテナはdocker run
の引数としてコマンドや--entrypoint=
を受け取る想定がないので、より通常の使用方法では上書きされにくいENTRYPOINT
に片寄せし、以下のような実装としました。
# Dockerfile FROM docker.io/ruby:2.7.1-slim-buster # 諸々のビルド ENTRYPOINT ["/bin/bash", "-c", "${APP_ROOT}/.container/web/entrypoint.sh"]
# entrypoint.sh #!/usr/bin/env bash set -e bundle exec rails db:migrate exec bundle exec pumactl start -F ${APP_ROOT}/.container/web/pumaconf.rb
なお、大事な余談ですが、たとえトラブル事例の$@
がうまくCMD
を引き渡せていたとしても良い結果にはならなかったと思われます。
その場合最後のコマンドはexec /bin/bash -c bundle exec pumactl start -F ${APP_ROOT}/.container/web/pumaconf.rb
となったかと想像されますが、この場合コンテナのメインプロセスは(おそらく)bashになります。
コンテナのメインプロセスがbashの場合、bashの実装依存ではあるようですがシグナルを伝播できません。
つまりコンテナのメインプロセスがSIGTERM
で正常終了できずSIGKILL
による強制終了のみになっていたはずです。
これはコンテナアプリケーションとしては避けるべき状態で、今回のコンテナが稼働する予定のAWS ECSでもSIGTERM
で終了できるよう求められています。
まとめ
コンテナ環境で稼働するアプリケーションを作る上で欠かせないDockerfileの仕様をまとめ、仕様を整理できていなかったがゆえに発生していたトラブル事象を解決しました。
今回の事象には以下の要素が影響しあって発生していました。
- Dockerfileの
ENTRYPOINT
とCMD
のexec形式とshell形式の違い - Dockerfileの
ENTRYPOINT
とCMD
の相互作用 - シェルの環境変数展開
- シェル引数が受け取れる内容
コンテナ環境で正しく動くアプリケーションを作るには様々な知識が求められます。
この記事もあるトラブルの一例だけしか解決していないので、他にも様々な苦労があるかと思いますが、この記事を読んだ人がこの事象だけは短時間で解消できることを期待しています。