読者です 読者をやめる 読者になる 読者になる

Consul の情報を Chef / Ohai から使う ohai-plugin-consul を作ったのとその周辺の話

先日とあるサービスに Consul を入れました。

内部 DNS と、たとえば nginx からアプリケーションサーバに振り分ける定義をするために service を使用しています。

そこで使うために、ohai-plugin-consul を書きました。Github にあります。

fujiwara/ohai-plugin-consul · GitHub

Ohai の version 6 と 7 で plugin の interface が変わっており、ohai-plugin-consul は Ohai 7 向けなので、Chefから使う場合は Chef-11.12.0 以上、または 11.10.4.ohai7.0 が必要です。
【参考】 Ohai, new Ohai plugins! - O'Reilly Radar

使用方法

ohai コマンドから使う場合は -d で plugin (consul.rb) を配置したディレクトリを指定して実行すると、最上位の consul というキーに API を叩いた情報が入ってきます。

ohai -d /path/to/plugin_dir | jq .consul
{
  "agent": {
    "checks": { ... },      #= /v1/agent/checks
    "members": [ ... ],     #= /v1/agent/members
    "services": [ ... ]     #= /v1/agent/services
  },
  "catalog": {
    "datacenters": [ ... ], #= /v1/catalog/datacenters
    "nodes": [ ... ],       #= /v1/catalog/nodes
    "services": [ ... ],    #= /v1/catalog/services
    "node": {
      "FOO": { },           #= /v1/catalog/node/FOO
      ...
    },
    "service": {
      "BAR": { },           #= /v1/catalog/service/BAR
      ...
    }
  }
  "status": {
    "leader": "...",        #= /v1/status/leader
    "peers": [ ... ],       #= /v1/status/peers
  }
}

Chefから使用する場合は、(client|solo).rb に plugin_path を定義してください。

Ohai::Config[:plugin_path] << '/path/to/plugins'

node[:consul] で上記と同様の情報が取得できます。

開発経緯

たとえば以下のように、app というサービスを定義して、その node に nginx からリクエストを振り分けたいとします。

$ curl localhost:8500/v1/catalog/services | jq .
{
  "app": [
    "pc",
    "mobile"
  ]
}
$ curl localhost:8500/v1/catalog/service/app | jq .
[
  {
    "ServicePort": 0,
    "ServiceTags": [
      "pc",
      "mobile"
    ],
    "ServiceName": "app",
    "ServiceID": "app",
    "Address": "192.168.1.11",
    "Node": "app001"
  },
  {
    "ServicePort": 0,
    "ServiceTags": [
      "pc",
      "mobile"
    ],
    "ServiceName": "app",
    "ServiceID": "app",
    "Address": "192.168.1.12",
    "Node": "app002"
  }
]

最初は DNS interface を使って、以下のように nginx から app.service.consul の名前解決をして振り分けようとしました。

# nginx.conf
location / {
  set $app "app.service.consul";
  proxy_pass http://$app:5000;
}

が、以下の事情により DNS による振り分けは断念。

  • Consul (v0.2.1) では DNS (UDP) でのアクセスでは、サービスに node が何台いても 3アドレスをランダムに返す
  • Consul が TTL 0 で応答を返すが、nginx は1秒間は名前解決結果を cache する
  • そのため、任意の1秒間では特定の 3 node にしか振り分けられない
  • 4 node 以上ある場合は1秒ごとに全くアクセスが行かない node ができてしまう

ということで、Chef でテンプレートから生成している nginx.conf に Consul API から取得した service を渡す形にしました。

# nginx.conf.erb
upstream pc_backend {
<% node[:consul][:catalog][:service][:app].select{|n| n[:ServiceTags].include?("pc") }.each do |n| %>
   server <%= n[:Address] %>:5000;  # <%= n[:Node] %>
<% end %>
}

このテンプレートを上記の service 定義で展開すると以下のようになります。

# nginx.conf
upstream pc_backend {
   server 192.168.1.11:5000;  # app001
   server 192.168.1.12:5000;  # app002
}

まだやってないこと

nginx.conf をファイルとして静的に展開するので、service の状態に変化 (nodeの増減など) があった場合にはそれを検知して設定ファイルを再生成、再読込する必要があります。

Consul には blocking query という仕組みがあり、状態の変化を long polling する HTTP API で検知することができます。

mizzyさんの consul-catalog というライブラリを使用すると、以下の Gist のようなコードで service の変更を検知して chef-client を実行、という形が取れるかと思います。

https://gist.github.com/fujiwara/4cdff1d718ecaa2b8294

【参考】【Consul】ブロッキング・クエリ(blokcing query)とは | Pocketstudio.jp log3

また、Consul の service にはヘルスチェック機構がありますが、他に Zabbix でやっている監視とうまく共用できないか構想中のためまだ入れていません。(backendに接続できなければ nginx が切り離すので、今はとりあえずそれで…)

zabbix-getコマンドのGo版を書いた ので、うまいこと組み合わせられないかと構想中です。