log.fstn

技術よりなことをざっくばらんにアウトプットします。

Consul + Capistrano でオーケストレーションさせてみた

はじめに

Serfに続いてHashiCorpからConsulが発表されて、2ヶ月少々経ちました。 公式では

  • Serf: service discovery and orchestration
  • Consul: service discovery and configuration

と言っていますが(http://www.serfdom.io/intro/vs-consul.html)、Consulも使い方によってはオーケストレーションできるかなと思って、試してみました。

ちなみに Serf や Consul の最近の動向については @zembutsu さんの記事がわかりやすいです

そもそもオーケストレーションとは

  • webサーバをproxyから追加したり抜いたり
  • webサーバにデプロイしたり
  • 障害が発生したサーバを撤去したり
  • dbのslaveを追加したり

など、複数のサーバが連携している様な構成に対して、SSHで入って手作業で行っていた作業を自動化することの総称です(多少バズってるので言葉の定義は曖昧ですが)。 日々の運用業務を簡略化でき、障害の一次対応などを自動化することで、アラート対応に追われる日々が改善されるかもしれません。

オーケストレーションの内容

オーケストレーションの動作はおおまかに次の2ステップに分けられると思います。

  • Step1. 対象サーバを監視し、変化を検知する
  • Step2. 検知したサーバに対して何かしらの操作を行う

例は次のとおりです。

  • ハードウェアの異常
    1. ホストダウンなどサービスを提供することが困難な状態を検知する
    2. サービスに影響が出ないよう、サービスアウトを行う(webの場合は、proxyから抜くなど)
  • ハードウェアの負荷
    1. CPU/メモリの使用率、ロードアベレージの監視し、特定のサーバに負荷がかかっていることを検知する
    2. スケールアウトを行う(webの場合は、proxy配下に新たにwebサーバを追加するなど)
  • ソフトウェアの異常
    1. ソフトウェアの死活監視やログ監視によって、サービスの異常動作を検知する
    2. サービスに影響が出ないようサービスアウトを行うなど

またこれらを実現するための既存のツールは次のとおりです。

  • Step1. 監視ツールの類
    • Zabbix
    • Sensu
    • Serf
    • Consul など
  • Step2. 任意のサーバにて任意のコマンドを実行するツールの類

Serfについて

各サーバにAegentを配置し、ゴシッププロトコルを利用してクラスタを形成します。 Serfの主な機能は以下のとおりです

  • 各種イベントを検知して任意のスクリプトを実行する
    • member-join: メンバーが加わったとき
    • member-leave: メンバーが抜けたとき
    • member-failed: メンバーを見失ったとき(疎通確認に失敗たとき)
    • member-update: メンバーの情報が更新されたとき
    • member-reap: メンバーをリストから削除したとき(member-leave またはmember-failed から一定時間経過したとき)
    • user: 任意のユーザイベントが発行されたとき
    • query: 任意のクエリが発行されたとき

Serfの使い方についてはこちらにまとめました -> Dockerで試す、はじめてのSerf

Consulについて

HashiCorpの4つめのプロダクト(Vagrant Packer Serf の次)

監視とコンフィグレーションのためのツールで、serfの仕組み(ゴシッププロトコル)を利用して、クラスタを形成し、クラスタ内の情報を共有しています。

Consulの主な機能は以下のとおりです

  • サービス検出: クラスタ内にどのようなサービス(Proxy Apache MySQL など)が存在するか検出する
  • 障害検知: ホストダウンやロードアベレージ、プロセスの死活監視など、任意のスクリプトで監視して、障害を検知することができます
  • Key/Value Strage: 任意のノードから編集でき、クラスタ内で共有されます
  • 複数データセンター対応: serfはネットワークセグメントが異なると(データセンターが異なると)、クラスタ形成は出来ないが、consulでは対応されています

サーバ情報の取得方法

consulで形成された各サーバやクラスタの情報、サービス監視の状態などは、クラスタ内外から取得するとこができます。 取得する方法は以下の2種類の方法があります

取得できる情報については省略します(ドキュメント丁寧にかかれているのでそちらを見た方がいい)。

Blocking Query

HTTP APIはBlocking Queryの仕組みを持っています。 例えば /v1/health/state/passingGETすると、即座に返答が返ってきます。 しかし /v1/health/state/passing?index=123のようにindexパラメータを付加してGETすると、即座に返答が返らず、クラスタ内に何かしらの変化があった瞬間に返答が返ってきます。これをConsulではBlocking Queryと呼んでいます。

なおindexクラスタの状態が変化すると数値が変わり、最新のindexをパラメータとして付加しないと、Blocing Queryは成立しません。 最新のindexの値は、HTTPのヘッダーのX-Consul-Indexに格納されています。

SerfとConsulの違いについて

Step1における違い

Serfはサーバの死活監視しか行うことが出来ないのに対して、Consulは任意のスクリプトによって柔軟にサーバやサービスを監視して検知することが可能です。 ただし、Serfは任意のイベントを発行することができるため、Serf + 他の監視ツールを組み合わせることによって柔軟な監視が可能になります(監視ツールによってSerfイベントを発行することができれば)。

Step2における違い

Serfはクラスタ内のサーバの異常を検知し、任意のスクリプトを実行します(検知から実行まで自律的に行う)。それに比べてConsulは、クラスタ内のサーバの異常を検知して、クラスタの情報を更新します(検知しても対象のサーバに対して何もしないし出来ない、他のツールへの通知もできません)。 ただし、Blocking Queryの仕組みを利用することで、状態変化を検知して任意のコマンドを実行することはできます(正確には、任意のコマンドを実行するツールを作ることはできます)。

Consul と Capistarno を組み合わせる

概要

Consul と Capistarno をつなぎ合わせるため今回

  • Attender
  • Enforcer

をつくりました(とりあえず動かそうと思ったため実装に不安あり)。

f:id:foostan:20140629021812p:plain

Attender

https://github.com/foostan/attender

Consulの状態変化を、Blocking Queryの仕組みを利用して取得し、他のツールへつなげるためのツールです。 動作内容については下記のデモに記載してあります。

Enforcer

https://github.com/foostan/enforcer

Attenderからの通知を受け取り、Capistranoを実行します。また、Capistranoで設定が必要なサーバ情報は、Consulから取得します。 オーケストレーションの具体的な操作(webをproxyから抜くなど)の実装については、利用者任せです(そもそも環境によってやることが様々なのでツールとして提供できない)。 動作内容については下記のデモに記載してあります。

Consul + Attender + Enforcer のデモ

デモ環境のインストール

git clone git@github.com:foostan/enforcer.git
cd enforcer
bundle install
vagrant up # Consul apache2 ntp がインストールされた server-01 と client-01 が起動する

Cunsulの動作確認

UI

192.168.33.101:8500/ui

f:id:foostan:20140629210657p:plain

API

> curl 192.168.33.101:8500/v1/catalog/node/server-01 -s | jq .
{
  "Services": {
    "ntp": {
      "Port": 0,
      "Tags": [],
      "Service": "ntp",
      "ID": "ntp"
    },
    "consul": {
      "Port": 8300,
      "Tags": null,
      "Service": "consul",
      "ID": "consul"
    },
    "apache2": {
      "Port": 0,
      "Tags": [],
      "Service": "apache2",
      "ID": "apache2"
    }
  },
  "Node": {
    "Address": "192.168.33.101",
    "Node": "server-01"
  }
}

> curl 192.168.33.101:8500/v1/health/state/passing -s | jq .
[
  {
    "ServiceName": "",
    "ServiceID": "",
    "Output": "Agent alive and reachable",
    "Notes": "",
    "Status": "passing",
    "Name": "Serf Health Status",
    "CheckID": "serfHealth",
    "Node": "server-01"
  },
  {
    "ServiceName": "ntp",
    "ServiceID": "ntp",
    "Output": "",
    "Notes": "",
    "Status": "passing",
    "Name": "Service 'ntp' check",
    "CheckID": "service:ntp",
    "Node": "server-01"
  },
  {
    "ServiceName": "",
    "ServiceID": "",
    "Output": "",
    "Notes": "",
    "Status": "passing",
    "Name": "Check load average",
    "CheckID": "loadavg",
    "Node": "server-01"
  },
  {
    "ServiceName": "",
    "ServiceID": "",
    "Output": "Agent alive and reachable",
    "Notes": "",
    "Status": "passing",
    "Name": "Serf Health Status",
    "CheckID": "serfHealth",
    "Node": "client-01"
  },
  {
    "ServiceName": "",
    "ServiceID": "",
    "Output": "",
    "Notes": "",
    "Status": "passing",
    "Name": "Check load average",
    "CheckID": "loadavg",
    "Node": "client-01"
  },
  {
    "ServiceName": "apache2",
    "ServiceID": "apache2",
    "Output": "",
    "Notes": "",
    "Status": "passing",
    "Name": "Service 'apache2' check",
    "CheckID": "service:apache2",
    "Node": "client-01"
  },
  {
    "ServiceName": "ntp",
    "ServiceID": "ntp",
    "Output": "",
    "Notes": "",
    "Status": "passing",
    "Name": "Service 'ntp' check",
    "CheckID": "service:ntp",
    "Node": "client-01"
  },
  {
    "ServiceName": "apache2",
    "ServiceID": "apache2",
    "Output": "",
    "Notes": "",
    "Status": "passing",
    "Name": "Service 'apache2' check",
    "CheckID": "service:apache2",
    "Node": "server-01"
  }
]

DNS

> dig @192.168.33.101 -p 8600 apache2.service.dc1.consul.

; <<>> DiG 9.8.3-P1 <<>> @192.168.33.101 -p 8600 apache2.service.dc1.consul.
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24634
;; flags: qr aa rd; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 0
;; WARNING: recursion requested but not available

;; QUESTION SECTION:
;apache2.service.dc1.consul.    IN  A

;; ANSWER SECTION:
apache2.service.dc1.consul. 0   IN  A   192.168.33.201
apache2.service.dc1.consul. 0   IN  A   192.168.33.101

;; Query time: 1 msec
;; SERVER: 192.168.33.101#8600(192.168.33.101)
;; WHEN: Sat Jun 21 16:30:42 2014
;; MSG SIZE  rcvd: 128

Attenderの動作確認

クラスタ内の状態変化を検知すると、変化内容が標準出力されます

> attender 192.168.33.101
- server-01 service:apache2 # server-01のapache2を停止させた
+ server-01 service:apache2 # server-01のapache2を起動させた
- server-01 service:ntp     # server-01のntpdを停止させた
+ server-01 service:ntp     # server-01のntpdを起動させた

Enforcerの動作確認

attenderコマンドの標準出力をパイプでaxコマンドに渡すと、変化に対応したタスクが実行されます。

> attender 192.168.33.101 | ax
# '- server-01 service:apache2' を受け取ると
# fail:service:apache2 タスクが実行されます(第一引数として 'server-01' が渡される)。
** Invoke test (first_time)
** Execute test
** Invoke load:defaults (first_time)
** Execute load:defaults
** Invoke fail:service:apache2 (first_time)
** Execute fail:service:apache2
Failing check service:apache2 of server-01

# '+ server-01 service:apache2' を受け取ると
# pass:service:apache2 タスクが実行されます(第一引数として 'server-01' が渡される)。
** Invoke test
** Invoke pass:service:apache2 (first_time)
** Execute pass:service:apache2
Passing check service:apache2 of server-01

上記の例で実行されるタスクはこちら https://github.com/foostan/enforcer/blob/master/lib/enforcer/capistrano/tasks/apache2.rake

ところで

envconsulというものがあって、これを利用するとConsulのKVSの変更のタイミングでイベントを起こすことができるようです。

それじゃ、KVSではなくConsulの各変化に応じてイベントを起こせないのか?と疑問に思ったところ @zembutsu さんより有力な情報を頂きました(ありがとうございます!)。

Consulのロードマップはこちら https://groups.google.com/d/msg/consul-tool/J3Oiysk3ggQ/kxh2gFL0ksIJ

予定だともうじき 0.4 がリリースされるはず(ここ数日コミットがないようだけど…)なので、期待して待っています。

さいごに

今回、Consul + Capistranoオーケストレーションさせてみようと思い、これらをつなぎ合わせるために Attender と Enforcer を作りました。 デモでは具体的な操作は行っていませんが、capistranoのtaskを作成すれば、割りと何でもできる気がします。

また、Consul v0.4 がリリースされると、Serfのような使い勝手が出来るようになりそうなので、Consul でできる幅がさらに広まります(Attenderの必要性はおそらくなくなりますね)。