Skip to content

Instantly share code, notes, and snippets.

@udzura
Last active November 7, 2021 12:17
Show Gist options
  • Save udzura/b640e961a93d7b27c3b0 to your computer and use it in GitHub Desktop.
Save udzura/b640e961a93d7b27c3b0 to your computer and use it in GitHub Desktop.
やわらか Consul

やわらか Consul

こわくないConsul

Setup

事前に、ワークショップ用のサンプルプロジェクトをチェックアウトし、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

Step1: Consulのインストールと立ち上げ

今回の構成は、以下のようになります。

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

Step2: クラスタの構築

では、メンバーを増やしてクラスタをワークさせてみましょう。まずは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を起動すると問題が起きにくいでしょう。

Step3: サービスと、その監視(check)を登録する

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のプラグインをそのまま使っています)。

Step4: Web UIでサービスの状態を見てみる

さて、ここで、 http://localhost:8500/ を開いてみてください。 Consul の web UI にアクセスできると思います。先ほど加えた「nginx」というサービスが画面からも確認できることを見てください。

web UI

Nginxを落としたり、立ち上げたりして、このチェックがどうなるかを確かめてみましょう。

sudo systemctl start nginx
# もしくは
vagrant provision front --provision-with step4

Step5: Rackのバックエンドアプリをインストールし、そのチェックを追加する

サンプルプロジェクトを、もう少し現実的な構成にしてみます。

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

Step6: consul-template を設定し、Nginxの設定ファイルを動的生成する

さて、ここからは、フロントの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

Step7: バックエンドアプリが落ちたら...

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によるオーケストレーションの中では最も一般的に使われるミドルウェアだと思います。

Step8: consul watch の仕組み

ここから、少し発展したトピックとして、 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は設定の共有などに大変便利ですが、一つの利用方法として、イベントのフックにもできます。以下のように、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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment