仮想環境構築に docker を使う
ちょっと前から Docker を使っているので、その話。
Dockr について
Docker は dotcloud がオープンソースで公開している、コンテナ技術による仮想化ソフトウェア。
以下のテクノロジーベースにしている:
- 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>
ファイルを配置
RUN
と CMD
の違いがわかりにくいかもしれない。例を出す。
# 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
つまり、RUN
は Dockerfile
を元にビルドしているときに参照され、CMD
はコンテナを実行する際に参照されるということがわかる。パッケージをインストールしたりといった用途では通常 RUN
を使う。
まとめ
仮想環境の発達でプログラマブルなインフラストラクチャーは実現できてきているけど、マシンを上げたり下げたりするのにどうしても時間がかかるし、それは仕方が無いものと我慢していた。Docker
を使ってみると、今までのそういった不満から解放されることができそう。一応開発中というステータスなのでプロダクション環境では使いづらいけど、開発やテスト、とくに構成管理ツールを設定するときなどは、この俊敏性、柔軟性は有効になると思う。