CoreOS環境でtraefikを使ってコンテナのサービスディスカバリを行う方法


はじめに

この記事はDocker2 Advent Calendar 2016の12日目の記事です。

このエントリではtraefikというGo製のリバースプロキシ兼ロードバランサをCoreOSクラスタ上にdockerコンテナとして展開し、サービスディスカバリを行う方法について書きます。

なお、検証で利用するCoreOSのクラスタ環境については、以前書いた、CoreOSのクラスタ環境(CoreOS+etcd+fleet)でdocker-composeを使う方法で作った環境で進めますが、適当なCoreOSのクラスタ環境であれば同じ方法で試せるかと思います。

使用ソフトウェアとバージョン

CoreOS:stable 1185.5.0
docker:1.11.2
traefik:1.1.1

Traefikについて簡単な紹介

Traefikとは様々なバックエンド(docker、swarm、kubernetes、mesos、consul、zookeeperなど)の状態を元に設定を動的に変更することができる、ロードバランサ兼リバースプロキシです。Goで書かれており、他のGo製ツールと同じく、バイナリをひとつ置くだけで実行できます。また、綺麗なGUIも備えています。

意訳ですが、公式サイトには概ねこのような文言が書かれています。
イメージ的にはバックエンドの状態に応じて動的に振り分け設定を変更できる、NginxやHAProxyといった感じです。

ちなみにTraefikだけサクッと試してみたいという方は、公式がチュートリアルを用意しているので、そちらで大体の感触を掴むことができます。
https://www.katacoda.com/courses/traefik/deploy-load-balancer

確認の流れ

では、実際にTraefikを展開して、動作を確認していきます。以下のような流れで進めます。

  1. CoreOSクラスタ上にTraefikコンテナを展開
  2. 振り分け先のアプリケーションとしてnginxの公式イメージをクラスタに展開
  3. nginxのサービスをtraefikに登録してアクセスできるか確認
  4. nginxをスケールアウトさせ、ロードバランスの挙動を確認

CoreOSクラスタ上にTraefikコンテナを展開

まずは、Traefikの公式DockerイメージをCoreOSのクラスタに展開します。
以下のようなユニットファイルを使って、fleetctlで展開します。

[Unit]
Description=sample traefik-etcd service
After=docker.service
Requires=docker.service

[Service]
EnvironmentFile=/etc/environment
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill traefik
ExecStartPre=-/usr/bin/docker rm traefik
ExecStartPre=/usr/bin/docker pull traefik
ExecStart=/usr/bin/docker run --rm --name traefik -p 80:80 -p 8080:8080 \
  traefik --web --etcd --etcd.endpoint=${COREOS_PRIVATE_IPV4}:2379
ExecStop=/usr/bin/docker stop traefik

[X-Fleet]
Conflicts=traefik@*.service
service展開
core@ip-172-31-30-102 ~ $ fleetctl start [email protected]
Unit [email protected] inactive
Unit [email protected] launched on 59541285.../172.31.30.102
起動確認
core@ip-172-31-30-102 ~ $ fleetctl list-units | grep traefik
[email protected]   59541285.../172.31.30.102   active  running

無事、配置が完了しました。
ユニットファイルにかかれているdockerコマンドについてですが、それぞれ下記のような意味を持ちます。

  • dockerのオプション
    • ポート80:リバースプロキシとして使用するポート
    • ポート8080:GUIで使用するポート
  • taraefikのオプション
    • --web:GUIを有効化するオプション
    • --etcd:Traefikの設定のバックエンドとしてetcdを利用するオプション
    • --etcd.endpoint:利用するetcdの接続先を指定するオプションで、今回はホストのCoreOSのetcdを使用

--etcd.endpointで環境変数COREOS_PRIVATE_IPV4を利用していますが、これは/etc/environmentに定義されている環境変数で、ホストのIPを渡しています。

traefikを展開したホストの8080番ポートをブラウザで覗くとtraefikのGUIに繋がります。
(CoreOS公式のCloud formationでクラスタを作成した場合、SGのポートを開けて下さい)
まだ、振り分け設定を何も入れていないので、空になっています。

振り分け先のアプリケーションとしてnginxの公式イメージをクラスタに展開

振り分け先を作るため、nginxの公式イメージをクラスタに展開します。
わかりやすさのため、ホストOSの/etc/hostnameをマウントさせて表示させる事にします。また、外に晒すポートは8081にします。
以下のようなユニットファイルを使って、fleetctlで展開します。

[Unit]
Description=sample nginx service
After=docker.service
Requires=docker.service

[Service]
TimeoutStartSec=0
ExecStartPre=-/usr/bin/docker kill nginx
ExecStartPre=-/usr/bin/docker rm nginx
ExecStartPre=/usr/bin/docker pull nginx
ExecStart=/usr/bin/docker run --rm -p 8081:80 --name nginx \
  -v /etc/hostname:/usr/share/nginx/html/index.html:ro nginx
ExecStop=/usr/bin/docker stop nginx

[X-Fleet]
Conflicts=nginx@*.service
service展開
core@ip-172-31-30-102 ~ $ fleetctl start [email protected] 
Unit [email protected] inactive
Unit [email protected] launched on 5ac56d10.../172.31.13.231
起動確認
core@ip-172-31-30-102 ~ $ fleetctl list-units | grep nginx
[email protected]     5ac56d10.../172.31.13.231   activating  start-pre

展開できました。一応、curlでちゃんと表示されるか、確認します。
(CoreOS公式のCloud formationでクラスタを作成した場合、SGのポートを開けて下さい)

curl確認
core@ip-172-31-30-102 ~ $ curl 172.31.13.231:8081
ip-172-31-13-231.ap-northeast-1.compute.internal

nginxのサービスをtraefikに登録

展開したnginxをtraefikに登録してnginx.example.comのドメインでアクセスできるようにします。
--etcdオプションを付けてtraefikを起動すると、etcdに格納された値を元に、動的に設定変更できるようになります。
まずは、以下のコマンドにより手動でetcdに値を入れてみます。

backend登録
core@ip-172-31-30-102 ~ $ etcdctl set /traefik/backends/backend1/servers/server1/url http://172.31.13.231:8081
172.31.13.231:8081
core@ip-172-31-30-102 ~ $ etcdctl set /traefik/backends/backend1/servers/server1/weight 1
1

etcdctlを使って、etcdに値を入れることで、振り分け先グループbackend1の振り分け対象その1(server1)のurlを172.31.13.231:8081にweightを1に設定しています。
ちなみにtraefikではサーバの振り分け先グループのことをbackendと呼びます。。。が、etcdやconsulなどの設定のロード先のこともbackendと呼んており、少しややこしいです。この記事では前者はbackend、後者はバックエンドと記載しています。

設定後、traefikの画面を確認すると、backend1が追加されているのがわかります。

backendの設定が完了したので、次はnginx.example.comへのアクセスをbackend1に結びつけるfrontendの設定を入れます。frontendとは受け取ったリクエストをbackendに振り分けるための一連のルールのセットになります。
以下のコマンドにより手動でetcdに値を入れてみます。

frontend登録
core@ip-172-31-30-102 ~ $ etcdctl set /traefik/frontends/frontend1/backend backend1
backend1
core@ip-172-31-30-102 ~ $ etcdctl set /traefik/frontends/frontend1/routes/test_1/rule Host:nginx.example.com
Host:nginx.example.com

上記はfrontend1という名前で振り分けルールを作成しており、振り分け先にbackend1を設定して、振り分け条件としてHost:nginx.example.comを設定しています。これはリクエストがtraefikに飛んできた際に、リクエスト先のサーバネームがnginx.example.comだった場合、backend1に振り分けを行うような設定となります。

設定後、traefikの画面を確認すると、frontend1が追加されているのがわかります。

以上で、traefikを介して、nginxのアプリケーションにアクセスできるようになりました。
curlで接続確認をしてみます。

curl確認
core@ip-172-31-30-102 ~ $ fleetctl list-units | grep traefik
[email protected]   59541285.../172.31.30.102   active  running
core@ip-172-31-30-102 ~ $ curl -H Host:nginx.example.com http://172.31.30.102
ip-172-31-13-231.ap-northeast-1.compute.internal

traefikを介してnginxにアクセスできています。

ここまで、etcdの登録をすべて手作業で行いましたが、これをfleetを使って登録するようにします。
一旦、etcdの設定はすべて消します

etcd値削除
core@ip-172-31-30-102 ~ $ etcdctl rm /traefik/frontends/frontend1/backend
PrevNode.Value: backend1
core@ip-172-31-30-102 ~ $ etcdctl rm /traefik/frontends/frontend1/routes/test_1/rule
PrevNode.Value: Host:nginx.example.com
core@ip-172-31-30-102 ~ $ etcdctl rm /traefik/backends/backend1/servers/server1/url
PrevNode.Value: http://172.31.13.231:8081
core@ip-172-31-30-102 ~ $ etcdctl rm /traefik/backends/backend1/servers/server1/weight
PrevNode.Value: 1

backend登録用のユニットファイルは以下のようになります。

[Unit]
Description=Register nginx in backend
BindsTo=nginx@%i.service
After=nginx@%i.service

[Service]
EnvironmentFile=/etc/environment
ExecStart=/bin/sh -c "while true; do \
  etcdctl set /traefik/backends/backend1/servers/server%i/url http://${COREOS_PRIVATE_IPV4}:8081 --ttl 60;\
  etcdctl set /traefik/backends/backend1/servers/server%i/weight 1 --ttl 60;\
  sleep 45;done"
ExecStop=/bin/sh -c "\
  etcdctl rm /traefik/backends/backend1/servers/server%i/url ;\
  etcdctl rm /traefik/backends/backend1/servers/server%i/weight"

[X-Fleet]
MachineOf=nginx@%i.service

UnitセクションにあるBindsToはこのユニットが依存するUnitを明記しています。%iにはインスタンス番号が入る変数となっており、このUnitはnginx@%i.serviceが無いと自動で死亡するようになっています。
serviceセクションでは、多少強引ですが、whileループによってetcdの設定を更新し続けるようになっています。etcdctlの--ttlオプションを設定することで、不意にnginxのサービスが落ちた時にetcdからも設定が消えるようになっています。X-FleetセクションのMachineOfはこのユニットが起動するホストの条件を指定しています。nginx@%i.serviceに依存するユニットなので同じホスト上で稼働させる必要があります。

core@ip-172-31-30-102 ~ $ fleetctl start [email protected]
Unit [email protected] launched on 5ac56d10.../172.31.13.231

frontend登録用ユニットファイルは以下のようになります。

register-nginx_example_com.service
[Unit]
Description=Register nginx.example.com in frontend

[Service]
ExecStart=/bin/sh -c "while true; do \
  etcdctl set /traefik/frontends/frontend1/backend backend1 --ttl 60;\
  etcdctl set /traefik/frontends/frontend1/routes/test_1/rule Host:nginx.example.com --ttl 60;\
  sleep 45;done"
ExecStop=/bin/sh -c "\
  etcdctl rm /traefik/frontends/frontend1/backend backend1 ;\
  etcdctl rm /traefik/frontends/frontend1/routes/test_1/rule"

大体、backendのユニットと同じです。ただし、こちらは何かのサービスに依存する作りにはなっていません。

register-nginx_example_com.service展開
core@ip-172-31-30-102 ~ $ fleetctl start register-nginx_example_com.service 
Unit register-nginx_example_com.service inactive
Unit register-nginx_example_com.service launched on 89391638.../172.31.30.100

上記の2ユニットを展開すると手動でetcdを設定した時と同じ状態になります。

ユニットファイル化したのでスケールさせてみます。
[email protected]を起動してnginxをスケールしてみます。

nginxスケール
core@ip-172-31-30-102 ~ $ fleetctl start [email protected]           
Unit [email protected] inactive
Unit [email protected] launched on 9184f486.../172.31.30.101
core@ip-172-31-30-102 ~ $ fleetctl start [email protected]          
Unit [email protected] inactive
Unit [email protected] launched on 9184f486.../172.31.30.101

無事、2台のnginxが登録されています。

traefikにアクセスするとロードバランスされていることが分かります。

curl確認
core@ip-172-31-30-102 ~ $ curl -H Host:nginx.example.com http://172.31.30.102
ip-172-31-13-231.ap-northeast-1.compute.internal
core@ip-172-31-30-102 ~ $ curl -H Host:nginx.example.com http://172.31.30.102
ip-172-31-30-101.ap-northeast-1.compute.internal

おわりに

以上でTraefikを使用して動的にnginxのサービスにリクエストを振り分けることができました。
後はnginx.example.comを名前解決できるように、内部DNSにエントリを追加してあげれば、サービス追加時に自由なURLを介してサービスを見ることができるようになります。が、長くなるのでその辺りは割愛します。fleetのBindsToとMachineOfを使って、traefikのユニットに依存するDNS登録ユニットを追加すればできるようになるかと。
ちなみに、traefikは複数個立ち上げれば簡単にスケールさせることができます。設定もetcdから読んでくれるので立ち上げるだけで済みます。
検証しきれていないですが、traefikにはSSLの終端、パスによるリクエスト振り分け、振り分け条件に正規表現を使用、振り分け先のヘルスチェック、HTTP2.0対応などリバプロ、ロードバランサに必要な機能は一通り揃ってそうですので、興味が出た方は試してみては如何でしょうか??