etcd + docker で簡単にリモートコンテナに接続しよう

# etcd + docker で簡単にリモートコンテナに接続しよう

Docker 盛り上がってますね。
色々使ってみるとこんなこと思うことありませんか？

- NAT じゃなきゃ楽なのに
- 他ホストの Docker と Link できたら楽なのに

そうです。色々使ってみるとネットワーク周りをどうするか？という問題にぶつかります。
導入、運用を考えてる方々は多くの場合、この問題に取り組まないといけなくなると思います。
では問題をおさらいしてみましょう

## Docker のネットワークの問題

Docker はポータビリティを上げるため他コンテナに IP, Port 番号などを教える機能を提供しています。
Link 機能です。

```bash
$ 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 を公開してみましょう。

```bash
$ 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 などクラスタ環境で動かすために以下を作りました。

https://github.com/mopemope/etcdocker

簡単に言えば etcd で docker の情報を共有する仕組みです。
etcd がないと動かないので注意して下さい。
使い方は通常の docker コマンドと同じです。
（デーモンは実装する意味がないので未実装）

```bash
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台構成です。

```bash
core@coreos1 ~ $ fleetctl list-machines
MACHINE         IP              METADATA
deae7626...     192.168.2.51    -
90942ff3...     192.168.2.50    -
```

```bash:マシンA
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
```

```bash:マシンB
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 と使い方は同じです。
（そのまま流用してるので）

```bash:マシンA
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 してみましょう。

```bash:マシンB
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 を指定するので有無を確認します。

流れ:

1. コマンドラインを解析し、nameを特定
2. nameがあれば docker run した後、container id を取得する
3. container id から docker inspect し、network 情報を取得する
4. name をキーに etcd にPOST

制限としては上記のように name がキーになるのでクラスタ内で name は一意になるようにしてください。

### Link

同様に --link も監視しています。
link する name の情報があるか etcd に確認します。

流れ:
1. コマンドラインを解析し、linkを特定
2. link 数分 etcd に問い合わせる
3. 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 を作成し、リモートコンテナを切り替える話をしたいと思います。