CoreOSクラスタ内のDockerコンテナの動的リンク

Dynamic Docker links with an ambassador powered by etcd

上記の記事を参考にCoreOSのクラスタ内で複数ホスト間にまたがりDockerコンテナを連携させる方法について検証した.

背景と問題

複数ホストにまたがりDockerのコンテナを接続する方法としてはAmbassador パターンが有名である.これはトラフィックを別ホストへforwardすることに特化したコンテナを立てる方法で,ホストに無駄な設定なし,かつDockerコンテナのみで行えるシンプルな方法である.例えば,あるホストからredis-cliを使って,別ホストで動くredisに接続する場合は以下のように接続する.

(redis-cli) --> (ambassador) ---network---> (ambassador) --> (redis)

redis-cliコンテナとambassadorコンテナ,redisコンテナとambassadorコンテナはdockerのlink機能で接続し,ambassadorコンテナはトラフィックをネットワーク越しにフォワードする.

この方法は,接続側がその相手先のホストを知っている必要がある.例えば上記の場合,redis-cliコンテナ側のambassadorコンテナは以下のように相手先のホストのIP(e.g., 192.168.1.52)を指定して起動しなければならない.

$ docker run -d --name ambassador --expose 6379 -e REDIS_PORT_6379_TCP=tcp://192.168.1.52:6379 svendowideit/ambassador

ホストが固定されている場合は問題ないが,CoreOSのように動的にホストが変わる可能性がある場合は問題になる.接続先のホスト情報を直接既述すると,ホストが変わる度に設定を更新する必要があり,かなり億劫な感じになる.

CoreOSにおける1つの解法

CoreOSはクラスタの形成に分散Key-Valueストアであるetcdを使っている.このetcdを使うと動的なambassadorパターンを作り上げることができる.つまり,以下のようなことをする.

  • 接続される側は接続情報をetcdに書き込み続けるコンテナを立てる
  • 接続する側はその情報を読み込み続ける動的なambassadorコンテナを立てる

あとは,この動的なambassadorコンテナとlink接続すれば,相手先の情報を環境変数として取得するとができる.これで接続する側は接続相手のホスト情報を知らなくてもよくなる.

検証ストーリー

これを実際にCoreOSクラスタを立てて検証してみる.

ここでは,Docker公式のドキュメント“Link via an Ambassador Container”と同様の例を用いる.クラスタ内のあるホストよりredis-cliコンテナを使って,別ホストのredisコンテナに接続するという状況を考える.

CoreOSクラスタを立てる

利用するCoreOSクラスタはtcnksm/vagrant-digitalocean-coreosを使って,VagrantでDigitalOcean上に立てる.

$ export NUM_INSTANCES=3
$ vagrant up --provider=digital_ocean

これで,DigitalOcean上に3つのCoreOSインスタンスが立ち上がる.

利用するコンテナ

全部で5つのコンテナを用いる.

https://coreos.com/assets/images/media/etcd-ambassador-hosts.png

https://coreos.com/assets/images/media/etcd-ambassador-hosts.png

HostA(redisを動かすホスト)では以下のコンテナを立てる.

  • crosbymichael/redis - Ambassador パターンで使われているものと同様のRedisコンテナ
  • polvi/simple-amb - socatコマンドを使って,特定のポートへのトラフィックを与えられたホストにforwardするだけのコンテナ.etcdへのフォーワードに利用する.
  • polvi/docker-register - docker portコマンドを使って与えられたDockerコンテナのIPとPortを取得し,etcdにそれを登録するコンテナ

HostB(redis-cliを動かすホスト)では以下のコンテナを立てる.

  • polvi/simple-amb - HostAと同様のコンテナ.etcdへのフォーワードに利用する.
  • polvi/dynamic-etcd-amb - etcdからRedisコンテナのホストとIPを取得し,環境変数にそれを設定するコンテナ
  • relateiq/redis-cli - Ambassador パターンで使われているものと同様のコンテナ

Unitファイル

CoreOSはコンテナの管理とスケジューリングにFleetを用い,その設定はUnitファイルで行う.上述したDockerコンテナを起動するためのUnitファイルについて簡単に説明する.なお,Fleetの詳しい使いかたなどは,“Fleetの使い方,Unitファイルの書き方”に書いた.

HostA

crosbymichael/redisを動かすためのredis.serviceは以下.

[Unit]
Description=Run redis

[Service]
TimeoutStartSec=0
KillMode=none
EnvironmentFile=/etc/environment
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull crosbymichael/redis
ExecStart=/usr/bin/docker run --rm --name %n -p $COREOS_PRIVATE_IPV46379 crosbymichael/redis
ExecStop=/usr/bin/docker stop -t 3 %n

単純にcrosbymichael/redisコンテナを起動するだけ.polvi/docker-registerコンテナを使って,ホストのIPとPortをetcdに登録する必要があるので,-p ${COREOS_PRIVATE_IPV4}::6379を指定して起動する.CoreOSはそのホスト情報を/etc/environmentに保存しているので,それをそのまま使う.どのホストかを直接指定する必要はない.

%nはsystemdのUnitファイルの記法で,そのファイル名が代入される(今回の場合は,redis.service).

polvi/simple-ambを動かすためのetcd-amb-redis.serviceは以下.

[Unit]
Description=Forward all traffic it gets on port 10000 to 172.17.42.1:4001 (redis)

[Service]
TimeoutStartSec=0
KillMode=none
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull polvi/simple-amb
ExecStart=/usr/bin/docker run --rm --name %n polvi/simple-amb 172.17.42.1:4001
ExecStop=/usr/bin/docker stop -t 3 %n

[X-Fleet]
X-ConditionMachineOf=redis.service

このコンテナは特定のポート(10000port)のトラフィックを与えられた引数のホストにforwardする.CoreOSのDockerコンテナからは172.17.42.1:4001でそのetcdにアクセスできる.引数にそれを与えることで10000portへのトラフィックはすべてetcdにforwardされるようになる.

X-ConditionMachineOfredis.serviceを指定することで,このコンテナは,crosbymichael/redisコンテナと同じホストにスケジューリングされるようになる.

polvi/docker-registerregister-redis-etcd.serviceは以下.

[Unit]
Description=Read the IP and port of redis.service from Docker API and publish it to etcd as service name of redis-A
After=redis.service
After=etcd-amb-redis.service
Require=etcd-amb-redis.service

[Service]
TimeoutStartSec=0
KillMode=none
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull polvi/docker-register
ExecStart=/usr/bin/docker run --link etcd-amb-redis.service:etcd -v /var/run/docker.sock:/var/run/docker.sock --rm --name %n polvi/docker-register redis.service 6379 redis-A
ExecStop=/usr/bin/docker stop -t 3 %n

[X-Fleet]
X-ConditionMachineOf=redis.service

このコンテナは与えられたコンテナのホストにおけるIPとPortのマッピング情報を取得し,それをetcdに登録する.今回はcrosbymichael/redisコンテナの6379portのホストにおけるIPとPortのマッピング情報をredis-Aという名前でetcdに登録する.polvi/docker-registerコンテナとlinkで接続することにより,環境変数でetcdの接続先を取得する.

X-ConditionMachineOfredis.serviceを指定することで,このコンテナはcrosbymichael/redisコンテナと同じホストにスケジューリングされるようになる.

HostB

polvi/simple-ambを動かすためのetcd-amb-redis-cli.serviceは以下.

[Unit]
Description=Forward all traffic it gets on port 10000 to 172.17.42.1:4001 (etcd)

[Service]
TimeoutStartSec=0
KillMode=none
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull polvi/simple-amb
ExecStart=/usr/bin/docker run --rm --name %n polvi/simple-amb 172.17.42.1:4001
ExecStop=/usr/bin/docker stop -t 3 %n

[X-Fleet]
X-Conflicts=redis.service

内容はHostAのetcd-amb-redis.serviceと同様だが,スケジューリングの条件が異なる.X-Conflictsredis.serviceを指定することでcrosbymichael/redisコンテナとは異なるホストにスケジューリングされるようになる.

polvi/dynamic-etcd-ambredis-dyn-amb.serviceは以下.

[Unit]
Description=Tells the proxy to expose port 6379 and point it to the service registered as redis-A in etcd.
After=etcd-amb-redis-cli.service
Require=etcd-amb-redis-cli.service
After=register-redis-etcd

[Service]
TimeoutStartSec=0
KillMode=none
ExecStartPre=-/usr/bin/docker kill %n
ExecStartPre=-/usr/bin/docker rm %n
ExecStartPre=/usr/bin/docker pull polvi/dynamic-etcd-amb
ExecStart=/usr/bin/docker run --link etcd-amb-redis-cli.service:etcd --rm --name %n -p 127.0.0.16379 polvi/dynamic-etcd-amb redis-A 6379
ExecStop=/usr/bin/docker stop -t 3 %n

[X-Fleet]
X-ConditionMachineOf=etcd-amb-redis-cli.service

このコンテナはetcdに保存されたホストへのプロキシとして動作する.6379portを解放し,そこへの接続をredis-Aという名前でetcdに登録されたcrosbymichael/redisコンテナが動くIPとPortに向けるようにする.polvi/docker-registerコンテナとlinkで接続することにより,環境変数でetcdの接続先を取得する.

relateiq/redis-cliコンテナは,このコンテナと接続することで,ネットワーク越しにcrosbymichael/redisコンテナに接続する.

接続を試す

上述したUnitファイルで定義したサービスをCoreOSクラスタにデプロイしredisコンテナに接続してみる.

まず,サービスのデプロイする..serviceファイルがあるディレクトリで以下を実行する.

$ fleetctl start *.service

サービスの起動を確認する.Unitファイルで定義したように別々のホストでサービスが起動していることが確認できる.

$ fleetctl list-units
UNIT                            MACHINE                         ACTIVE  SUB
etcd-amb-redis-cli.service      7f0be3a3.../10.132.180.245      active  running
redis-dyn-amb.service           7f0be3a3.../10.132.180.245      active  running
etcd-amb-redis.service          dc324c84.../10.132.181.182      active  running
redis.service                   dc324c84.../10.132.181.182      active  running
register-redis-etcd.service     dc324c84.../10.132.181.182      active  running

redisコンテナに接続するには,etcd-amb-redis-cliコンテナが動いているホストに移動する必要がある.以下で移動できる.

$ fleetctl ssh etcd-amb-redis-cli

実際に接続してみる.

$ docker run -it --link redis-dyn-amb.service:redis relateiq/redis-cli
redis 172.17.0.3:6379> ping
PONG

etcdに保存された情報をみると,redis.serviceが動いているホストの情報が保存されているのが確認できる.

$ etcdctl get /services/redis-A/redis.service
{ "port": 49155, "host": "10.132.181.182" }

耐障害性

redisコンテナを再起動してもすぐに設定は更新され再接続できる.これは,redisコンテナが動くホスト情報のetcdへの動的な書き込み,読み込みにより実現できる.

また,redisコンテナが動くホストを,ホストごと殺しても再び接続できる.これは,fleetによるフェイルオーバー(再スケジューリング)とetcdの動的に書き込み・読み込みにより実現できる.

まとめ

CoreOSのクラスタ内で複数ホスト間にまたがりDockerコンテナを連携させる方法について検証した.etcdに接続先のホストを保存することで,コンテナがどのホストで動いているかを意識しないでそれに接続することができた.