etcd + docker で簡単にリモートコンテナに接続しよう
Docker 盛り上がってますね。
色々使ってみるとこんなこと思うことありませんか?
- NAT じゃなきゃ楽なのに
- 他ホストの Docker と Link できたら楽なのに
そうです。色々使ってみるとネットワーク周りをどうするか?という問題にぶつかります。
導入、運用を考えてる方々は多くの場合、この問題に取り組まないといけなくなると思います。
では問題をおさらいしてみましょう
Docker のネットワークの問題
Docker はポータビリティを上げるため他コンテナに IP, Port 番号などを教える機能を提供しています。
Link 機能です。
$ sudo docker run -d --name redis crosbymichael/redis
$ sudo docker run -t -i --link redis:db ubuntu bash
root@451f4256fbc8:/# env
HOSTNAME=451f4256fbc8
DB_NAME=/cranky_brattain/db
DB_PORT_6379_TCP_PORT=6379
TERM=xterm
DB_PORT=tcp://172.17.0.2:6379
DB_PORT_6379_TCP=tcp://172.17.0.2:6379
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
DB_PORT_6379_TCP_ADDR=172.17.0.2
DB_PORT_6379_TCP_PROTO=tcp
SHLVL=1
HOME=/
_=/usr/bin/env
root@451f4256fbc8:/# apt-get -qq update && apt-get -qy install redis-server
Reading package lists...
Building dependency tree...
The following NEW packages will be installed:
redis-server
0 upgraded, 1 newly installed, 0 to remove and 63 not upgraded.
Need to get 204 kB of archives.
After this operation, 523 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu/ precise/universe redis-server amd64 2:2.2.12-1build1 [204 kB]
Fetched 204 kB in 2s (79.9 kB/s)
Selecting previously unselected package redis-server.
(Reading database ... 9737 files and directories currently installed.)
Unpacking redis-server (from .../redis-server_2%3a2.2.12-1build1_amd64.deb) ...
Processing triggers for ureadahead ...
Setting up redis-server (2:2.2.12-1build1) ...
invoke-rc.d: policy-rc.d denied execution of start.
root@451f4256fbc8:/# redis-cli -h $DB_PORT_6379_TCP_ADDR -p $DB_PORT_6379_TCP_PORT
redis 172.17.0.2:6379> ping
PONG
redis 172.17.0.2:6379>
もちろん Link するコンテナは起動していないといけません。
redis-cli に接続情報を渡して起動していますが、ホスト、ポートなどを直接書いていませんね。
上記のように Link を使うと redis という名前のコンテナのネットワーク情報が環境変数に設定されます。
つまりこの機能を使うと Link コンテナがどのポートで動作しているなど意識しないで済み、コンテナのポータ
ビリティを上げることができます。
もちろん、Link を使いたい場合は Link 機能対応しないといけないケースがほとんどでしょう。
環境変数からアプリケーションへ設定する部分は作成する必要があります。
Link がない場合はどうでしょうか?
使う側のコンテナは IP, Port を直打ち、あるいは設定ファイルに落としこむ必要があります。
その時点で接続情報は固定化され、使い回しが利かなくなり、コンテナのポータビリティが下がってしまいます。
Link の問題
上記を見ても偉えればわかりますが、IP は NAT のアドレスですね。
つまり Link 機能は同一ホスト内でしか機能しないのです。
せっかく Link に対応したコンテナ WebApp, Database Server, Cache Serverなどにわけても作成しても、これ
らは全て同一ホスト内でしかうまく動作しません。
社内システムなどでは問題ないかも知れませんが、負荷を考えると同一ホストはちょっと…という方も多いので
はないでしょうか?
expose を使って redis の Port を公開してみましょう。
$ sudo docker run -d --name redis-1 -p 6379 crosbymichael/redis
98ef4fda3639d91334ded7794541e82bf976382d601870db541afab37cb9a125
$ sudo docker run -t -i --link redis-1:db ubuntu bash
root@66c433f24d84:/# env
HOSTNAME=66c433f24d84
DB_NAME=/goofy_fermat/db
DB_PORT_6379_TCP_PORT=6379
TERM=xterm
DB_PORT=tcp://172.17.0.3:6379
DB_PORT_6379_TCP=tcp://172.17.0.3:6379
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
DB_PORT_6379_TCP_ADDR=172.17.0.3
DB_PORT_6379_TCP_PROTO=tcp
SHLVL=1
HOME=/
_=/usr/bin/env
root@66c433f24d84:/# exit
$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
98ef4fda3639 crosbymichael/redis:latest redis-server --bind 26 seconds ago Up 25seconds 0.0.0.0:49153->6379/tcp goofy_fermat/db,redis-1
Port がぶつからないように公開 Port を自動割り当てにしています。
当たり前ですが Link した側から見ても NAT のアドレスしかわかりませんね。
外部に公開した Port 番号すらもわかりません。
やはり同一ホストでないとどうしようもないようです。
これが Docker の現状です。
分散環境で Docker のポータビリティを維持したまま運用するのは現状厳しいのです。
etcd + docker で解決する
これらの問題を一番簡単に解決する方法は SDN を構築する方法だと思います。
が現状 SDN を構築するのは敷居が高く、設定がそれなりに大変でまだまだ難しいのではないかと思います。
仕方ないので CoreOS などクラスタ環境で動かすために以下を作りました。
簡単に言えば etcd で docker の情報を共有する仕組みです。
etcd がないと動かないので注意して下さい。
使い方は通常の docker コマンドと同じです。
(デーモンは実装する意味がないので未実装)
core@coreos2 ~ $ ./etcdocker
Usage: docker [OPTIONS] COMMAND [arg...]
-H=[unix:///var/run/docker.sock]: tcp://host:port to bind/connect to or unix://path/to/socket to use
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
cp Copy files/folders from the containers filesystem to the host path
diff Inspect changes on a container's filesystem
events Get real time events from the server
export Stream the contents of a container as a tar archive
history Show the history of an image
images List images
import Create a new filesystem image from the contents of a tarball
info Display system-wide information
inspect Return low-level information on a container
kill Kill a running container
load Load an image from a tar archive
login Register or Login to the docker registry server
logs Fetch the logs of a container
port Lookup the public-facing port which is NAT-ed to PRIVATE_PORT
ps List containers
pull Pull an image or a repository from the docker registry server
push Push an image or a repository to the docker registry server
restart Restart a running container
rm Remove one or more containers
rmi Remove one or more images
run Run a command in a new container
save Save an image to a tar archive
search Search for an image in the docker index
start Start a stopped container
stop Stop a running container
tag Tag an image into a repository
top Lookup the running processes of a container
version Show the docker version information
wait Block until a container stops, then print its exit code
run コマンドのみ拡張されています。
通信する先のetcd endpoint と自身の ip をセットするオプションが追加されています。
Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
Run a command in a new container
-a, --attach=[]: Attach to stdin, stdout or stderr.
-c, --cpu-shares=0: CPU shares (relative weight)
--cidfile="": Write the container ID to the file
-d, --detach=false: Detached mode: Run container in the background, print new container id
--dns=[]: Set custom dns servers
--dns-search=[]: Set custom dns search domains
-e, --env=[]: Set environment variables
--endpoint="": etcd endpoint. default 127.0.0.1:4001
--entrypoint="": Overwrite the default entrypoint of the image
--env-file=[]: Read in a line delimited file of ENV variables
--expose=[]: Expose a port from the container without publishing it to your host
-h, --hostname="": Container host name
-i, --interactive=false: Keep stdin open even if not attached
--link=[]: Add link to another container (name:alias)
-m, --memory="": Memory limit (format: <number><optional unit>, where unit = b, k, m or g)
-n, --networking=true: Enable networking for this container
--name="": Assign a name to the container
-o, --opt=[]: Add custom driver options
-P, --publish-all=false: Publish all exposed ports to the host interfaces
-p, --publish=[]: Publish a container's port to the host (format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort) (use 'docker port' to see the actual mapping)
--peer="": Container host ipaddr
--privileged=false: Give extended privileges to this container
--rm=false: Automatically remove the container when it exits (incompatible with -d)
--sig-proxy=true: Proxify all received signal to the process (even in non-tty mode)
-t, --tty=false: Allocate a pseudo-tty
-u, --user="": Username or UID
-v, --volume=[]: Bind mount a volume (e.g. from the host: -v /host:/container, from docker: -v /container)
--volumes-from=[]: Mount volumes from the specified container(s)
-w, --workdir="": Working directory inside the container
Demo
せっかくなので CoreOS 上でデモをします。
CoreOS は 2台構成です。
core@coreos1 ~ $ fleetctl list-machines
MACHINE IP METADATA
deae7626... 192.168.2.51 -
90942ff3... 192.168.2.50 -
core@coreos1 ~ $ ifconfig ens3
ens3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.2.50 netmask 255.255.255.0 broadcast 192.168.2.255
inet6 fe80::5054:ff:fee2:ae39 prefixlen 64 scopeid 0x20<link>
ether 52:54:00:e2:ae:39 txqueuelen 1000 (Ethernet)
RX packets 19138347 bytes 3673953778 (3.4 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 13046974 bytes 2368351812 (2.2 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
core@coreos2 ~ $ ifconfig ens3
ens3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.2.51 netmask 255.255.255.0 broadcast 192.168.2.255
inet6 fe80::5054:ff:feef:184d prefixlen 64 scopeid 0x20<link>
ether 52:54:00:ef:18:4d txqueuelen 1000 (Ethernet)
RX packets 12924281 bytes 2356382747 (2.1 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 18293237 bytes 3109865402 (2.8 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
では片方で redis server を動かします。
etcdocker は docker client と使い方は同じです。
(そのまま流用してるので)
core@coreos1 ~ $ ./etcdocker run -d --name redis -p 6379 crosbymichael/redis
36ba7c3b7524843ef52c61f0877c1c469a7e057ee96127f0d5762731ad2e95b7
core@coreos1 ~ $ ./etcdocker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
36ba7c3b7524 crosbymichael/redis:latest redis-server --bind 2 seconds ago Up 1 seconds 0.0.0.0:49153->6379/tcp redis
Port を自動割り当てにしたので 192.168.2.50:49153 で redis server に接続できるはずです。
ではもう一台から etcdocker 経由で Link してみましょう。
core@coreos2 ~ $ ./etcdocker run -t -i --link redis:db ubuntu bash
root@d21ae643bba1:/# env
HOSTNAME=d21ae643bba1
DB_PORT_6379_TCP_PORT=49153
TERM=xterm
DB_PORT=tcp://192.168.2.50:49153
DB_PORT_6379_TCP=tcp://192.168.2.50:49153
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PWD=/
DB_PORT_6379_TCP_ADDR=192.168.2.50
DB_PORT_6379_TCP_PROTO=tcp
SHLVL=1
HOME=/
_=/usr/bin/env
root@d21ae643bba1:/# apt-get -qq update && apt-get -qy install redis-server
Reading package lists...
Building dependency tree...
The following NEW packages will be installed:
redis-server
0 upgraded, 1 newly installed, 0 to remove and 63 not upgraded.
Need to get 204 kB of archives.
After this operation, 523 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu/ precise/universe redis-server amd64 2:2.2.12-1build1 [204 kB]
Fetched 204 kB in 1s (106 kB/s)
Selecting previously unselected package redis-server.
(Reading database ... 9737 files and directories currently installed.)
Unpacking redis-server (from .../redis-server_2%3a2.2.12-1build1_amd64.deb) ...
Processing triggers for ureadahead ...
Setting up redis-server (2:2.2.12-1build1) ...
invoke-rc.d: policy-rc.d denied execution of start.
root@d21ae643bba1:/# redis-cli -h $DB_PORT_6379_TCP_ADDR -p $DB_PORT_6379_TCP_PORT
redis 192.168.2.50:49153> ping
PONG
redis 192.168.2.50:49153>
なんということでしょう!
env で確認すると、正しくもう一台の IP, Port が返ってきてるではないですか!
実際に ping も通っています。
これでリモートでも Link 機能を使用し、ポータビリティをあげることができますね。
どうやって実現しているのでしょうか?
ドキュメントを真面目に読んでいれば簡単に予想がつきますね。
Docker Run コマンドを再考
再度、リファレンスに目を通してみましょう。
http://docs.docker.io/en/latest/reference/run/
Overriding Dockerfile Image Defaults とあり更に下に以下の情報がありますね。
http://docs.docker.io/en/latest/reference/run/#env-environment-variables
そうです。Link の機能は環境変数で実現されているので、上書きしてやればよいのです。
etcdockerは以下のように動作しています。
Run with NAME
etcdocker は起動する際に --name があるか確認します。
(現状では name が明示的に定義されてるときのみ対応しています)
Link を使う場合には name を指定するので有無を確認します。
流れ:
- コマンドラインを解析し、nameを特定
- nameがあれば docker run した後、container id を取得する
- container id から docker inspect し、network 情報を取得する
- name をキーに etcd にPOST
制限としては上記のように name がキーになるのでクラスタ内で name は一意になるようにしてください。
Link
同様に --link も監視しています。
link する name の情報があるか etcd に確認します。
流れ:
- コマンドラインを解析し、linkを特定
- link 数分 etcd に問い合わせる
- etcd に情報があれば コマンドラインから -e でネットワーク情報を上書きする
以下のコマンド場合
$ docker run --link redis:db -t -i ubuntu bash]
実際には以下のように展開実行されます。
$ docker run -e DB_PORT=tcp://192.168.2.50:49153 -e DB_PORT_6379_TCP=tcp://192.168.2.50:49153 -e DB_PORT_6379_TCP_ADDR=192.168.2.50 -e DB_PORT_6379_TCP_PORT=49153 -e DB_PORT_6379_TCP_PROTO=tcp -t -i ubuntu bash
ホストが別の場合は --link は削除されます。
(動作確認が入って起動できないので)
同一ホストの場合はコマンドは展開されず通常の Link で起動します。
これで外部との接続も Link の機能でうまく動作できますね。
最後に
これらのあらびきな方法は現状の仕様だから実現できています。
が現状でも応用を効かせればいろいろな事ができますという例でした。
今度機会があれば同様な仕組みでダイナミック proxy を作成し、リモートコンテナを切り替える話をしたいと思います。