MicroServices、コンテナオーケストレーションなどが花盛りですが、そこに本格的に踏み出すにはまだ若干の時間が必要なように思います。 とはいえ、サーバの動的な構成変更への対応はいくらでも出てくるので、サービスディスカバリと、サーバのステータスをトリガにしたアクションには興味がありました。
そういうわけで、まずは Consul by HashiCorp の機能を試してみたいと思います。
まずは Consul とは何か
Consul は、以下の機能を持ったツールです。
- サービスディスカバリ
- ヘルスチェック
- K/V ストア
「ツール」と書いたように、実装として取り込む「ライブラリ」ではなく、独立して動作する「バイナリ」として提供されます。
アーキテクチャ
アーキテクチャとしてはクライアント・サーバ構成になっていて、クライアント、サーバともに複数台から構成することで可用性を上げる考えになっています。 クライアント、サーバをまとめて、Consul Agent と呼ばれます。
基本的には、データセンタ(ここでの「データセンタ」は、一般的な意味な DC ではなく、低遅延、高帯域のネットワークで繋がれた環境として定義されます) 内でクライアントとサーバがクラスタとして動作し、 このデータセンタ内に閉じて、メンバシップ情報やサービスステータスといった情報共有を行います。
一方、データセンタ間は個々のデータサーバ間の互いのサーバ同士が、互いの情報の共有を行うという多層構造になっています。
公式のガイドでいうと、以下の図が分かりやすいかと思います。
上記で LAN/WAN GOSSIP と書かれているところは SWIM をベースとした通信を、サーバ間あるいはクライアント間で行われる GOSSIP については、それぞれ以下のエントリを参照ください。
個々の Consul Agent は、HTTP API と DNS Interface を持っていて、
- ノードの情報
- サービスの情報
なんかが、これらのインタフェースを利用することで参照できます。 DNS Interface を持っているっていうことは、アプリケーションからはホスト名をちょっと変えるだけで、わりと transparent に Service Discovery を利用できるってことですね。
クラスタを構成する
そこそこの台数の consul agent でクラスタを形成したかったので、Docker Composeを使うことにしました。
Dockerfile
consul に関しては、公式が alpine ベースのイメージを提供してくれています。
いくつかコマンドを使いたかったので、以下のような Dockerfile を用意しました。
上記の consul の image をベースにして、apache2 をインストールしています。
httpd
とコマンドを叩けば apache が起動するようになっています。
FROM consul:1.1.0 ENV HTTPD_DIR /usr/local/apache2 ENV HTTPD_PID_DIR /run/apache2 # We install apache2 with some utility commands, but it is to be run manually. RUN set -ex \ && apk update \ && apk add --no-cache --virtual .deps \ curl \ bind-tools \ apache2 \ && adduser -D -S -G www-data www-data \ && mkdir -p "$HTTPD_DIR" "$HTTPD_PID_DIR" \ && chown www-data:www-data "$HTTPD_DIR" "$HTTPD_PID_DIR"
docker-compose.yaml
docker-compose.yaml には、サーバとクライアントをそれぞれサービスとして用意します。
これにより、サーバ、クライアントとも、docker-compose
の scale
オプションで任意の台数を指定できるようになります。
version: '3' services: server: &agent build: context: . dockerfile: Dockerfile image: kiririmode/consul:latest command: "agent -server -ui -bootstrap-expect ${SERVER_NUM} -retry-join consul_server_1 -client 0.0.0.0 -recursor=8.8.8.8" ports: - "8500-8549:8500" - "10000-10049:80" client: <<: *agent command: "agent -ui -retry-join consul_server_1 -client 0.0.0.0 -recursor=8.8.8.8" ports: - "8550-8599:8500" - "10050-10099:80"
クラスタをあげる
サーバ台数は、Raft を使用している関係と、パフォーマンスの見地から、3 台か 5 台が推奨されています。 ここではまず、1 つのデータセンタ内にサーバ 3 台、クライアント 5 台からなるクラスタを構築してみます。
$ export SERVER_NUM=3 $ docker-compose -p consul up --scale server=3 --scale client=5
まずはメンバーを確認します。 以下のように、意図したとおり、サーバ 3 台、クライアント 5 台のクラスタが構成されていることが分かります。
$ docker-compose -p consul exec --index=1 server consul members Node Address Status Type Build Protocol DC Segment 37087184553e 172.18.0.4:8301 alive server 1.1.0 2 dc1 <all> 6a56ea9fa974 172.18.0.3:8301 alive server 1.1.0 2 dc1 <all> df2dc341f480 172.18.0.2:8301 alive server 1.1.0 2 dc1 <all> 4804c51b4d9f 172.18.0.9:8301 alive client 1.1.0 2 dc1 <default> 712f685e59ca 172.18.0.6:8301 alive client 1.1.0 2 dc1 <default> 9c182e4347bc 172.18.0.8:8301 alive client 1.1.0 2 dc1 <default> d455dbe41191 172.18.0.5:8301 alive client 1.1.0 2 dc1 <default> fbd8cbc2cea2 172.18.0.7:8301 alive client 1.1.0 2 dc1 <default>
Raft のステータスはどうでしょうか。Raft のステータスを確認するためには、consul operator raft <sub-command>
を使います。
以下のように、Node 0a35
がリーダとして選出されていることがわかります。
$ docker-compose -p consul exec --index=1 server consul operator raft list-peers Node ID Address State Voter RaftProtocol 6a56ea9fa974 0a35ddd5-f851-e1dc-804e-1aad15a165b8 172.18.0.3:8300 leader true 3 df2dc341f480 bc14f2e3-988f-5177-973d-78ab02ada1ed 172.18.0.2:8300 follower true 3 37087184553e e7636e01-6fa2-0610-a5d9-8b7da9a107b2 172.18.0.4:8300 follower true 3
では、DNS Interface、HTTP API それぞれでメンバの状態を見てみましょう。
DNS Interface
consul は DNS サーバがバイナリに埋め込まれており、デフォルトでは 8600 番ポートで起動します。
名前空間は consul
で終わるようになっています。
例えば、consul サーバを名前解決する場合は、consul.service.consul
に問合せを行います。
結果としては、以下のように 3 台の server が名前解決できています。
$ docker-compose -p consul exec --index=1 server \ dig +noall +answer @127.0.0.1 -p 8600 consul.service.consul consul.service.consul. 0 IN A 172.18.0.3 consul.service.consul. 0 IN A 172.18.0.2 consul.service.consul. 0 IN A 172.18.0.4
HTTP API
HTTP サーバについてもバイナリに埋め込まれており、参照系・更新系といった種々の機能が利用できます (そういう意味で、DNS Interface より高機能です)。
以下は、メンバの情報を参照する API ですが、このように 8 台の情報が返ってくることが分かります。
$ docker-compose -p consul exec --index=1 server \ curl http://127.0.0.1:8500/v1/catalog/nodes \ | jq -r '.[].Address' 172.18.0.4 172.18.0.9 172.18.0.3 172.18.0.6 172.18.0.8 172.18.0.5 172.18.0.2 172.18.0.7
ノードの死に対する耐性
consul において、ノードのステータスは通常状態の他に、left
と failed
があります。
行儀よくクラスタを離脱した場合は left
、いきなりの故障が発生した場合は failed
に遷移します。
ここでは、3 つ目のサーバに SIGKILL を発行してみます。
$ docker kill -s KILL consul_server_3
consul_server_3
こうすると、クライアント 5 はもはや周囲のノードからの ping に応答しなくなり、SWIM の中で
failed
と判定されます。実際、consul members
の結果は以下のとおりで、1 台のクライアントが failed
の
ステータスになっていることがわかります。
$ docker-compose -p consul exec --index=1 server \ consul members Node Address Status Type Build Protocol DC Segment 37087184553e 172.18.0.4:8301 alive server 1.1.0 2 dc1 <all> 6a56ea9fa974 172.18.0.3:8301 alive server 1.1.0 2 dc1 <all> df2dc341f480 172.18.0.2:8301 failed server 1.1.0 2 dc1 <all> 4804c51b4d9f 172.18.0.9:8301 alive client 1.1.0 2 dc1 <default> 712f685e59ca 172.18.0.6:8301 alive client 1.1.0 2 dc1 <default> 9c182e4347bc 172.18.0.8:8301 alive client 1.1.0 2 dc1 <default> d455dbe41191 172.18.0.5:8301 alive client 1.1.0 2 dc1 <default> fbd8cbc2cea2 172.18.0.7:8301 alive client 1.1.0 2 dc1 <default>
DNS Interface で問い合わせてみても、1 台減っていることが確認できます。
$ docker-compose -p consul exec --index=1 server \ dig +noall +answer @127.0.0.1 -p 8600 consul.service.consul consul.service.consul. 0 IN A 172.18.0.3 consul.service.consul. 0 IN A 172.18.0.4
Service Discovery
なんといっても、consul のユースケースの 1 つは Service Discovery です。
上記の consul.service.consul
への DNS クエリもその 1 つではあったのですが、ここでは動的に Service の設定を追加してみます。
Docker ホストに、以下のような web.json
を用意します。
これは、"apache" というサービスに関する設定として定義していて、10 秒毎に http://localhost
に GET でヘルスチェックを実施します。
{ "Name": "apache", "Port": 80, "Check": { "Name": "http on port 80", "HTTP": "http://localhost", "Method": "GET", "Interval": "10s", "Timeout": "1s" } }
これを任意の consul エージェントに HTTP で PUT します。ここでは、1 番目のクライアントに PUT してみましょう。
# 1 番目のクライアントの 8500 番ポートにマッピングされているポート $ docker-compose -p consul port --index=1 client 8500 0.0.0.0:8571 # Docker ホストからサービス設定を投入 $ curl -XPUT -d@web.json http://localhost:8571/v1/agent/service/register
この段階では、1 番目のクライアントで httpd が立ち上がっていないので、"apache" というサービスをディスカバリしようとしても 存在しません。
# 1 番目のサーバ上で DNS での Service Discovery $ docker-compose -p consul exec --index=1 server \ dig +noall +answer @127.0.0.1 -p 8600 apache.service.consul # 何も返却されません
では、httpd を立ち上げてみた後の結果が以下になります。 このとおり、きちんと apache サービスが Discovery できることがわかります。
# 1 番目のクライアント上で httpd を立ち上げる $ docker-compose -p consul exec --index=1 client httpd # 1 番目のサーバ上で httpd の Service Discovery $ docker-compose -p consul exec --index=1 server \ dig +noall +answer @127.0.0.1 -p 8600 apache.service.consul apache.service.consul. 0 IN A 172.18.0.8
もちろん、この設定を他のノードにも投入すれば、Service Discovery の結果の数が増えていきます。
今回は http でヘルスチェックを行いましたが、他にも TCP や、任意のスクリプトを用いてのヘルスチェックも実現できます。
K/V Store
consul は Key Value Store の機能も持っています。 ここでは、3 台目のクライアントで Key-Value を投入し、それを 2 台目のサーバで確認してみます。
まずは投入。key は kiririmode
、value は dislikes onion
としてます。
$ curl -XPUT -d "dislikes onion" http://localhost:$(docker-compose -p consul port --index=3 client 8500 | cut -d: -f2)/v1/kv/kiririmode true
これをサーバで確認してみます。
$ curl http://localhost:$(docker-compose -p consul port --index=2 server 8500 | cut -d: -f2)/v1/kv/kiririmode\?pretty [ { "LockIndex": 0, "Key": "kiririmode", "Flags": 0, "Value": "ZGlzbGlrZXMgb25pb24=", "CreateIndex": 153, "ModifyIndex": 153 } ]
このように、value は base64 でエンコードされています。デコードすると、たしかに別サーバで同じ値が取得できることがわかります。
$ curl -s http://localhost:$(docker-compose -p consul port --index=2 server 8500 | cut -d: -f2)/v1/kv/kiririmode \ | jq -r '.[0].Value' \ | base64 -d dislikes onion
最後に
今回は ローカルホスト上に複数個の Docker Container として浮かべる形でクラスタを構成しました。 これは単にその方が手軽に検証できるからです。
consul を Container から利用する場合、
- Docker Host に対して 1 つの Container とする
- また、bridge ではなくて Host Networking (
--net host
) を使用する
というのが本来なのでご注意ください。
詳細は、consul をご参照ください。