apatheia.info

仮想環境構築に docker を使う

2013.06.17 docker lxc

ちょっと前から Docker を使っているので、その話。

Dockr について

Dockerdotcloud がオープンソースで公開している、コンテナ技術による仮想化ソフトウェア。

以下のテクノロジーベースにしている:

  • LXC
    • 前にも書いた。Xen とか VirtualBOX みたいにホスト内に仮想マシンを立ち上げるんじゃなくて、ホスト内の隔離された環境で仮想マシンを動かす技術。物理マシンをシミュレーションしているんじゃないってことは、VPS とか EC2 とかの仮想マシン上でも問題なく動くし、マシンを起動するプロセスが不要となるので、一瞬で使い始められるというメリットにつながっている。
  • AUFS
    • UnionFS(ディレクトリを重ね合わせることができる)の実装の一つ。元の仮想マシンイメージを書き換えないで、更新が発生した部分は別の場所に書き込んでいくようになっている。これにより、仮想マシンの立ち上げ時にイメージのコピーが発生しないので、すぐに使い始められる。

Docker を使う前は LXC のラッパーとして取っつきにくさを緩和してくれる、とかそういうレベルだと思ったんだけど、予想はよい方向に裏切られた。

仮想マシンのイメージを可視化したものを見ると、まるで Git のコミットログみたいに見えると思う。実際、情報は差分で管理され、履歴を残したり分岐させたりといった操作が非常に軽量にできていて、Git を操作するかのように仮想マシンを操作できるようになっている。

動かし方

Arch Linux や Debian で動かしている人がいるみたいだけど、公式サポートは今のところ Ubuntu のみ。Ubuntu 12.04 LTS を使っているのであれば、curl get.docker.io | sh -x で動くようになる。

ちゃんとしたやり方は ドキュメントを見れば、特にはまることもないと思う。できるだけ新しい Ubuntu を使っておけばいい。

すぐに試してみたいんなら、Vagrant 経由で簡単に使い始められる。

git clone https://github.com/dotcloud/docker.git
cd docker
vagrant up --provider virtualbox # or vagrant up --provider aws

基礎的な操作方法

インストールがうまくいって Docker が起動しているものとして、早速使ってみる。

$ docker
Usage: docker [OPTIONS] COMMAND [arg...]
  -H="127.0.0.1:4243": Host:port to bind/connect to

  A self-sufficient runtime for linux containers.

  Commands:
  attach    Attach to a running container
  build     Build a container from a Dockerfile
  commit    Create a new image from a container's changes
(以下省略)

コマンドがずらっと表示されるかと思う。まずは単発のコマンドをコンテナ内で実行してみる。

$ docker run base /bin/echo hi
Pulling repository base from https://index.docker.io/v1
Pulling image b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc (latest) from base
Pulling b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc metadata
Pulling b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc fs layer
Downloading 10240/? (n/a)
Pulling 27cf784147099545 metadata
Pulling 27cf784147099545 fs layer
Downloading 94863360/? (n/a)
Pulling image 27cf784147099545 () from base
hi

docker コマンドに run サブコマンドを指定して、base という仮想マシンで /bin/echo hi コマンドを実行する」という意味になる。仮想マシンがダウンロードされるが、これは初回実行時のみ。最後に表示された「hi」というのが今回の実行結果で、このコンテナの役割はこれで終わり。

今度は作ったマシンの中に入ってみるために、-i-t オプションで入出力できるようにして /bin/bash を起動してみる。

$ docker run -i -t base /bin/bash
root@bc43a290f0ce:/#

端末から抜けるとホスト側に制御が戻る。

root@bc43a290f0ce:/# exit
exit
$

今度は -d オプションでコマンドを実行しっぱなしにする。

$ docker run -i -t -d base /bin/ping -i 5 www.aikatsu.net
79365b2985c4
$

ID が返されて、すぐに端末が利用可能になる。稼働中のプロセスを確認してみる。

$ docker ps
ID                  IMAGE               COMMAND                CREATED             STATUS              PORTS
79365b2985c4        base:latest         /bin/ping -i 5 www.a   22 seconds ago      Up 21 seconds

次に実行中の出力をのぞいてみよう。

$ docker logs 79365b2985c4
PING www.aikatsu.net (60.32.7.37) 56(84) bytes of data.
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=1 ttl=49 time=282 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=3 ttl=49 time=278 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=4 ttl=49 time=283 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=5 ttl=49 time=266 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=6 ttl=49 time=268 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=8 ttl=49 time=264 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=9 ttl=49 time=270 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=10 ttl=49 time=290 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=11 ttl=49 time=284 ms

順調に動き続けているようなので、このジョブにアタッチしてみる。

$ docker attach 79365b2985c4
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=18 ttl=49 time=239 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=19 ttl=49 time=291 ms
64 bytes from www3.sunrise-anime.jp (60.32.7.37): icmp_req=20 ttl=49 time=275 ms
(出力が続く)

アタッチ中の端末は Ctrl-p Ctrl-q でデタッチできる。(このとき use of closed network connection っていうエラーが出る場合 Ctrl-c で抜けるしかないっぽい。バグレポートは上がっているので、じきに直ると思う。)

最後にkillでこのプロセスを消してみる。

$ docker kill 79365b2985c4
$ docker ps
$

psからプロセスが消えた。基礎的なコンテナの操作の説明は以上。

詳細

コンテナ

これまでコマンドを実行したり、kill されたコンテナはどうなっているのか。実は全部残っている。停止したコンテナを表示するために-aをつける。ついでに、情報を省略しないで表示するために-notrunc もつける。

$ docker ps -a -notrunc
ID                                                                 IMAGE               COMMAND                          CREATED             STATUS              PORTS
79365b2985c43a2a6977764f4dde2d375084020fbc04cc855508c417a36f88c2   base:latest         /bin/ping -i 5 www.aikatsu.net   14 minutes ago      Exit 0
bc43a290f0ced4677ee7eb1a0d662cca496cc720d8db20e746dda45e4659f503   base:latest         /bin/bash                        16 minutes ago      Exit 0
7a666192cca72cea81cade398b22700c982fbb9271a7eca23ff51c6c504d5971   base:latest         /bin/echo hi                     16 minutes ago      Exit 0
8b0af4fc390d762c33dadc1b149516ba95bdb70d093e991ec2df563817f55ffb   base:latest         /bin/bash                        21 minutes ago      Exit 0
4637bc6341706c25e066c5ccfe92e10c923bfe4955a9e8b3ce07237fda0fb34a   base:latest         /bin/echo hi                     21 minutes ago      Exit 0

正常終了しているので、すべてExit 0になっている。また、ID は省略表記されていたこともわかる。コンテナの実体は /var/lib/docker/containers/<ID> 以下に格納されている。

$ sudo ls /var/lib/docker/containers/
4637bc6341706c25e066c5ccfe92e10c923bfe4955a9e8b3ce07237fda0fb34a
79365b2985c43a2a6977764f4dde2d375084020fbc04cc855508c417a36f88c2
7a666192cca72cea81cade398b22700c982fbb9271a7eca23ff51c6c504d5971
8b0af4fc390d762c33dadc1b149516ba95bdb70d093e991ec2df563817f55ffb
bc43a290f0ced4677ee7eb1a0d662cca496cc720d8db20e746dda45e4659f503

どんどんたまっていくから心配かもしれないけど、各コンテナはベースイメージからの差分しかもたないので、問題にならない。もし、消したくなったら docker rm <コンテナのID> で消せる。

作業領域であったコンテナを commit するとイメージとして使い回せるようになる。ユーザー名/名称にするのが作法っぽい。

$ docker commit -m "My first container" 4637bc634170 f440/first_container
02036952e5dc
$ docker images
REPOSITORY             TAG                 ID                  CREATED
base                   latest              b750fe79269d        12 weeks ago
base                   ubuntu-quantl       b750fe79269d        12 weeks ago
base                   ubuntu-quantal      b750fe79269d        12 weeks ago
base                   ubuntu-12.10        b750fe79269d        12 weeks ago
f440/first_container   latest              02036952e5dc        3 seconds ago

これで今後は docker run f440/first_container をベースにしたコンテナを作れるようになる。

イメージ

もう一回イメージの一覧を内容を確認してみよう。

$ docker images
REPOSITORY             TAG                 ID                  CREATED
f440/first-container   latest              141fef9a2f57        14 seconds ago
base                   latest              b750fe79269d        12 weeks ago
base                   ubuntu-12.10        b750fe79269d        12 weeks ago
base                   ubuntu-quantl       b750fe79269d        12 weeks ago
base                   ubuntu-quantal      b750fe79269d        12 weeks ago

base イメージは latest, ubuntu-quantl, ubuntu-quantal, ubuntu-12.10 といった複数のタグがついていることがわかる。イメージは複数の名称をタグ付けできるようになっており、base:latest, base:ubuntu-12.10 といった形で異なるイメージを呼び出せるようになっている。省略時は base:latest と同じ。

pull してくるイメージは https://index.docker.io/ から情報を持ってくる。コマンドラインで検索したい場合は search コマンドを利用する。

$ docker search centos
Found 4 results matching your query ("centos")
NAME                          DESCRIPTION
centos
backjlack/centos-6.4-x86_64
creack/centos
mbkan/lamp                    centos with ssh, LAMP, PHPMyAdmin(root pas...

ローカルにキャッシュされたイメージを消すには docker rmi <イメージのID>でいい。

自前で作ったイメージを https://index.docker.io/ に登録するには、あらかじめサイト上でアカウントを作っておき、 docker login した後に docker push する。イメージ名にアンダーバー使っていると push で失敗するのと、アップロードしたイメージを消す機能がまだなかったりするので注意。

イメージの実体は /var/lib/docker/graph/ にある。

$ docker images -a -notrunc
REPOSITORY          TAG                 ID                                                                 CREATED
base                latest              b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc   12 weeks ago
base                ubuntu-12.10        b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc   12 weeks ago
base                ubuntu-quantl       b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc   12 weeks ago
base                ubuntu-quantal      b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc   12 weeks ago
<none>              <none>              27cf784147099545                                                   12 weeks ago

$ sudo ls -1 /var/lib/docker/graph
141fef9a2f57e86dd6d9aa58fe9318b0d9d71d91053079842051d9738bad6e45
27cf784147099545
b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc
checksums
:tmp:

ここで images に ID: 27cf784147099545 というのが現れた。これは何か。inspect を使うとイメージの詳細を表示できる。

$ docker inspect base
{
    "id": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc",
    "parent": "27cf784147099545",
    "created": "2013-03-23T22:24:18.818426-07:00",
    "container": "3d67245a8d72ecf13f33dffac9f79dcdf70f75acb84d308770391510e0c23ad0",
    "container_config": {
        "Hostname": "",
        "User": "",
        "Memory": 0,
        "MemorySwap": 0,
        "CpuShares": 0,
        "AttachStdin": false,
        "AttachStdout": false,
        "AttachStderr": false,
        "PortSpecs": null,
        "Tty": true,
        "OpenStdin": true,
        "StdinOnce": false,
        "Env": null,
        "Cmd": [
            "/bin/bash"
        ],
        "Dns": null,
        "Image": "base",
        "Volumes": null,
        "VolumesFrom": ""
    }
}

ID: 27cf784147099545 は base イメージの親イメージの ID であることがわかる。イメージは差分になっているので、親のイメージが必要ということで初回実行のタイミングで base と一緒に 27cf784147099545 もダウンロードされていたのだった。

ネットワーク

docker run 時に -p をつけることで、コンテナから外部にさらすポートを決められる。コンテナ側のポートはホスト側のポートに変換される際、ポート番号が変更される(49153以降になる)ので、docker port <ジョブのID> <ポート番号> あるいは docker ps でポートの対応状況を確認する必要がある。

ドキュメントの Expose a service on a TCP port がわかりやすい。

# 以下、コメントは書き換えてある
# また、途中経過がわかりやすいように set -x しておく
set -x

# 4444 を晒すよう -p オプションをつけて docker run しつつ、
# コンテナは netcat で4444を待ち受ける
JOB=$(docker run -d -p 4444 base /bin/nc -l -p 4444)
++ docker run -d -p 4444 base /bin/nc -l -p 4444
+ JOB=c86c892574f7

# 4444 がローカルのどのポートに対応するのか確認
# docker ps でも調べることはできる
PORT=$(docker port $JOB 4444)
++ docker port c86c892574f7 4444
+ PORT=49166

# ルーティングによっては localhost とか 127.0.0.1 だと
# うまくいかないことがあるので、eth0 のIPアドレスを使おう、
# ってことらしい
IP=$(ifconfig eth0 | perl -n -e 'if (m/inet addr:([\d\.]+)/g) { print $1 }')
++ perl -n -e 'if (m/inet addr:([\d\.]+)/g) { print $1 }'
++ ifconfig eth0
+ IP=10.156.137.111
echo hello world | nc $IP $PORT
+ nc 10.156.137.111 49166
+ echo hello world

# コンテナが受信したメッセージを logs で表示
echo "Daemon received: $(docker logs $JOB)"
++ docker logs c86c892574f7
+ echo 'Daemon received: hello world'
Daemon received: hello world

Dockerfile

DSLで書かれた設定(通常ファイル名はDockerfileとする)をあらかじめ用意することで、手順に従ってイメージを作ることができる。

読み込ませ方 (1)
docker build <Dockerfileのあるディレクトリ>
# ex. docker build .

読み込ませ方 (2)
docker build -
# ex. docker build - < /foo/bar/Dockerfile

Dockerfile の例

FROM base
RUN /bin/echo hi

これで、docker build すれば docker run base /bin/echo hi と同じ効果が得られる。

指定できるはコマンドは以下の通り。大文字小文字は区別しないけど、引数と見分けやすいように大文字が使われる。

  • FROM <image> ベースとなるイメージを指定
  • MAINTAINER <name> メンテナの名前を指定
  • RUN <command> ビルド中に実行したいコマンドを指定
  • CMD <command> 起動後のコンテナで実行したいコマンドを指定
  • EXPOSE <port> [<port> ...] 外部に晒すポートの指定
  • ENV <key> <value> 環境変数の設定
  • INSERT <file url> <path> deprecated なので ADD を利用すること
  • ADD <src> <dest> ファイルを配置

RUNCMD の違いがわかりにくいかもしれない。例を出す。

# RUN, CMD で指定したコマンドが実行されたとき、
# 標準出力と /tmp/*.log に記録を残す

$ cat <<SCRIPT >Dockerfile
> FROM base
> RUN /bin/echo run | tee /tmp/run.log
> CMD /bin/echo cmd | tee /tmp/cmd.log
> SCRIPT

# ビルドの実行

$ docker build .
Caching Context 10240/? (n/a)
FROM base ()
===> b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc
RUN /bin/echo run | tee /tmp/run.log (b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc)
===> d10b6bd1321d45b0228b5741c01d1f76fd0288052e56836609f9bdf217854f3d
CMD /bin/echo cmd | tee /tmp/cmd.log (d10b6bd1321d45b0228b5741c01d1f76fd0288052e56836609f9bdf217854f3d)
===> 60671e9969185841032fb02f623917672c4f871a6be68e5aa575e8fdf1f94229
Build successful.
===> 60671e9969185841032fb02f623917672c4f871a6be68e5aa575e8fdf1f94229

# run, cmd の実行結果を確認
# => run だけが実行されている

$ docker run 60671e99691 /bin/ls /tmp/
run.log

# イメージを inspect する
# => どうやらコンテナは記憶していることがわかる

$ docker inspect 60671e99691
{
    "id": "60671e9969185841032fb02f623917672c4f871a6be68e5aa575e8fdf1f94229",
    "parent": "d10b6bd1321d45b0228b5741c01d1f76fd0288052e56836609f9bdf217854f3d",
    "created": "2013-06-16T16:29:14.602237Z",
    "container": "4c54683cec90500f329dfaad2e0856cc408483be0ae3166018121d4d4b9b3282",
    "container_config": {
        "Hostname": "78c72f8ba6ad",
        "User": "",
        "Memory": 0,
        "MemorySwap": 0,
        "CpuShares": 0,
        "AttachStdin": false,
        "AttachStdout": false,
        "AttachStderr": false,
        "PortSpecs": null,
        "Tty": false,
        "OpenStdin": false,
        "StdinOnce": false,
        "Env": null,
        "Cmd": [
            "/bin/sh",
            "-c",
            "#(nop) CMD [/bin/sh -c /bin/echo cmd | tee /tmp/cmd.log]"
        ],
        "Dns": null,
        "Image": "d10b6bd1321d45b0228b5741c01d1f76fd0288052e56836609f9bdf217854f3d",
        "Volumes": null,

# 引数でコマンドを指定せずに run を実行
# => cmd で登録した内容が実行される

$ docker run 60671e99691
cmd

つまり、RUNDockerfile を元にビルドしているときに参照され、CMD はコンテナを実行する際に参照されるということがわかる。パッケージをインストールしたりといった用途では通常 RUN を使う。

まとめ

仮想環境の発達でプログラマブルなインフラストラクチャーは実現できてきているけど、マシンを上げたり下げたりするのにどうしても時間がかかるし、それは仕方が無いものと我慢していた。Docker を使ってみると、今までのそういった不満から解放されることができそう。一応開発中というステータスなのでプロダクション環境では使いづらいけど、開発やテスト、とくに構成管理ツールを設定するときなどは、この俊敏性、柔軟性は有効になると思う。

参考