オブジェクトの広場はオージス総研グループのエンジニアによる技術発表サイトです

コンテナ・マイクロサービス

さわって理解する Docker 入門

第7回 バインドマウントで Hello Java しよう
オージス総研
樋口 匡俊
2018年2月8日

本連載では、Docker に興味はありつつもまだ触ったことのない方向けに、実際に Docker を触って理解していただくための記事を提供します。今回は Docker のバインドマウント機能について説明します。また、バインドマウントの具体的な例として、Java の Hello World プログラムをコンパイルし実行する方法を紹介します。

Docker のマウント機能

「マウント」という用語については、Linux や Windows など OS の用語としてご存知の方が多いかと思います。例えば「ハードディスクをマウントする」と言った場合、OS がハードディスクを認識し、利用できる状態にすることを意味します。

Docker における「マウント」とは、コンテナの外にあるデータを、コンテナの中で利用できる状態にすることを意味します。昨年末に公開された Docker v17.12.0-ce では、以下の三種類のマウント機能が提供されています。

  1. バインドマウント (bind)
  2. ボリュームマウント (volume)
  3. 一時ファイルシステムマウント (tmpfs)

今回説明する1番のバインドマウントは、Dockerホストのファイルやディレクトリを利用できるようにする機能です。バインドマウントを行うと、コンテナの外にある Dockerホストのファイルを、コンテナの中から読み書きできるようになります。バインドマウントしたファイルは、「/home」や「/var」のようなパスを用いて、もとからコンテナの中にあるファイルと同じように利用できるのです。

2番のボリュームマウントは、Dockerホストのファイルやディレクトリのうち、Dockerが管理しているものを利用できるようにする機能です。3番の一時ファイルシステムマウントは、Dockerホストにファイルとして保存したくないデータを一時的に利用できるようにする機能です。どちらも今回詳細は説明しませんが、データの利用目的に応じてそれぞれを使い分けるとよいでしょう。

マウント概要

コンテナの隔離性 (isolation)

バインドマウントは Docker の初期の頃から提供されている機能であり、現在でも非常によく利用されています。バインドマウントが必要とされる理由は、コンテナの特徴の一つである隔離性 (isolation) と強く関係しています。

通常、コンテナはそのコンテナの中にあるデータだけを利用できます。あるコンテナから、別のコンテナの中にあるデータや、Dockerホストのデータを利用することはできません。例えば、コンテナの中で ls /home というコマンドを実行しても、出力されるのはコンテナの中の /home の情報だけです。別のコンテナの /home や、Dockerホストの /home の情報はまったく出力されません。そのような性質を、コンテナの隔離性といいます。

コンテナはファイルやディレクトリを隔離するだけではなく、プロセスやネットワークなども隔離してくれます。Docker が仮想化技術の一種と言われるのは、そのように様々なものを隔離し、仮想的に独立した環境を作り出してくれるためです。

隔離性は、Dockerにとって非常に重要な性質です。隔離性を高めることには、Dockerホストなど周辺環境への依存度を低下させるなど、様々な利点があります。しかしその逆に、隔離性が高いことで不便を感じる場合もあります。

例えば、今回取り上げる Java の Hello World プログラムのように、ちょっとしたファイルを作成してコンテナで利用する場合を考えてみましょう。Dockerホストにファイルを作成した場合、コンテナの中からはそのファイルを読み書きできません。コンテナの中でファイルを作成すれば読み書きは当然できますが、普段使いなれたテキストエディタなどがコンテナの中にあるとは限りません。また、コンテナの中で作成したファイルは、コンテナを削除すると一緒に削除されてしまいます。削除される前に docker container cp コマンドを実行し、ファイルをコンテナの外にコピーすることもできますが、なんだか面倒な感じがします。

そのような状況を、バインドマウントは隔離性を下げることで解消してくれます。通常はコンテナの中から読み書きできない Dockerホストのファイルを、バインドマウントしたものに限り、特別に読み書きできるようにしてくれるのです。

事前準備

ではここからは Java の Hello World プログラムを例に、バインドマウントの具体的な使い方について見ていきましょう。概要は以下のとおりです。

  • 事前準備
  • 隔離性の実験:バインドマウントしないとどうなるか
  • バインドマウントの基本:mountオプション
  • Java のコンパイル: mountオプション
  • Java の実行: workdirオプション と mountオプション
  • alias の活用
  • Windows 上のファイルのバインドマウント
  • volume オプション

まずは事前準備として、Dockerホストを用意してください。本記事の内容は、Docker Machine v0.12.2 を用いて Windows 7 上の VirtualBox に作成した Dockerホストで検証しています。Docker Machine の使い方については、第5回を参考にしてください。

Docker のバージョンは v17.06 以上としてください。このあと --mount というオプションを使用するためです。本記事の Docker のバージョンは v17.12.0-ce です。

Dockerホストが用意できたら、Java のコンテナを動かしてみましょう。今回は Docker の公式イメージである openjdkイメージ を利用します。

以下のコマンドを実行し、Java のバージョンを確認してください。

$ docker run --rm openjdk:9 java --version
openjdk 9.0.1
OpenJDK Runtime Environment (build 9.0.1+11-Debian-1)
OpenJDK 64-Bit Server VM (build 9.0.1+11-Debian-1, mixed mode)

openjdk:9イメージから作成されたコンテナの中で、java --version というコマンドが実行され、バージョン情報が出力されました。このように今回は Docker の公式イメージである openjdkイメージ のうち、JDK のバージョン9系が含まれているイメージを利用します。

上記のコマンドで --rm オプションを指定していることに注目してください。--rm は、コンテナが停止したら自動的に削除するオプションです。このあと何度も Java コンテナを作成・起動するのですが、何もしなければコンテナは次々と溜まっていく一方ですので、このように --rm オプションで自動的に削除します。

最後に、Dockerホストに以下のような Javaファイルを作成してください。

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, World");
    }
}

これを /home/docker/Hello.java として保存します。繰り返しになりますが、この「Hello.java」は Dockerホスト上に作成してください。

事前準備はこれで完了です。

隔離性の実験:バインドマウントしないとどうなるか

それでは、まずはじめにバインドマウントしないとどうなるのか、隔離性とはどういったものなのか確かめてみましょう。

以下のコマンドを実行してみてください。

$ ls /home/docker/Hello.java
/home/docker/Hello.java
$ docker run --rm openjdk:9 ls /home/docker/Hello.java
ls: cannot access '/home/docker/Hello.java': No such file or directory

1つ目の ls は Dockerホストで実行していますので、Dockerホストの /home/docker ディレクトリにある Hello.java が見えています。

2つ目の ls はコンテナの中で実行していますので、コンテナの中の /home/docker ディレクトリを探しにいきます。コンテナの中のディレクトリは隔離されているので、Dockerホストの Hello.java は見つかりません。(そもそも openjdkイメージから作成したコンテナには、/home ディレクトリはありますが、/home/docker ディレクトリはありません。)

さらに実験してみましょう。今度は cat コマンドを実行して、Hello.java の中身を出力してみてください。

$ cat /home/docker/Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, World");
    }
}

$ docker run --rm openjdk:9 cat /home/docker/Hello.java
cat: /home/docker/Hello.java: No such file or directory

やはり2つ目の cat は、コンテナの中で実行しているため、Dockerホストの Hello.java を見つけられませんでした。

最後に、コンテナで Java のコンパイルができるか試してみましょう。

$ docker run --rm openjdk:9 javac Hello.java
javac: file not found: Hello.java
Usage: javac <options> <source files>
use --help for a list of possible options

やはりエラーが出力されました。コンテナの javac コマンドを実行し、Hello.java をコンパイルしようとしたのですが、Dockerホストの Hello.java が見つけられませんでした。

せっかく公式イメージから JDK のコンテナを簡単に作成できるのに、このままでは Hello.java をコンパイルできません。

コンテナの隔離性

バインドマウントの基本:mountオプション

それでは、バインドマウントを用いて Hello.java をコンテナの中で利用できるようにしましょう。

バインドマウントの設定は、--mountオプションで行います。以下のように、バインドマウントを設定して cat コマンドを実行してください。

$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test openjdk:9 cat /home/test/Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, World");
    }
}

Hello.java の中身が出力されました!コンテナの中で実行した cat から、Dockerホストの Hello.java が見えているということですね。

--mountオプションを見てみましょう。--mountオプションには、「キー=値」の形式でマウントの設定を記述します。「キー=値」はカンマ区切りで複数指定できます。カンマの左右に空白を付けないよう注意してください。各「キー=値」の意味は以下のとおりです。

  • type=bind

    マウントの種類です。 指定できる値は、冒頭で紹介した三種類 bind, volume, tmpfs です。 ここではバインドマウントを行うため、bind と指定しています。

  • src=/home/docker

    マウントする Dockerホストのディレクトリです。 ここでは Hello.java が入っている /home/docker を指定しています。 一つ上の /home を指定してもよいです。 その場合、/home の下の /home/dockerHello.java も一緒にマウントされます。 src のかわりに、source と書いても同じ意味になります。

  • dst=/home/test

    マウントする Dockerホストのディレクトリの、コンテナの中におけるパスです。 コンテナの中では /home/test というパスで、上記 src に指定した /home/docker を利用できるようになります。 /home/test ディレクトリは、もとのコンテナの中に存在していても、存在していなくてもかまいません。 dst のかわりに、destinationtarget と書いても同じ意味になります。

バインドマウント

Java のコンパイル: mountオプション

バインドマウントができるようになりましたので、Hello.java をコンパイルしてみましょう。

以下のコマンドを実行してください。

$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test openjdk:9 javac /home/test/Hello.java

これでコンパイルが行われたはずです。確認してみましょう。

$ ls /home/docker/Hello*
/home/docker/Hello.class  /home/docker/Hello.java

Hello.class が生成されています。コンパイル成功です。

バインドマウントを行うことで、Dockerホストの Javaファイルをコンテナでコンパイルし、クラスファイルを Dockerホストに出力できることが分かりました。

Java の実行: workdirオプション と mountオプション

次に、この Hello.class を実行してみましょう。

Hello.class を実行するコマンドは java Hello とします。ひとまず、先ほどコンパイルしたときの javac ... コマンドを java Hello に置き換えて実行してみましょう。

$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test openjdk:9 java Hello
Error: Could not find or load main class Hello
Caused by: java.lang.ClassNotFoundException: Hello

ClassNotFoundException が発生しました。Hello.class が見つからないようです。

クラスファイルが見つからないときの解決方法はいくつか考えられますが、ここでは --workdirオプション(省略形 -w)を使って、javaコマンドを実行するディレクトリを変えてみましょう。

--workdirオプションは、コンテナの中のどのディレクトリで処理を実行するかを指定できるオプションです。そのように処理を実行するディレクトリを、以下では作業ディレクトリと呼びます。

--workdirオプションを指定しない場合は、利用するイメージの設定に従い作業ディレクトリが決定されます。先ほど ClassNotFoundException が発生したコマンドの作業ディレクトリが何だったのか、pwdコマンドで調べてみましょう。

$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test openjdk:9 pwd
/

作業ディレクトリは、ルートディレクトリ(/)ですね。Hello.class が置いてある場所はルートディレクトリではなく、dst に指定した /home/testディレクトリであるため、ClassNotFoundException が発生したのでしょう。java Hello を実行する作業ディレクトリを、ルートディレクトリではなく /home/testディレクトリに変更すれば、Hello.class を見つけることできそうですね。

そこで以下のように --workdir オプションに /home/test を指定してみましょう。

$ docker run --rm --mount type=bind,src=/home/docker,dst=/home/test --workdir /home/test openjdk:9 java Hello
Hello, World

実行できました。Hello, World 成功です!

--workdirオプションは、バインドマウントや Java のコンテナに限らず、よく利用する便利なオプションです。今回のように、あるはずのファイルが見つからなくて困ったときなどに、思い出して使ってみるとよいでしょう。

alias の活用

バインドマウントは便利ですが、ちょっとコマンドが長いですね。以下のように、Linux の alias コマンドで短くしてみましょう。

$ alias java='docker run --rm --mount type=bind,src=/home/docker,dst=/home/test --workdir /home/test openjdk:9 java'

$ java Hello
Hello, World

2つ目のコマンド java Hellojava は alias ですので、実際にはコンテナが動いています。このように alias を活用すると、まるでローカルにインストールされているツールのようにコンテナを利用することができます。

Windows 上のファイルのバインドマウント

第5回のように Docker Machine を用いて Windows 上の VirtualBox に Dockerホストを作成している場合、Windows 上のファイルもバインドマウントすることができます。

どういうことなのか、以下のように Dockerホストの /c/Usersフォルダを眺めてみると直感的に理解できます。

$ ls /c/Users
All Users     Default User  Public/
Default/      Taro/     desktop.ini

お使いの Windows マシンによって異なりますが、おおよそ上記のような出力結果になると思います。なんだか見たことのあるフォルダが並んでいませんか? Windows の ユーザーフォルダですね。

この /c/Usersフォルダは、Docker Machine が自動的に設定してくれる VirtualBox の共有フォルダです。共有フォルダのおかげで、Windows のユーザーフォルダを Dockerホストのフォルダとして利用できる状態になっているのです。この共有フォルダを、例えば --mount src=/c/Users/Taro のようにバインドマウントすれば、Windows のファイルがバインドマウントされたのと同じように利用できるというわけです。

volume オプション

最後に --volumeオプション(省略形 -v)についても紹介しておきます。

今回紹介したバインドマウントとおおよそ同じような処理を、--volumeオプションを用いて実行することができます。--volume--mount よりもずっと以前から存在し、長らく利用されているオプションです。本記事では説明しませんが、--volumeオプションのサンプルコードや解説記事が多数存在しますので、検索してみるとよいでしょう。

しかし Docker社としては、これからマウント処理を学ぶ人は、より扱いやすい --mountオプションを利用することを推奨しています。筆者は、--mountオプションの使い方をおぼえると、第4回で紹介した Docker Compose やクラスタリング機能の Swarm、そして --volumeオプションについて理解しやすくなると考えています。そのため、最初は --mountオプションから学習することをおすすめします。

おわりに

今回は、Docker のバインドマウント機能について説明しました。また、バインドマウントの具体的な例として、Java の Hello World プログラムをコンパイルし、実行する方法を紹介しました。

バインドマウントは、コンテナの特徴である隔離性を敢えて下げて、コンテナを便利に利用できるようにする機能と言えます。コンテナの利点を大きく損なわないよう注意しながら、バインドマウントを上手に利用し、コンテナを活用できる場面を広げていきましょう。