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

#はじめに
この記事は[Docker2 Advent Calendar 2016](http://qiita.com/advent-calendar/2016/docker2)の12日目の記事です。

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

なお、検証で利用するCoreOSのクラスタ環境については、以前書いた、[CoreOSのクラスタ環境（CoreOS+etcd+fleet）でdocker-composeを使う方法](http://qiita.com/kanga/items/a7714f73aced9f872427)で作った環境で進めますが、適当な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コンテナを展開
1. 振り分け先のアプリケーションとしてnginxの公式イメージをクラスタに展開
1. nginxのサービスをtraefikに登録してアクセスできるか確認
1. nginxをスケールアウトさせ、ロードバランスの挙動を確認

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

```bash:traefik@.service
[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
```

```shell-session:service展開
core@ip-172-31-30-102 ~ $ fleetctl start traefik@1.service
Unit traefik@1.service inactive
Unit traefik@1.service launched on 59541285.../172.31.30.102
```

```shell-session:起動確認
core@ip-172-31-30-102 ~ $ fleetctl list-units | grep traefik
traefik@1.service	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のポートを開けて下さい）
まだ、振り分け設定を何も入れていないので、空になっています。

![traefik01.png](https://qiita-image-store.s3.amazonaws.com/0/87808/44691252-8b47-0399-2d9e-91d6b2899cab.png)

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

```bash:nginx@.service
[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
```

```shell-session:service展開
core@ip-172-31-30-102 ~ $ fleetctl start nginx@1.service 
Unit nginx@1.service inactive
Unit nginx@1.service launched on 5ac56d10.../172.31.13.231
```

```shell-session:起動確認
core@ip-172-31-30-102 ~ $ fleetctl list-units | grep nginx
nginx@1.service		5ac56d10.../172.31.13.231	activating	start-pre
```

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

```shell-session: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に値を入れてみます。

```shell-session: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``の振り分け対象その１(``server1``)のurlを``172.31.13.231:8081``にweightを``1``に設定しています。
ちなみにtraefikではサーバの振り分け先グループのことを``backend``と呼びます。。。が、etcdやconsulなどの設定のロード先のこともbackendと呼んており、少しややこしいです。この記事では前者は``backend``、後者はバックエンドと記載しています。

設定後、traefikの画面を確認すると、backend1が追加されているのがわかります。
![traefik02.png](https://qiita-image-store.s3.amazonaws.com/0/87808/3c880915-451e-783c-7674-771643c5ab85.png)


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

```shell-session: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が追加されているのがわかります。
![traefik03.png](https://qiita-image-store.s3.amazonaws.com/0/87808/92165ba9-178e-1537-5e8d-c82c6964ed0c.png)

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

```shell-session:curl確認
core@ip-172-31-30-102 ~ $ fleetctl list-units | grep traefik
traefik@1.service	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の設定はすべて消します

```shell-session: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登録用のユニットファイルは以下のようになります。

```bash:register-nginx@.service
[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に依存するユニットなので同じホスト上で稼働させる必要があります。

```shell-session:register-nginx@.service展開
core@ip-172-31-30-102 ~ $ fleetctl start register-nginx@1.service
Unit register-nginx@1.service launched on 5ac56d10.../172.31.13.231
```

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

```bash: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``のユニットと同じです。ただし、こちらは何かのサービスに依存する作りにはなっていません。

```shell-session: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
```

上記の２ユニットを展開すると手動でetcdを設定した時と同じ状態になります。
![traefik04.png](https://qiita-image-store.s3.amazonaws.com/0/87808/1fc58a56-1249-bb93-dd91-11782f960ef8.png)

ユニットファイル化したのでスケールさせてみます。
nginx@2.serviceを起動してnginxをスケールしてみます。

```shell-session:nginxスケール
core@ip-172-31-30-102 ~ $ fleetctl start nginx@2.service           
Unit nginx@2.service inactive
Unit nginx@2.service launched on 9184f486.../172.31.30.101
core@ip-172-31-30-102 ~ $ fleetctl start register-nginx@2.service          
Unit register-nginx@2.service inactive
Unit register-nginx@2.service launched on 9184f486.../172.31.30.101
```

無事、2台のnginxが登録されています。
![traefik05.png](https://qiita-image-store.s3.amazonaws.com/0/87808/87837ae7-c9a1-ade0-01a9-6e171ac7ff48.png)


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

```shell-session: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対応などリバプロ、ロードバランサに必要な機能は一通り揃ってそうですので、興味が出た方は試してみては如何でしょうか？？