DockerfileのCMDとENTRYPOINTを読み解く(1/3) — Shell形式とExec形式とは何か #docker #dockerfile
この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。
Dockerイメージを作成するためのDockerfileには多くの命令があります。中でもCMD命令とENTRYPOINT命令は、コンテナ内で実行するPID 1のプロセスを指定するものです。言いかえると「このイメージは一体何をするのか、何のためのイメージなのか」という性質を決定づける命令です。しかしこの両命令にはいろいろな特徴があり、理解するには一筋縄ではいきません。
このCMD命令とENTRYPOINT命令についての理解を深めるため、全3回のブログシリーズとして掲載していきます。なお、Linux版Dockerコンテナの内容であることをご了承ください。
- 第1回: Shell形式とExec形式とは何か ← 本稿
- 第2回: CMD命令とENTRYPOINT命令の基礎
- 第3回: CMD/ENTRYPOINT/Shell/Exec一覧表
本稿では、CMD命令とENTRYPOINT命令について詳しく見ていく前に、Dockerfileで命令を記述する際のShell形式とExec形式について見ていきます。
はじめに
CMD命令とENTRYPOINT命令をはじめとしたDockerfileの命令は、Shell形式とExec形式でコマンドを受け付けます。まず簡単に、Shell形式とExec形式の利点・欠点を述べておきます。
Shell形式:
- 利点
- 記述が簡単。
- シェル変数展開が利用できる。
- 欠点
- 指定のコマンドを「/bin/sh -c」に渡して実行するため、イメージに「/bin/sh」が含まれていなければならない。イメージサイズの肥大化や攻撃面の増加を招く。
- PID 1のプロセスが「/bin/sh」になる場合があり、シグナル送受などに影響する可能性がある。
Exec形式:
- 利点
- イメージに「/bin/sh」が必要ないため、イメージサイズと攻撃面を削減できる。
- 欠点
- 記述が面倒。
- シェル変数展開が利用できない。
では、Shell形式とExec形式について、サンプルDockerfileと実行例をまじえて詳しく見ていきましょう。
Shell形式
Shell形式とは、指定のコマンドを「/bin/sh -c」に渡して実行するものです。そのため「/bin/sh」がイメージに含まれていなければいけません。利点としては、一般のコマンドラインに書いているようなコマンドをそのままコピー&ペーストできること、「/bin/sh」に渡しているためシェル変数展開が利用できることです。
Dockerfile:
FROM busybox ENV COUNT 3 CMD /bin/ping -c $COUNT 8.8.8.8
イメージサイズ:
% docker image ls test REPOSITORY TAG IMAGE ID CREATED SIZE test latest 3ce0375a3809 24 hours ago 1.15MB
イメージ内容の確認:
% docker image save test | tar xvf - 3ce0375a38098cccbb74c98f8df4ab78400b0d5bb2668accfece068ec87eeb01.json e5bcc03d45cb7a99053f78b8fbb3833895ea7f04ff1393c234993d00958c8f1f/ e5bcc03d45cb7a99053f78b8fbb3833895ea7f04ff1393c234993d00958c8f1f/VERSION e5bcc03d45cb7a99053f78b8fbb3833895ea7f04ff1393c234993d00958c8f1f/json e5bcc03d45cb7a99053f78b8fbb3833895ea7f04ff1393c234993d00958c8f1f/layer.tar manifest.json repositories % tar tf e5bcc03d45cb7a99053f78b8fbb3833895ea7f04ff1393c234993d00958c8f1f/layer.tar | wc -l 413 % tar tf e5bcc03d45cb7a99053f78b8fbb3833895ea7f04ff1393c234993d00958c8f1f/layer.tar | grep -E 'bin/ping|bin/sh' bin/ping bin/ping6 bin/sh bin/sha1sum bin/sha256sum bin/sha3sum bin/sha512sum bin/showkey bin/shred bin/shuf
このように、/bin/pingや/bin/shだけでなく、他にも多くのファイルが含まれています。
実行結果:
% docker container run --rm --name test test PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: seq=0 ttl=117 time=9.731 ms 64 bytes from 8.8.8.8: seq=1 ttl=117 time=9.180 ms 64 bytes from 8.8.8.8: seq=2 ttl=117 time=9.265 ms --- 8.8.8.8 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 9.180/9.392/9.731 ms
このように、環境変数COUNTに設定した「3」の値の通りに3回パケットを送信しているので、シェルの変数展開が可能であることがわかります。
次に、中身が何も入っていないscratchイメージにマルチステージビルドを用いて、pingコマンドのみをコピーして実行してみましょう。
Dockerfile:
FROM busybox AS source # nothing to do. FROM scratch COPY --from=source /bin/busybox /bin/ping ENV COUNT 3 CMD /bin/ping -c $COUNT 8.8.8.8
イメージサイズ:
% docker image ls test REPOSITORY TAG IMAGE ID CREATED SIZE test latest 26b6278e007e 24 hours ago 1.07MB
イメージ内容の確認:
% docker image save test | tar xvf - 26b6278e007e9c45c66b79d88c4f5e1b4c9653a0acafaea18ae814ac5e1c8ee7.json bd338c5234a060c32e131a8e8b6d6eee793ad575b7549da01c6ccdf02beab453/ bd338c5234a060c32e131a8e8b6d6eee793ad575b7549da01c6ccdf02beab453/VERSION bd338c5234a060c32e131a8e8b6d6eee793ad575b7549da01c6ccdf02beab453/json bd338c5234a060c32e131a8e8b6d6eee793ad575b7549da01c6ccdf02beab453/layer.tar manifest.json repositories % tar tf bd338c5234a060c32e131a8e8b6d6eee793ad575b7549da01c6ccdf02beab453/layer.tar | wc -l 2 % tar tf bd338c5234a060c32e131a8e8b6d6eee793ad575b7549da01c6ccdf02beab453/layer.tar bin/ bin/ping
このように/bin/pingのみが含まれており、/bin/shを含んでいないぶんわずかですがイメージサイズも小さくなっています。
実行結果:
% docker container run --rm --name test test docker: Error response from daemon: OCI runtime create failed: container_linux.go:370: starting container process caused: exec: "/bin/sh": stat /bin/sh: no such file or directory: unknown. ERRO[0003] error waiting for container: context canceled
このように、CMD命令の引数(ここでは/bin/ping 〜)を渡すための /bin/sh が存在していないため実行できないということがわかります。
また、指定のコマンドを「/bin/sh -c」に渡して実行するため、指定のコマンドではなく「/bin/sh -c」がPID 1になってしまう場合があります(※例外あり。後述)。
Dockerfile:
FROM ubuntu:18.04 RUN apt update && apt install -y iputils-ping ENTRYPOINT ping 8.8.8.8
実行結果:
% docker container run --rm --name test -d test % docker container exec test ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 4628 764 ? Ss 08:07 0:00 /bin/sh -c ping 8.8.8.8 root 7 0.0 0.0 13456 1072 ? S 08:07 0:00 ping 8.8.8.8 root 41 0.0 0.0 34404 2848 ? Rs 08:16 0:00 ps aux
このように、ENTRYPOINT命令で指定した「ping 8.8.8.8」ではなく、「/bin/sh」がPID 1となっています。コンテナに何らかのシグナルを送信した場合、pingにシグナルを送信したつもりが実際に受け取るのは/bin/shとなり、意図した動作とならない場合があります。
ただし、/bin/sh の実体によって指定のコマンドがPID 1になる場合もあります。例えば、
- busybox:1.32 (ash)
- alpine:3.12.3 (ash)
- centos:7 (bash)
はPID 1となるのは/bin/shではなく指定のコマンドとなり、
- debian:9 (dash)
- ubuntu:18.04 (dash)
では「/bin/sh」がPID 1となりました。
Exec形式
Exec形式とは、指定のコマンドを直接実行するものです。そのためShell形式と異なり、「/bin/sh」がイメージに含まれている必要はありません。欠点としては、JSONフォーマットで記述しなければいけないため面倒なこと、シェルに渡していないためシェル変数展開が利用できないことです。
Dockerfile:
FROM busybox CMD [ "/bin/ping", "-c", "3", "8.8.8.8" ]
実行結果:
% docker container run --rm --name test test PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: seq=0 ttl=117 time=9.608 ms 64 bytes from 8.8.8.8: seq=1 ttl=117 time=9.373 ms 64 bytes from 8.8.8.8: seq=2 ttl=117 time=9.422 ms --- 8.8.8.8 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 9.373/9.467/9.608 ms
問題なく実施できました。では、環境変数を使ってみましょう。
Dockerfile:
FROM busybox ENV COUNT 3 CMD [ "/bin/ping", "-c", "$COUNT", "8.8.8.8" ]
実行結果:
% docker container run --rm --name test test ping: invalid number '$COUNT'
変数展開が行われず「$COUNT」そのものを引数と扱っているため、エラーとなってしまいました。
次に、中身が何も入っていないscratchイメージにマルチステージビルドを用いて、pingコマンドのみをコピーして実行してみましょう。
Dockerfile:
FROM busybox AS source # nothing to do. FROM scratch COPY --from=source /bin/busybox /bin/ping CMD [ "/bin/ping", "-c", "3", "8.8.8.8" ]
実行結果:
% docker container run --rm --name test test PING 8.8.8.8 (8.8.8.8): 56 data bytes 64 bytes from 8.8.8.8: seq=0 ttl=117 time=10.383 ms 64 bytes from 8.8.8.8: seq=1 ttl=117 time=9.302 ms 64 bytes from 8.8.8.8: seq=2 ttl=117 time=9.130 ms --- 8.8.8.8 ping statistics --- 3 packets transmitted, 3 packets received, 0% packet loss round-trip min/avg/max = 9.130/9.605/10.383 ms
このように、Exec形式ではCMD命令の引数(ここでは/bin/ping 〜)を直接実行するため、/bin/shが存在していなくても実行できていることがわかります。
Shell形式とExec形式のまとめ
再び、Shell形式とExec形式の利点・欠点の簡単なまとめです。
Shell形式:
- 利点
- 記述が簡単。
- シェル変数展開が利用できる。
- 欠点
- 指定のコマンドを「/bin/sh -c」に渡して実行するため、イメージに「/bin/sh」が含まれていなければならない。イメージサイズの肥大化(たった100KB?ちりも積もれば…と言います)や攻撃面の増加を招く。
- PID 1のプロセスが「/bin/sh」になる場合があり、シグナル送受などに影響する可能性がある。
Exec形式:
- 利点
- イメージに「/bin/sh」が必要ないため、イメージサイズと攻撃面を削減できる。
- 欠点
- 記述が面倒。
- シェル変数展開が利用できない。
Shell形式とExec形式の特徴を見た次は、「CMD命令とENTRYPOINT命令の基礎」について見ていきましょう。
- 第1回: Shell形式とExec形式とは何か
- 第2回: CMD命令とENTRYPOINT命令の基礎 ← 次稿
- 第3回: CMD/ENTRYPOINT/Shell/Exec一覧表
また、クリエーションラインではMirantis社公認Dockerトレーニングを提供しております。さらにDockerを習得するために本トレーニングの受講を是非ご検討ください。