コンテナ/クラウド環境におけるSerfを使った構成管理 2ページ

Serfでクラスタを構成する

 Serfでは、「エージェント」という機能を使ってクラスタを構成する。前述のように分散型のアーキテクチャを採用しており、そのため管理用のノードは存在しない。各ノードで実行されるエージェント同士が直接情報をやり取りすることで、各ノードの状況を監視する仕組みになっている(図3)。

図3 Serfの「エージェント」
図3 Serfの「エージェント」

 さて、クラスタを構築する場合には、すでに起動しているエージェントに対し、新たにクラスタに加入したいノードの情報を通知する必要がある。Serfでは「mDNS」という仕組みを使用し、自動的にネットワーク内で起動しているエージェントを検出してクラスタ加入申請を送信する仕組みがある。これにより、クラスタメンバのIPアドレスといった情報を知らずともクラスタに加入できるようになっている。

 この仕組みを使用してエージェントを起動するには、以下のように「-discover」オプションでクラスタ名を指定して「serf agent」コマンドを実行する。クラスタ名は任意のものを使用できる。

$ serf agent -iface=<使用するネットワークインターフェイス> -discover=<クラスタ名>

 たとえば、「eth1」というネットワークインターフェイスを使用し、「cluster01」というクラスタ名を持つクラスタに加入する場合、以下のようになる。

$ serf agent -iface=eth1 -discover=cluster01

 ただし、mDNSはマルチキャストパケットを使用するため、環境によっては利用できない場合がある。たとえばさくらのクラウドではスイッチ/ルーターによってmDNSで使われるマルチキャストパケットが遮断されるため、この方法は利用できない。

 mDNSが利用できない場合、クラスタに参加する2台目以降のノードでは「-join=<IPアドレス>」オプションでクラスタ内で稼動しているいずれかのノードのIPアドレスを指定してエージェントを起動する必要がある(図4)。ここで指定するIPアドレスは、クラスタに参加しているノードのものであればどれでも構わない。

図4 mDNSによる自動認識が利用できない場合、クラスタ内のどれか1つのノードを指定してエージェントを起動する
図4 mDNSによる自動認識が利用できない場合、クラスタ内のどれか1つのノードを指定してエージェントを起動する

 こういった環境でクラスタを構築する場合、まず1台目のノードではクラスタ内にほかのノードが存在しないため、「-join」オプションなしでエージェントを起動する。

$ serf agent -iface=eth1
==> Using interface 'eth1' address '192.168.1.10'
==> Starting Serf agent...
==> Starting Serf agent RPC...
==> Serf agent running!
         Node name: 'centos10'
         Bind addr: '192.168.1.10:7946'
          RPC addr: '127.0.0.1:7373'
         Encrypted: false
          Snapshot: false
           Profile: lan

==> Log data will now stream in as it occurs:

    2016/07/14 20:30:54 [INFO] agent: Serf agent starting
    2016/07/14 20:30:54 [INFO] serf: EventMemberJoin: centos10 192.168.1.10
    2016/07/14 20:30:55 [INFO] agent: Received event: member-join

 続いて、クラスタに追加したい別のノードで「-join」オプション付きでエージェントを起動する。先のノードでは「192.168.1.10」というIPアドレスが使われているので、こちらを-joinオプションの引数として指定する。

$ serf agent -iface=eth1 -join=192.168.1.10
==> Using interface 'eth1' address '192.168.1.11'
==> Starting Serf agent...
==> Starting Serf agent RPC...
==> Serf agent running!
         Node name: 'centos11'
         Bind addr: '192.168.1.11:7946'
          RPC addr: '127.0.0.1:7373'
         Encrypted: false
          Snapshot: false
           Profile: lan
==> Joining cluster...(replay: false)
    Join completed. Synced with 1 initial agents

==> Log data will now stream in as it occurs:

    2016/07/14 20:31:08 [INFO] agent: Serf agent starting
    2016/07/14 20:31:08 [INFO] serf: EventMemberJoin: centos11 192.168.1.11
    2016/07/14 20:31:08 [INFO] agent: joining: [192.168.1.10] replay: false
    2016/07/14 20:31:08 [INFO] serf: EventMemberJoin: centos10 192.168.1.10
    2016/07/14 20:31:08 [INFO] agent: joined: 1 nodes
    2016/07/14 20:31:09 [INFO] agent: Received event: member-join

 このようにしてクラスタにノードを追加すると、1台目のノードの出力にも以下のような表示が出力され、追加したノードが認識されていることが分かる。

    2016/07/14 20:31:08 [INFO] serf: EventMemberJoin: centos11 192.168.1.11
    2016/07/14 20:31:09 [INFO] agent: Received event: member-join

 なお、エージェントはフォアグラウンドで実行され、Ctrl-Cを入力することで終了できる。エージェントを終了すると、自動的にクラスタからそのノードは削除される。

Serfの起動スクリプトを用意する

 エージェントはdaemonとして起動されるわけではないため、バックグラウンドで実行させるには工夫が必要だ。たとえばCentOS 7やUbuntu 16.04 LTSなどでは、/etc/systemd/system/serf.serviceとして次のような設定ファイルを用意すれば良い。

[Unit]
Description=Serf cluster agent
Documentation=https://www.serfdom.io/
After=network.target

[Service]
EnvironmentFile=-/etc/sysconfig/serf ←CentOSの場合、/etc/sysconfig/ディレクトリ以下に環境変数設定ファイルを配置する
ExecStart=/usr/local/bin/serf agent $SERF_AGENT_OPTS
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

 この設定ファイルではserfコマンドに与えるオプションを「/etc/sysconfig/serf」という設定ファイルに記述するようにしている。このファイルの内容は以下のとおりだ。

SERF_AGENT_OPTS="<ここに「serf agent」コマンドに与えるコマンドラインオプションを記述する>"

 なおUbuntuの場合、/etc/sysconfig/ディレクトリではなく/etc/default/ディレクトリ以下に設定ファイルを配置するのが慣例となっているので、/etc/systemd/system/serf.serviceファイルの「EnvironmentFile=」行を以下のように変更するとともに、「/etc/default/serf」というファイルでオプション設定用のファイルを置けば良い。

EnvironmentFile=-/etc/default/serf

 また、設定ファイルを変更したら、その都度systemdのリロードが必要になるので注意しよう。

# systemctl daemon-reload

 これで、systemctlコマンド経由でSerfのエージェントの起動や停止といった管理が行えるようになる。たとえばエージェントを起動するには、以下のように実行する。

# systemctl start serf

 また、serfのログは以下のようにして確認できる。

# journalctl -u serf

Dockerコンテナ内でSerfエージェントを実行する

 Dockerコンテナ内でSerfエージェントを実行する場合、あらかじめコンテナイメージを生成するためのDockerfile内でSerfのバイナリファイルをコンテナ内にコピーしておき、かつコンテナの起動時に自動的に「serf agent」コマンドが実行されるようにしておく必要がある。これは、Dockerfileの「ENTRYPOINT」コマンドでコンテナ実行時にあらかじめ指定しておいたシェルスクリプトを実行するよう指定しておき、そのシェルスクリプト内で「serf agent」コマンドを実行することで実現できる。

 たとえばbusyboxイメージをベースとする場合、Dockerfileは次のようになる。これ以外のイメージをベースとする場合も、下記はほぼそのまま利用できるだろう。なお、この例ではSerfのバイナリ(「serf」ファイル)や「entry-point.sh」ファイルはDockerfileと同じディレクトリ内に作成した「files」ディレクトリ内に配置している。

FROM busybox

# ↓serfのバイナリを/usr/local/bin/以下にコピーする
COPY files/serf /usr/local/bin/serf
RUN chmod 755 /usr/local/bin/serf

# ↓エージェントを起動するためのシェルスクリプト「entry-point.sh」をコピーする
COPY files/entry-point.sh /entry-point.sh
RUN chmod 755 /entry-point.sh

# ↓コンテナの起動時にentry-point.shスクリプトが実行されるようにする
ENTRYPOINT ["/entry-point.sh"]
CMD ["sh"]

 また、コンテナの起動時に実行させるentry-point.shは以下のようになる。

#!/bin/sh
# ↓パイプ中でエラーが発生した場合処理途中で終了するよう設定する
set -eo pipefail

# ↓「serf agent」コマンドに与えるオプションを指定する
SERF_AGENT_OPT="-discover=container01"

# ↓「serf agent」コマンドを実行し、その出力は/var/log/serf.logファイルに格納させておく
mkdir -p /var/log
/usr/local/bin/serf agent $SERF_AGENT_OPT > /var/log/serf.log &
SERF_PID=$!

# ↓「docker run」コマンドの引数で与えられたコマンドを実行する
"$@"

# ↓SerfエージェントにSIGINTシグナルを送信して終了させる
test -n "$SERF_PID" && kill -s SIGINT $SERF_PID && sleep 3

 このシェルスクリプト内では「docker run」コマンドに引数として与えたコマンドの実行完了後、killコマンドでエージェントのプロセスを終了させている。このとき、単純にkillコマンドでエージェントを終了させてしまうと、ほかのノードからはエージェントが異常終了したもの(後述する「failed」状態)と認識されてしまうので注意したい。ここではkillコマンドに「-s SIGINT」オプションを付け、コンソールからCtrl-Cを入力した際に送信されるのと同じSIGINTシグナルを送信することで、エージェントが正常に終了したように認識させている。

 なお、Dockerのコンテナ同士を接続する仮想ネットワークは、通常はmDNSで使われるマルチキャストパケットをそのまま通過させることができ、「-discover」オプションによるクラスタの自動追加機能を利用できる。