コンテナ/クラウド環境におけるSerfを使った構成管理 2ページ
Serfでクラスタを構成する
Serfでは、「エージェント」という機能を使ってクラスタを構成する。前述のように分散型のアーキテクチャを採用しており、そのため管理用のノードは存在しない。各ノードで実行されるエージェント同士が直接情報をやり取りすることで、各ノードの状況を監視する仕組みになっている(図3)。
さて、クラスタを構築する場合には、すでに起動しているエージェントに対し、新たにクラスタに加入したいノードの情報を通知する必要がある。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アドレスは、クラスタに参加しているノードのものであればどれでも構わない。
こういった環境でクラスタを構築する場合、まず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」オプションによるクラスタの自動追加機能を利用できる。