こわくないConsul
事前に、ワークショップ用のサンプルプロジェクトをチェックアウトし、VMの作成だけしておきましょう。 --no-provision
でお願いします!
git clone https://github.com/udzura/consul-workshop.git && cd consul-workshop
vagrant up --no-provision
また、Consulのインストールは簡単です。Goで書かれているため、バイナリひとつで動きます... が、サービスを立ち上げて管理するためのinitスクリプト、Unitファイルなどは標準で添付されていません。
udzura/hashibuild
という、CentOS7向けのrpm(systemd対応)を作るためのプロジェクトを別途用意したので、そのビルドも事前にやっていただけると良いかと思います。
brew install docker-compose
./build-rpms.sh
今回の構成は、以下のようになります。
192.168.100.0/24
+--------+ +--------+
| front | -----------------> | back01 | .111
+--------+ | +--------+
Nginx | +--------+
.101 |----------> | back02 | .112
| +--------+
| +--------+
`----------> | back03 | .113
+--------+
Rack apps
まず、各サーバに事前に必要になる予定のパッケージを入れてしまいましょう。一緒に、iptablesは止めます。
sudo iptables -F
sudo yum -y check-update
sudo yum -y install epel-release
sudo yum -y install jq nagios-plugins-all
sudo yum -y install /vagrant/rpms/consul-0.5.2-1.el7.centos.x86_64.rpm
# ショートカット
vagrant provision --provision-with step0
consul-0.5.2
がこれで入りますので、つづいて、 front
のみ必要なconsul web uiとconsul-templateを入れておきます。
sudo yum -y install /vagrant/rpms/consul-ui-0.5.2-1.el7.centos.x86_64.rpm
sudo yum -y install /vagrant/rpms/consul-template-0.10.0-1.el7.centos.x86_64.rpm
最後に、consulの設定ファイル(JSON)を作成して、consulをリスタートしましょう。
echo '{"server": true, "bind_addr": "192.168.100.101", "client_addr": "0.0.0.0", "bootstrap_expect": 3}' | sudo tee /etc/consul/default.json
sudo systemctl restart consul
設定の各項目は、以下のような意味です。
server
- サーバーモードでの起動。基本的にはサーバーモードで。非サーバーモードとの違いは後述bind_addr
- Consulを、どのIPにひも付けて立ち上げるかの指定client_addr
- 各種APIについて、どのアドレスをリスンするかを別途指定できるbootstrap_expected
- Consulクラスタの起動のために、何台のクラスタメンバーを必要とするかの指定。詳細後述
ここまでで、無事、consulデーモンは立ち上がっていると思います。 ですが、ログを見るとエラーが目立ちます。
2015/10/08 08:42:01 [INFO] serf: EventMemberJoin: front.workshop.example.dc1 192.168.100.101
2015/10/08 08:42:01 [INFO] raft: Node at 192.168.100.101:8300 [Follower] entering Follower state
2015/10/08 08:42:01 [INFO] consul: adding server front.workshop.example (Addr: 192.168.100.101:8300) (DC: dc1)
2015/10/08 08:42:01 [INFO] consul: adding server front.workshop.example.dc1 (Addr: 192.168.100.101:8300) (DC: dc1)
2015/10/08 08:42:01 [ERR] agent: failed to sync remote state: No cluster leader
2015/10/08 08:42:03 [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election.
2015/10/08 08:42:20 [ERR] agent: failed to sync remote state: No cluster leader
2015/10/08 08:42:39 [ERR] agent: failed to sync remote state: No cluster leader
2015/10/08 08:43:01 [ERR] agent: failed to sync remote state: No cluster leader
2015/10/08 08:43:25 [ERR] agent: failed to sync remote state: No cluster leader
現在の状態では、クラスタにリーダーがいないという状態になります。
Consulがクラスタとして機能するには、
- リーダーが必要です。
- リーダーは、
bootstrap_expected
で指定した数のメンバーがジョインした段階で、初めて選出されます。
# ここまでのショートカット
vagrant provision front --provision-with step1
では、メンバーを増やしてクラスタをワークさせてみましょう。まずは1台、 back01
だけクラスタにジョインさせます。
echo '{"server": true, "bind_addr": "192.168.100.111"}' | sudo tee /etc/consul/default.json
sudo systemctl restart consul
# 少し待って
consul join 192.168.100.101
# ショートカット
vagrant provision back01 --provision-with step2
ログを見ると、ジョインした胸のログ、それでもまだ投票が始まらないという内容のログが出ています。
2015/10/08 08:51:15 [INFO] raft: Node at 192.168.100.111:8300 [Follower] entering Follower state
2015/10/08 08:51:15 [INFO] consul: adding server back01.workshop.example (Addr: 192.168.100.111:8300) (DC: dc1)
2015/10/08 08:51:15 [INFO] consul: adding server back01.workshop.example.dc1 (Addr: 192.168.100.111:8300) (DC: dc1)
2015/10/08 08:51:15 [ERR] agent: failed to sync remote state: No cluster leader
2015/10/08 08:51:16 [INFO] agent.rpc: Accepted client: 127.0.0.1:45026
2015/10/08 08:51:16 [INFO] agent: (LAN) joining: [192.168.100.101]
2015/10/08 08:51:16 [INFO] serf: EventMemberJoin: front.workshop.example 192.168.100.101
2015/10/08 08:51:16 [INFO] agent: (LAN) joined: 1 Err: <nil>
2015/10/08 08:51:16 [INFO] consul: adding server front.workshop.example (Addr: 192.168.100.101:8300) (DC: dc1)
2015/10/08 08:51:17 [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election.
続いて3台目をジョインさせます。
vagrant provision back02 --provision-with step2
これでようやくリーダーが決まります。
2015/10/08 08:52:49 [INFO] consul: adding server front.workshop.example (Addr: 192.168.100.101:8300) (DC: dc1)
2015/10/08 08:52:49 [INFO] consul: adding server back01.workshop.example (Addr: 192.168.100.111:8300) (DC: dc1)
2015/10/08 08:52:49 [WARN] raft: EnableSingleNode disabled, and no known peers. Aborting election.
2015/10/08 08:52:50 [INFO] consul: New leader elected: front.workshop.example
同じように4台目も起動、ジョインさせておきます。
vagrant provision back03 --provision-with step2
リーダーが決まったら、どこでもお好きなサーバに入って、APIを叩いてみましょう。
[vagrant@front ~]$ curl -s localhost:8500/v1/catalog/nodes | jq .
[
{
"Node": "back01.workshop.example",
"Address": "192.168.100.111"
},
{
"Node": "back02.workshop.example",
"Address": "192.168.100.112"
},
{
"Node": "front.workshop.example",
"Address": "192.168.100.101"
}
]
# DNS interface も試せます
[vagrant@front ~]$ dig @127.0.0.1 -p 8600 front.workshop.example.node.consul
; <<>> DiG 9.9.4-RedHat-9.9.4-18.el7_1.5 <<>> @127.0.0.1 -p 8600 front.workshop.example.node.consul
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 34111
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available
;; QUESTION SECTION:
;front.workshop.example.node.consul. IN A
;; ANSWER SECTION:
front.workshop.example.node.consul. 0 IN A 192.168.100.101
;; Query time: 3 msec
;; SERVER: 127.0.0.1#8600(127.0.0.1)
;; WHEN: 木 10月 08 08:55:26 UTC 2015
;; MSG SIZE rcvd: 102
(DNS Interfaceについては、 http://pocketstudio.jp/log3/2014/05/01/consul_with_dnsmasq_name_resolution/ などの記事で利用法の解説がされています)
ここで、リーダーとなったサーバのConsulを落としてみるとどうなるでしょう。
[vagrant@front ~]$ sudo systemctl stop consul
他のサーバが再度リーダーに選出されるかと思います。
2015/10/08 08:59:33 [WARN] raft: Heartbeat timeout reached, starting election
2015/10/08 08:59:33 [INFO] raft: Node at 192.168.100.101:8300 [Candidate] entering Candidate state
2015/10/08 08:59:34 [WARN] raft: Election timeout reached, restarting election
2015/10/08 08:59:34 [INFO] raft: Node at 192.168.100.101:8300 [Candidate] entering Candidate state
2015/10/08 08:59:36 [WARN] raft: Election timeout reached, restarting election
2015/10/08 08:59:36 [INFO] raft: Node at 192.168.100.101:8300 [Candidate] entering Candidate state
2015/10/08 08:59:38 [WARN] raft: Election timeout reached, restarting election
2015/10/08 08:59:38 [INFO] raft: Node at 192.168.100.101:8300 [Candidate] entering Candidate state
2015/10/08 08:59:39 [WARN] raft: Election timeout reached, restarting election
2015/10/08 08:59:39 [INFO] raft: Node at 192.168.100.101:8300 [Candidate] entering Candidate state
2015/10/08 08:59:40 [INFO] raft: Node at 192.168.100.101:8300 [Follower] entering Follower state
2015/10/08 08:59:40 [WARN] raft: Failed to get previous log: 20 log not found (last: 16)
2015/10/08 08:59:40 [INFO] consul: New leader elected: back01.workshop.example
2015/10/08 08:59:42 [INFO] agent: Synced service 'consul'
ところで、非サーバーモードのノードは、リーダーに選出されることはありません。むやみにリーダー不在期間を作らないために、頻繁に上げ下ろしをするようなロールのサーバーは、非サーバーモードでConsulを起動すると問題が起きにくいでしょう。
Consulは、ただクラスタメンバーを管理するだけでなく、そのサーバにひも付くサービスの登録と、ヘルスチェックを行うことができます。
frontサーバにNginxをインストールし、サービス登録してみましょう。
sudo yum -y install nginx
cat <<JSON | sudo tee /etc/consul/step3-check-nginx.json
{
"service": {
"id": "nginx",
"name": "nginx",
"port": 80,
"check": {
"script": "/usr/lib64/nagios/plugins/check_http -H localhost",
"interval": "30s"
}
}
}
JSON
sudo systemctl reload consul
# ショートカット
vagrant provision front --provision-with step3
JSONファイルのそれぞれの項目の詳細は、想像がつくでしょうのでここではお話ししません(公式ドキュメントがあります)が、
一点だけ、 check
という属性で、「ヘルスチェックにどういうコマンドを使うか」と「何秒ごとにチェックするか」を指定できます。
このヘルスチェックのコマンドの終了コードは、 0
が正常、 1
が不明、 2
以上が警告というルールです(Nagiosのプラグインと互換性があります。なので、よくNagiosのプラグインをそのまま使っています)。
さて、ここで、 http://localhost:8500/
を開いてみてください。 Consul の web UI にアクセスできると思います。先ほど加えた「nginx」というサービスが画面からも確認できることを見てください。
Nginxを落としたり、立ち上げたりして、このチェックがどうなるかを確かめてみましょう。
sudo systemctl start nginx
# もしくは
vagrant provision front --provision-with step4
サンプルプロジェクトを、もう少し現実的な構成にしてみます。
back01 ~ back03 に、Rubyの非常に簡単なアプリケーションを構築し、チェックも追加しましょう。まず、Rubyをインストールし、Rackのアプリを作成します。
sudo yum -y install rubygem-rack
cat <<RUBY | sudo tee /usr/local/app.ru
require "socket"
run lambda{ |e|
[200, {'Content-Type'=>'text/plain'},
["OK: response from " + Socket.gethostname]]
}
RUBY
rackup -p 3000 /usr/local/app.ru
というコマンドで、Rubyのアプリが立ち上がることを確認してください(Vagrantの設定により、例えば localhost:3001
からback01の3000番に立ち上がったアプリを確認可能です)。
続いて、このアプリを管理するUnitファイルを作ります。
cat <<UNIT | sudo tee /etc/systemd/system/ruby-app.service
[Unit]
Description=Ruby rack app
After=network.target
Requires=network.target
[Service]
Type=simple
ExecStart=/usr/bin/rackup -p 3000 /usr/local/app.ru
[Install]
WantedBy=multi-user.target
UNIT
このアプリをsystemdに登録し、立ち上げましょう。
systemctl enable ruby-app.service
systemctl start ruby-app.service
仕上げとして、このアプリケーションもConsulのサービスとして登録します。今回は、3000番ポートをチェックするだけの簡単な監視にします。
cat <<JSON | sudo tee /etc/consul/step5-check-application.json
{
"service": {
"id": "application",
"name": "application",
"port": 3000,
"check": {
"script": "/usr/lib64/nagios/plugins/check_http -H localhost -p 3000",
"interval": "30s"
}
}
}
JSON
sudo systemctl reload consul
ここまでできたら、 localhost:8500
にて、Rackアプリも無事サービス登録されたことを確認しましょう。
以下は、ショートカットコマンドです。
vagrant provision back01 back02 back03 --provision-with step5
さて、ここからは、フロントのNginxを設定します。
まず、デフォルトのNginxのページが邪魔になるので、リスンするポートを変えて無効にしてしまいましょう。
sudo ruby -i -e 'print ARGF.read.sub(/listen +80/, "listen 10080")' /etc/nginx/nginx.conf
ここから、consul-templateの設定となります。
consul-templateは、Consulと連携できるツールの一つで、クラスタのサービスの状態変更を検知して、動的に設定を書き換えたり、コマンドを発行するためのデーモンです。
今回は、バックエンドのRackアプリケーションの状態により、自動的にプロクシ設定を変更する、動的ロードバランサーを作ってみます。
以下の要領でconsul-templateのためのテンプレートファイルを作ります。
cat <<TEMPLATE | sudo tee /usr/local/sample.conf.ctmpl
upstream backend_apps {
{{range service "production.application@dc1" "passing"}}
server {{.Address}}:{{.Port}};{{end}}
}
server {
listen 80;
server_name _;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Server \$host;
location / {
proxy_pass http://backend_apps;
}
}
TEMPLATE
このテンプレートでは、以下の部分が重要で、
upstream backend_apps {
{{range service "production.application@dc1" "passing"}}
server {{.Address}}:{{.Port}};{{end}}
}
最終的には以下のような設定に展開されます。
upstream backend_apps {
server 192.168.100.111:80;
server 192.168.100.113:80;
}
range service "production.application@dc1" "passing"
というテンプレートの組み込み関数で、「production
」というタグのついた、application
という、dc1
データセンターにある(ここではデフォルトのデータセンターです)サービスから、 passing
つまりチェックが正常であるものを抽出し、その情報をもとにテンプレートを展開する、という意味になります。
HTTP API でいうと以下のコマンドと等価になるでしょう。
curl 'localhost:8500/v1/health/service/application?tag=production&passing'
つづいて、consul-template自体の設定を作って読み直しておきます。
cat <<HCL | sudo tee /etc/consul-template/consul-template.hcl
consul = "127.0.0.1:8500"
retry = "10s"
max_stale = "10m"
log_level = "info"
pid_file = "/var/run/consul-template.pid"
template {
source = "/usr/local/sample.conf.ctmpl"
destination = "/etc/nginx/conf.d/sample.conf"
command = "systemctl reload nginx"
}
HCL
sudo systemctl restart consul-template
この段階では、バックエンドのアプリケーションは0件です。なぜなら、さっき作ったRackアプリケーションのチェックに、タグをつけていないからです。
タグをつけるために、back01 ~ back03それぞれで以下のコマンドを発行し、設定JSONを変更してください。追加されている箇所は、 "tags"
属性です。
cat <<JSON | sudo tee /etc/consul/step5-check-application.json
{
"service": {
"id": "application",
"name": "application",
"port": 3000,
"tags": ["production"],
"check": {
"script": "/usr/lib64/nagios/plugins/check_http -H localhost -p 3000",
"interval": "30s"
}
}
}
JSON
sudo systemctl reload consul
これで、 localhost:8080
にアクセスすると、確かにバックエンドのRackアプリにアクセスでき、かつ各ホストに均等にアクセスを分配していることがわかるかと思います。
ここまでのショートカットです。
vagrant provision --provision-with step6
back01 ~ back03 のどれでも良いのですが、以下のように、Rackアプリケーションを異常終了させて落としてみましょう。
sudo kill -9 $(pgrep rackup)
その状態でも、 localhost:8080
にアクセスしてエラーになることはないはずです。ですが、確かに落としたバックエンドにはたどり着かないようになっています(設定への反映に数秒のラグがあることはあります)。
この時、frontを見てみると、確かにバックエンドのアプリケーションから外れていることがわかります。
[vagrant@front ~]$ sudo cat /etc/nginx/conf.d/sample.conf
upstream backend_apps {
server 192.168.100.112:3000;
server 192.168.100.113:3000;
}
また、落としたアプリケーションを sudo systemctl start ruby-app
を実行し立ち上げなおすと、元どおりバックエンドに、それも自動で入ってくれます(Nginxのログを見れば、リロードもしてくれてるとわかります)。
このように、consul-templateを用いることで、Consulのヘルスチェックや、メンバー追加、削除と、設定ファイルとを、ダイナミックに連携させることができます。
consul-templateはHashicorpが公式に提供していることもあり、Consulによるオーケストレーションの中では最も一般的に使われるミドルウェアだと思います。
ここから、少し発展したトピックとして、 consul watch
サブコマンドの使い方を見てみましょう。
consul watch
は、ざっくり説明すると
- Consulのクラスタの様々な変更を監視するデーモンを作成できる
- 監視できる内容は、ヘルスチェックの変更、ノードの追加削除、KVSの変更、手動で発行するイベントなど
- デーモンは、何かしらの変更があった場合、任意のコマンドの標準入力に対し、変更のサマリをJSONで渡すことができる
まず、「手動で発行するイベント」で試してみましょう。
どこのノードでもいいのですがログインして、以下のコマンドを発行してください。
consul watch -type event -name "hello" /bin/jq
出力として、以下のように空っぽの配列が表示され、ターミナルにとどまると思います。
[]
別のターミナルを開いて、今度は別のノードに入り、以下のコマンドを叩きます。
consul event -name "hello"
# Event ID: f0002e01-4a0f-315d-20f6-2de05a34ae18
するとwatch
コマンドを発行したターミナルで、イベントを受領したことがわかります。
[
{
"ID": "f0002e01-4a0f-315d-20f6-2de05a34ae18",
"Name": "hello",
"Payload": null,
"NodeFilter": "",
"ServiceFilter": "",
"TagFilter": "",
"Version": 1,
"LTime": 5
}
]
Payload
としてなんらかの値を送りつけたい場合はコマンドの後ろに追加するだけです。JSONでは、base64変換されて届きます。
consul event -name "hello" 'I am consul!!'
# Another terminal...
[
{
"ID": "4d5d9808-8396-10c9-57e6-d0aa2d0f3a27",
"Name": "hello",
"Payload": "SSBhbSBjb25zdWwhIQ==",
"NodeFilter": "",
"ServiceFilter": "",
"TagFilter": "",
"Version": 1,
"LTime": 11
}
]
# ruby -e 'p "SSBhbSBjb25zdWwhIQ==".unpack("m")' => ["I am consul!!"]
逆に、違う名前でイベントを発行しても、watchのターミナルは反応しないことを確認してください。
consul event -name "not-hello"
ここでもう一つ興味深いこととして、 consul watch -type event -name "hello" /bin/jq
のコマンドをまた別のノードで立ち上げてみると、 consul event -name "hello"
のキックの結果は 全てのノードに 届いていく、ということがわかります。consulのイベントは、どのノードで待ち受けることもできますし、どのノードからキックしても同じような結果になります。
ノードを限定してイベントを届けることも可能です。以下のようにします。
consul event -name "hello" -node 'back.*'
[
{
"ID": "e5a6a031-f18d-9d71-cf22-b8eb5f49b3eb",
"Name": "hello",
"Payload": null,
"NodeFilter": "back.*",
"ServiceFilter": "",
"TagFilter": "",
"Version": 1,
"LTime": 13
}
]
もう一つの例を見てみましょう。
ConsulのKVSの状態を監視することができます。
最初にKVSについての説明をします。Consulは、全てのクラスタノードで状態を統一させていますが、その機能の一環として、全台で共有するKVS機能を持っています。
ノードAで以下のように値をプッシュすると、
curl -X PUT -d 'World' localhost:8500/v1/kv/hello
ノードBで以下のように値を取り出せます。クラスタのノードであれば、どこからでもプッシュでき、どこからでも取り出せるようになっています。
curl -s localhost:8500/v1/kv/hello | jq .
[
{
"CreateIndex": 61,
"ModifyIndex": 61,
"LockIndex": 0,
"Key": "hello",
"Flags": 0,
"Value": "V29ybGQ="
}
]
curl -s localhost:8500/v1/kv/hello | jq -r .[0].Value | base64 -d
# または curl -s localhost:8500/v1/kv/hello?raw
World
KVSの中身は、以下のようにWeb UIでも確認可能です
このKVSは設定の共有などに大変便利ですが、一つの利用方法として、イベントのフックにもできます。以下のように、KVSの pepabo/*
のキーの様子をウォッチして、特定のプログラムに渡すことができます。
consul watch -type keyprefix -prefix "pepabo/" /bin/jq
以下のようなコマンドを別のノードから叩いてみて、watchのターミナルがどういう出力を返すか、調べてみましょう。
curl -X PUT -d 'sample' localhost:8500/v1/kv/pepabo/hi
curl -X PUT -d 'sample' localhost:8500/v1/kv/pepabo/foo/bar
curl -X PUT -d 'sample' localhost:8500/v1/kv/not-pepabo/hi
curl -X DELETE localhost:8500/v1/kv/pepabo/hi