Serf 使ってみた

先日、社内勉強会以外の何か(仮)で、serf について話したときの資料がでてきたので置いておきます。

https://gist.github.com/ngyuki/23b9fa494fd49e358734

まあ、4096番煎じぐらいで真新しいものではありません。

Serf is なに?

Packer や Vagrant の HashiCorp 社が作っているオーケストレーションツール

オーケストレーション is なに?

サーバプロビジョニングを構成する要素の一部だと言われていますが・・・

  • Bootstrapping
    • Kickstart とか
  • Configuration
    • Ansible とか
  • Orchestration
    • Serf とか

オーケストレーション (コンピュータ) - Wikipedia

オーケストレーション(英: Orchestration)は、複雑なコンピュータシステム/ミドルウェア/サービスの配備/設定/管理の自動化を指す用語。 何らかの知的制御や自律制御として議論されることが多いが、技術的解説と言うよりも大部分は単なるアナロジーである。実際には、オーケストレーションは制御理論の要素としてオートメーションやシステムの考え方を持ち込んだものと言える。 このようなコンピュータシステムの「オーケストレーション」という用語は、仮想化やプロビジョニングの文脈で語られることが多く、バズワード的要素が強い。

Bootstrapping でも Configuration でもないその他いろいろ、程度のニュアンスだと思います。

Docker コンテナ

試す環境として Docker を使います。

docker run -dit --name node1 -h node1 centos:7 bash
docker run -dit --name node2 -h node2 centos:7 bash
docker run -dit --name node3 -h node3 centos:7 bash

それぞれ別のターミナルでログインします。

docker exec -it node1 bash
docker exec -it node2 bash
docker exec -it node3 bash

この Docker コンテナは最小構成の CentOS 7 なので、あとで必要になるパッケージをインストールしておきます。

yum install -y wget unzip

serf インストール

serf は golang 製なのでバイナリいっこ配置するだけでインストールできます。

wget https://dl.bintray.com/mitchellh/serf/0.6.4_linux_amd64.zip
unzip 0.6.4_linux_amd64.zip
mv serf /usr/local/bin

serf をとりあえず試す

すべてのノードでエージェントを起動します。

serf agent -iface=eth0

クラスタメンバの確認します。上で開いたターミナルは serf agent がフォアグラウンドにいるので、別のターミナルから docker exec でコンテナの中でコマンドを実行します(インデント部分はコマンドの出力)。

docker exec node1 serf members
    node1  172.17.0.10:7946  alive

docker exec node2 serf members
    node2  172.17.0.11:7946  alive

docker exec node3 serf members
    node3  172.17.0.12:7946  alive

まだお互いを認識していないので、メンバには自分自身だけ表示されます。

node02 で node01 のクラスタにジョインします。

docker exec node2 serf join 172.17.0.10

もう一回クラスタメンバを確認してみます(インデント部分はコマンドの出力)。

docker exec node1 serf members
    node1  172.17.0.10:7946  alive  
    node2  172.17.0.11:7946  alive

docker exec node2 serf members
    node2  172.17.0.11:7946  alive  
    node1  172.17.0.10:7946  alive

docker exec node3 serf members
    node3  172.17.0.12:7946  alive

node01 と node02 はお互いを認識しました。

node03 も node01 のクラスタにジョインします。

docker exec node3 serf join 172.17.0.10

すべてのノードがお互いを認識するようになりました(インデント部分はコマンドの出力)。

docker exec node1 serf members
    node3  172.17.0.12:7946  alive  
    node1  172.17.0.10:7946  alive  
    node2  172.17.0.11:7946  alive

docker exec node2 serf members
    node2  172.17.0.11:7946  alive  
    node1  172.17.0.10:7946  alive  
    node3  172.17.0.12:7946  alive

docker exec node3 serf members
    node3  172.17.0.12:7946  alive  
    node2  172.17.0.11:7946  alive  
    node1  172.17.0.10:7946  alive

ディスカバリ

前述の方法だと、最初にいずれかのノードを指定してクラスタにジョインする必要がありましたが、マルチキャストが使える環境であれば自動的にクラスタにジョインさせることもできます。

いったんすべてのノードでエージェントを停止します。

次のように -discover に適当な名前を付けて起動します。

serf agent -iface=eth0 -discover=oreore

起動後にクラスタメンバを確認してみると、自動的にクラスタにジョインされています(インデント部分はコマンドの出力)。

docker exec node1 serf members
    node1  172.17.0.10:7946  alive  
    node2  172.17.0.11:7946  alive  
    node3  172.17.0.12:7946  alive

docker exec node2 serf members
    node2  172.17.0.11:7946  alive  
    node1  172.17.0.10:7946  alive  
    node3  172.17.0.12:7946  alive

docker exec node3 serf members
    node3  172.17.0.12:7946  alive  
    node1  172.17.0.10:7946  alive  
    node2  172.17.0.11:7946  alive

なお、-discover に指定した名前がクラスタの名前になるので、同じセグメントに複数のクラスタがある場合はクラスタごとに異なる名前にする必要があります。

イベントハンドラ

serf はクラスタ内で発生するさまざまなイベントに対してスクリプトを実行することができます。

一旦 node1 のエージェントを停止してイベントハンドラのスクリプトを node1 に作成します。

cat <<'EOS'> handler.sh
#!/bin/bash
echo 
printf "\e[0;32m%s=%s\e[m\n" "SERF_EVENT" "${SERF_EVENT}"
printf "\e[0;32m%s=%s\e[m\n" "SERF_SELF_NAME" "${SERF_SELF_NAME}"
printf "\e[0;32m%s=%s\e[m\n" "SERF_USER_EVENT" "${SERF_USER_EVENT}"
printf "\e[0;32m%s=%s\e[m\n" "SERF_USER_LTIME" "${SERF_USER_LTIME}"
while read line; do
    printf "  \e[0;32m%s\e[m\n" ${line}
done
EOS
chmod +x handler.sh

イベントハンドラを指定してエージェントを起動します。

serf agent -iface=eth0 -discover=oreore -log-level=debug -event-handler=$PWD/handler.sh

さっそくノードがジョインしたイベントが発生しました。

SERF_EVENT=member-join
SERF_SELF_NAME=node1
SERF_USER_EVENT=
SERF_USER_LTIME=
  node1
  172.17.0.10
  node2
  172.17.0.11
  node3
  172.17.0.12

node2 のエージェントを停止すると member-leave イベントが発生します。

SERF_EVENT=member-leave
SERF_SELF_NAME=node1
SERF_USER_EVENT=
SERF_USER_LTIME=
  node2
  172.17.0.11

もう一度 node2 のエージェントを起動すると member-join が発生します。

SERF_EVENT=member-join
SERF_SELF_NAME=node1
SERF_USER_EVENT=
SERF_USER_LTIME=
  node2
  172.17.0.11

node3 のエージェントをサスペンドさせると member-failed が発生します (Ctrl+Z)。

SERF_EVENT=member-failed
SERF_SELF_NAME=node1
SERF_USER_EVENT=
SERF_USER_LTIME=
  node3
  172.17.0.12

再開させると (fg) member-join が発生します。

SERF_EVENT=member-join
SERF_SELF_NAME=node1
SERF_USER_EVENT=
SERF_USER_LTIME=
  node3
  172.17.0.12

カスタムイベント

次のように任意のカスタムイベントを発生させることができます

docker exec node1 serf event hoge 1234567890

hoge はイベント名で 1234567890 はペイロードです、ペイロードは標準入力から得られます

SERF_EVENT=user
SERF_SELF_NAME=node1
SERF_USER_EVENT=hoge
SERF_USER_LTIME=1
  1234567890

クエリ

カスタムイベントはイベントを通知するだけですが、クエリだと各ノードからコマンドやスクリプトの実行結果を得ることができます

一旦すべてのノードのエージェントを停止して、次のようにイベントハンドラを指定してエージェントを起動します

serf agent -iface=eth0 -discover=oreore -event-handler=query:shell=/bin/bash

クエリを実行します

docker exec node1 serf query shell uptime

すべてのノードで uptime を実行した結果が得られます。

Query 'shell' dispatched
Ack from 'node1'
Response from 'node1':  12:55:05 up 1 day, 12:09,  0 users,  load average: 0.00, 0.00, 0.00
Ack from 'node2'
Ack from 'node3'
Response from 'node3':  12:55:06 up 1 day, 12:09,  0 users,  load average: 0.00, 0.00, 0.00
Response from 'node2':  12:55:06 up 1 day, 12:09,  0 users,  load average: 0.00, 0.00, 0.00
Total Acks: 3
Total Responses: 3

Docker コンテナを管理

もう少し実用的な用途として、Docker のコンテナ起動時に serf を自動でホストを含むクラスタに参加させるようにしてみます。

まず、ホスト側にも serf をインストールします。

wget https://dl.bintray.com/mitchellh/serf/0.6.4_linux_amd64.zip
unzip 0.6.4_linux_amd64.zip
sudo mv serf /usr/local/bin

コンテナ起動時に serf を実行するための Dockerfile を作成します。

FROM centos:7

RUN yum install -y wget unzip ;\
    yum clean all

RUN wget -q https://dl.bintray.com/mitchellh/serf/0.6.4_linux_amd64.zip && \
    unzip 0.6.4_linux_amd64.zip && \
    mv -v serf /usr/local/bin ;\
    rm -vf unzip 0.6.4_linux_amd64.zip

ENTRYPOINT ["serf"]
CMD ["agent", "-iface=eth0", "-discover=oreore", "-event-handler=query:shell=/bin/bash"]

イメージをビルドします。

docker build -t example:serf .

コンテナを起動します。

docker run -d --name node1 -h node1 example:serf
docker run -d --name node2 -h node2 example:serf
docker run -d --name node3 -h node3 example:serf

さらにホスト側でも起動

serf agent -iface=docker0 -discover=oreore -node=host >/dev/null &

メンバを一覧すると・・・

serf members

コンテナの一覧が得られます。

host   172.17.42.1:7946  alive  
node1  172.17.0.10:7946  alive  
node2  172.17.0.11:7946  alive  
node3  172.17.0.12:7946  alive

クエリを使えば・・・

serf query shell uptime

すべてのコンテナでコマンドを実行することができます。

Query 'shell' dispatched
Ack from 'host'
Ack from '361998e3d055'
Response from '361998e3d055':  13:10:24 up 1 day, 12:24,  0 users,  load average: 0.00, 0.05, 0.02
Ack from 'd0e5b5fb6c63'
Response from 'd0e5b5fb6c63':  13:10:24 up 1 day, 12:24,  0 users,  load average: 0.00, 0.05, 0.02
Ack from '3fc7c50f677e'
Response from '3fc7c50f677e':  13:10:25 up 1 day, 12:24,  0 users,  load average: 0.00, 0.05, 0.02
Total Acks: 4
Total Responses: 3

さいごに

Docker コンテナを自動でクラスタにする例はなかなか実用的な雰囲気がありましたが、実際のところなかなかよい用途が思いつかないような気もします。

よく紹介されている例だと、次のような用途があるようです。

  • ノードの起動時に /etc/hosts を自動で追記
    • 停止時には /etc/hosts から自動で削除する
  • ノードの起動時のロードバランサに自動で追加
    • 停止時にはロードバランサから自動削除する
  • ノードの起動時に自動的に munin の監視対象として追加
    • 停止時には自動的に削除される
  • デプロイなどの一括実行
    • SSH 経由の Push 型よりも並列度を高められる
  • 簡単な Active/Standby の HA クラスタ
    • ノードに優先度を設けて Active を選択