fbpx

DockerfileのCMDとENTRYPOINTを読み解く(1/3) — Shell形式とExec形式とは何か #docker #dockerfile

この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。

New call-to-action

Dockerイメージを作成するためのDockerfileには多くの命令があります。中でもCMD命令とENTRYPOINT命令は、コンテナ内で実行するPID 1のプロセスを指定するものです。言いかえると「このイメージは一体何をするのか、何のためのイメージなのか」という性質を決定づける命令です。しかしこの両命令にはいろいろな特徴があり、理解するには一筋縄ではいきません。

このCMD命令とENTRYPOINT命令についての理解を深めるため、全3回のブログシリーズとして掲載していきます。なお、Linux版Dockerコンテナの内容であることをご了承ください。

本稿では、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命令の基礎」について見ていきましょう。

また、クリエーションラインではMirantis社公認Dockerトレーニングを提供しております。さらにDockerを習得するために本トレーニングの受講を是非ご検討ください。

Author

Chef・Docker・Mirantis製品などの技術要素に加えて、会議の進め方・文章の書き方などの業務改善にも取り組んでいます。「Chef活用ガイド」共著のほか、Debian Official Developerもやっています。

Daisuke Higuchiの記事一覧

新規CTA