Docker入門(第四回)~Dockerfileについて~

こんにちは、Acroquest Technologyの横山です。

4/19(木)にKubernetesやDockerを中心としたコンテナ活用の現状をひとまとめにした開発者のためのイベントである「Japan Container Days v18.04」に参加してきました。コンテナの管理ツール(オーケストレーションツール)であるKubernetesを活用した事例が多数あり、とても刺激的でした。発表資料をまとめてくれている方がいますので、興味のある方は「Japan Container Days v18.04 の資料」を見てみてください。

はじめに

第三回では、各種Dockerコマンドの説明と、コンテナ内でTomcatインストールを行い、そのコンテナをDockerイメージにしました。
第四回では、Dockerfileを使ってDockerイメージを作成してみます。また、Dockerfileを使用することの利点について説明します。

Dockerfileを使ってDockerイメージを作成してみよう

Dockerfileとは

前回(第三回)でも少し触れましたが、公開されているDockerイメージをそのまま使うのではなく、必要なパッケージやアプリ、各種設定を含んだDockerイメージを自分で作成して使用するケースが出てきます。その場合、Dockerfileを作成し、それを使用して必要なDockerイメージを作成することができます。Dockerfileには、ベースとするDockerイメージに対して実行する内容を記述します。

まずは、実際にDockerfileを書いて、Dockerイメージを作成してみましょう。

前回、CentOSコンテナ起動後に、コンテナにログインしてTomcatインストールを行い、そのコンテナからDockerイメージを作成しました。今回は、それをDockerfileで記述して、TomcatのDockerイメージを作成します。また、Dockerイメージを作成する際は、基本的にはDockerfileを使用することをお薦めします。この理由については、具体例のあとに説明します。

Dockerfileの作成

それでは、TomcatのDockerイメージを作成するためのDockerfileを作成します。前回実施したTomcatのインストールをDockerfileで記述すると以下のようになります。

FROM centos:7               # ①
RUN yum install -y java     # ②
ADD files/apache-tomcat-9.0.6.tar.gz /opt/  # ③
CMD [ "/opt/apache-tomcat-9.0.6/bin/catalina.sh", "run" ]  # ④

①のFROMは、ベースとするDockerイメージを指定しています。今回は、centos:7のDockerイメージをもとに②以降を行います。また、事前にcentos:7のDockerイメージを取得(docker pull)していなくても、ローカルにない場合は、Dockerイメージ作成のコマンド実行時に自動で取得してきます。

②のRUNは、OSのコマンドを実行する際に使用します。ここでは、Javaのインストールを実行しています。「-y」オプションを付けて、インストールするかどうか聞かれないようにしておきましょう。

③のADDは、tar.gzファイルのコンテナへのコピーと、tarの展開を行います。「ADD <コピー元> <Dockerイメージ内のコピー・展開先>」の書き方です。今回の例の場合は、Dockerfileと同階層にfilesディレクトリを作成し、その中にtomcatの媒体を配置しておいてください。また、ADDコマンドと似たコマンドとして、COPYコマンドがあります。本連載では詳細は省略しますが、COPYコマンドはtarの展開はおこなわず、コピー処理のみおこないます。

④のCMDは、コンテナ起動時に実行するコマンドを記述します。CMDコマンドと似たコマンドとして、ENTRYPOINTコマンドがあります。本連載では詳細は省略しますが、気になる方は「CMD と ENTRYPOINT がどのように作用するか学ぶ」を参考にしてください。

また、上記だけでなく、Dockerfileの命令コマンドは他にも多数ありますので、「Dockerfile リファレンス」などを参考にしてください。

Dockerfileを使用して、Dockerイメージを作成

それでは、作成したDockerfileを使ってDockerイメージを作成します。Dockerfileのあるディレクトリに移動して、Dockerビルド(Dockerイメージを作成するコマンド)を実行します。

# cd <Dockerfileが存在するディレクトリ>
# docker build -t tomcat:1 .
 (docker build -t <Dockerイメージ名> <Dockerfileが存在するディレクトリ>)
# docker images
REPOSITORY   TAG   IMAGE ID       CREATED         SIZE
tomcat       1     10af894cf09a   1 minutes ago   456MB

これでTomcatのDockerイメージが作成できました。それでは、このDockerイメージを使用して、コンテナを起動します。

# docker run -it -d --name tomcat-1 -p 8081:8080 tomcat:1

これで、「http://<IPアドレス>:8081/」で接続できるはずです。また、以下のコマンドを実行することで、DockerfileのCMDで実行したコンソールログが確認できます。

# docker logs -f tomcat-1

Dockerfileを書く際のポイントや嵌りどころ、注意点について

Dockerfileの各コマンドは、毎回コンテナを起動して実行している

Dockerfileに書かれたコマンドは、毎回、中間的なDockerコンテナとして起動し、各コマンドを実行して各段階でDockerイメージを作成する、というのを繰り返します。この各段階のDockerイメージはレイヤーと呼ばれます。

そのため、一つ前のコマンド実行状態になっているわけではありません。例えば、以下のような記述があったとしましょう。

RUN cd /tmp           # ⑤
RUN touch test.txt    # ⑥

test.txtはどこに作成されるでしょうか。

それは、「/tmp」配下ではなく「/」配下に作成されます。明示的に指定しなければ、centos:7のDockerイメージの場合は、命令実行時の作業ディレクトリは「/」となっていて、Dockerfileの各種コマンドは「/」配下で実行されます。つまり、①の処理は中間的なDockerコンテナ上で「cd /tmp」は実行されますが、次の②の処理の際には新しく起動したコンテナで実行され、「/」配下で実行されます。もし、ディレクトリ移動してから実行したい場合などは、シェルの実行と同じように、「&&」などで連結して実行させればよいです。

また、実行ユーザや命令実行時の作業ディレクトリを変更したい場合には、Dockerfileで「USER」、「WORKDIR」を使用することで可能となります。

キャッシュを意識する

Dockerビルドをする際、Dockerfileですでに実行済みのコマンドは、キャッシュが使用されます。そのため、あまり変わらない部分については、Dockerfileの最初のほうに書いておくことで、Dockerビルドの時間が短縮できます。ここで言う「あまり変わらない部分」というのは、例えば基本必要となるパッケージのインストールなどです。ADDやCOPYコマンドでビルド媒体などをコンテナ内にコピーする際、コピー元のファイルに変更があった場合は、キャッシュは使用されず、新しい媒体がコンテナ内にコピーされます。それ以降は、キャッシュは使用されずに、再度一つ一つDockerfileのコマンドが実行されます。

実際に試してみましょう。以下のように「Dockerfile_2」を作成してください。

FROM centos:7
RUN yum install -y java
RUN touch /tmp/test.txt     # 変更箇所
ADD files/apache-tomcat-9.0.6.tar.gz /opt/
CMD ["/opt/apache-tomcat-9.0.6/bin/catalina.sh", "run"]

「Dockerfile_2」を使用してDockerビルドを行ってみます。「-f」オプションで、使用するDockerfileを指定することができます。(「-f」オプションを使用しない場合は、デフォルトで「Dockerfile」が使用されます。)

# docker build -t tomcat:2 -f Dockerfile_2 .
Sending build context to Docker daemon 9.499MB
Step 1/5 : FROM centos:7
---> 2d194b392dd1
Step 2/5 : RUN yum install -y java
---> Using cache   ・・・★
---> db67ba97aaff
Step 3/5 : RUN touch /tmp/test.txt
---> Running in dff4c625f388
Removing intermediate container dff4c625f388
---> e76913cfa2e2
Step 4/5 : ADD files/apache-tomcat-9.0.6.tar.gz /opt/
---> 63f1c3a602d2
Step 5/5 : CMD [ "/opt/apache-tomcat-9.0.6/bin/catalina.sh", "run" ]
---> Running in 2b9399b835a6
Removing intermediate container 2b9399b835a6
---> 566185029ac1
Successfully built 566185029ac1
Successfully tagged tomcat:2

「RUN yum install -y java」の部分までは、「Using cache(★の部分)」となり、キャッシュが使用されていることがわかります。最初に実行したときのJavaのインストール処理は表示されませんでしたね。

このように、変更がない部分は、以前作成されたレイヤー(中間的なDockerイメージ)が使用されるため、Dockerビルドの処理時間が短くなります。そのため、コマンドの実行順序を意識してDockerfileを書くとよいです。

なるべくまとめて実行する

Dockerfileの各コマンドごとにレイヤーが作成されるので、その分Dockerイメージのサイズが大きくなってしまいます。そのため、なるべく1コマンドで実行できるものは、まとめて実行することをお薦めします。また、作成できるレイヤーには上限(128レイヤー)があるため、その意味でもなるべくまとめて実行したほうがよいですね。

例えば、yumインストールについても、一つ一つを個別のyumコマンドで実行するのではなく、一度のyumコマンドで複数インストールするとよいです。あとは、キャッシュとの兼ね合いから、どこまでまとめて実行するかを考えていくとよいと思います。

Dockerfileを使うことの利点

Infrastructure as Code (IaC)、CI/CD

Dockerfileを作成することで、自分で好きなようにDockerイメージを作成することができるようになりました。では、このDockerfileを書くことでどのような利点があるでしょうか。

それは、OS設定などのインフラ構築部分も含めコード化できる、ということです。

これにより、同じ環境を簡単にミスなく作ることができ、またインフラ構築処理も構成管理することが可能になります。構成管理ツール(GitやSVNなど)でDockerfileを管理し、CIツール(Jenkinsなど)を使えば、アプリのビルドからDockerビルド(Dockerイメージの作成)、コンテナのデプロイまでを自動化することができます。さらにデプロイした環境に対して自動試験(Seleniumなど)を連携させるのもよいでしょう。

そうすれば、ソースコードや環境構築部分で変更が入った際、簡単に本番に近い状態でデプロイ&試験もでき、開発のサイクルを向上させることができます。本番に近い状態で常に試験を回せて成功していれば、リリース直前で問題が発生することも減り、開発者側としても安心して開発を進めることもできると思います。

このような考え方は「Infrastructure as Code (IaC)」と呼びます。上記に書いたように、ソースコード開発のプラクティスをインフラ面にも適用しています。

Dockerイメージの管理

Dockerfileだけでなく、Dockerイメージについても管理することができます。方法としては、有償のDocker Trusted Registryを使用するか、自分でプライベートなDocker Registryを構築するやり方があります。また、公開しても問題ないものでしたら、Docker Hubで公開することも可能です。

Docker RegistryでDockerイメージを管理しておけば、他の人も作成済みのDockerイメージを取得可能になります。例えば、開発環境用(特定のPythonバージョンが入った環境など)としてDockerイメージを作成した場合、それをDocker Registryで管理しておけば、ほかの開発者も簡単に同じ開発環境を取得して開発することができます。

まとめ

今回は、Dockerfileを使用してDockerイメージを作成する方法とDockerfileを使う良さについて説明しました。これで、必要なDockerイメージを自分で作成することができます。
次回は、複数のコンテナ間通信などについて説明する予定です。