ちょっと話題の記事

Multi-AZに対応した高可用Cronサーバを構築する

はじめに

ジョブスケジューリングを簡易的な仕組みで構築する場合、まず候補に上がるのはEC2上のLinuxでcronを利用したものだと思います。特別なミドルウェアの追加インストールはいらないし、使い慣れているし、スクリプトと組み合わせればだいたい何でも出来ちゃいますし。しかし単体のEC2上でcronを動かすだけでは、可用性が確保出来ません。AWSにおいてAZ障害まで考慮するのであれば、Multi-AZに冗長化されたシステムを構成し、可用性を確保する必要があります。

で、単純に複数のAZに分散してEC2を構築し、crontabを共有するだけでは、ジョブが二重に実行されてしまいます。アクティブ/スタンバイに動作するような仕組みを考慮しなくてはいけません。

そこで今回は、クラスタ構成がサポートされている最新のcrond(cronie)を使って、Multi-AZに対応した高可用Cronサーバを構築します!

構成

AWSの東京リージョンで、2つのAZにそれぞれEC2を構築します。OSはAmazon Linux(2015.03)です。

2台のEC2のうち、AZ-aにあるほうをCronマスタ、AZ-cにあるほうをCronスレーブとします。各ユーザが設定するcrontab(/var/spool/cron)は、lsyncdによってCronマスタからCronスレーブに同期されます。

クラスタ構成がサポートされているcrondでは、/var/spool/cron/.cron.hostnameファイルによってアクティブとスタンバイの動作を切り替えます。.cron.hostnameファイルに自ホスト名が記述されている場合のみ、アクティブとして/var/spool/cron/下のcrontabファイルを実行します。.cron.hostnameファイルが無い場合、あるいは自ホスト名が記述されていない場合は、スタンバイとして/var/spool/cron/下のcrontabファイルを実行しません。

なお、このアクティブ/スタンバイは/var/spool/cron/下のcrontabファイルに対してのみ行われ、/etc/crontabや/etc/cron.*下についてはそれぞれのホスト個別に実行されます。この仕様を使って、/etc/crontabによって定義されたping.shによってアクティブとスタンバイを切り替えます。

正常動作時はAZ-aにあるCronマスタがアクティブになります。Cronスレーブは.cron.hostnameが存在しないのでスタンバイとなります。

AWS_Simple_Icons_2_3_light_edition_pptx

Cronマスタが停止した場合、/etc/crontabによって起動されたping.shがCronマスタの停止を検知し、/var/spool/cron/.cron.hostnameを作成し、アクティブとなります。今回は仕組みを簡素化するためにCronスレーブからCronマスタへのcrontabの同期は行っておりませんが、必要あれば相互に同期することももちろん可能です。

AWS_Simple_Icons_2_3_light_edition_pptx 2

Cronスレーブが停止した場合は、特に動作に変化はありません。Cronスレーブが復帰したら、また/var/spool/cron下が同期されます。

AWS_Simple_Icons_2_3_light_edition_pptx 3

やってみる

下準備

今回の仕組みを使うためには、それぞれのEC2でホスト名が明確になっている必要があるので、以下のように設定しておきます。なおCronマスタが「cron-a」、Cronスレーブが「cron-c」です。

$ sudo vi /etc/sysconfig/network
HOSTNAME=cron-a

最新版のCronieをインストールする

この作業はCronマスタ、Cronスレーブともに行います。

Amazon LinuxにインストールされているCron(cronie)は1.4.4です。

$ sudo rpm -qa | grep cron
cronie-1.4.4-12.6.amzn1.x86_64

しかし、このバージョンではクラスタ構成がサポートされておらず、-cオプションが使えません。

$ crond -c
crond: 無効なオプション -- 'c'
usage:  crond [-n] [-p] [-s] [-i] [-m <mail command>] [-x [ext,sch,proc,pars,load,misc,test,bit]]

そこで、最新のcronieをインストールします。まずはインストールに必要なパッケージをインストールします。

$ sudo yum install automake gcc
$ sudo yum install git

gitリポジトリをcloneし、autoreconfを実行します。

$ git clone https://git.fedorahosted.org/git/cronie.git
$ cd cronie
$ autoreconf
configure.ac:7: error: required file './config.guess' not found
configure.ac:7:   'automake --add-missing' can install 'config.guess'
configure.ac:7: error: required file './config.sub' not found
configure.ac:7:   'automake --add-missing' can install 'config.sub'
configure.ac:5: error: required file './install-sh' not found
configure.ac:5:   'automake --add-missing' can install 'install-sh'
configure.ac:5: error: required file './missing' not found
configure.ac:5:   'automake --add-missing' can install 'missing'
anacron/Makefile.am: error: required file './depcomp' not found
anacron/Makefile.am:   'automake --add-missing' can install 'depcomp'
autoreconf: automake failed with exit status: 1

エラーが出てしまいました。automakeを手動で実行してmissingを解消し、再度autoreconfします。

$ automake --add-missing
configure.ac:7: installing './config.guess'
configure.ac:7: installing './config.sub'
configure.ac:5: installing './install-sh'
configure.ac:5: installing './missing'
anacron/Makefile.am: installing './depcomp'

$ autoreconf

今度はエラーが出ませんでした。ではmakeしてmake installします!

$ ./configure --sysconfdir=/etc --localstatedir=/var
$ make
$ sudo make install

無事インストールされたことを確認します!

$ ls -alF /usr/local/bin
-rwxr-xr-x  1 root root 131912  5月  8 07:10 crontab*

$ ls -alF /usr/local/sbin
-rwxr-xr-x  1 root root 198853  5月  8 07:10 crond*

しかしこのままだとうまく動きません。/usr/bin/crontabと同様に、/usr/local/bin/crontabにスティッキービットを設定します。

$ sudo chmod u+s /usr/local/bin/crontab
$ ls -alF /usr/local/bin
合計 140
-rwsr-xr-x  1 root root 131912  5月  8 08:46 crontab*

さて、起動スクリプトを書き換えて、新しくインストールしたcrondが使われるように変更します。

$ sudo vi /etc/init.d/crond
### /usr/sbin/crondから変更 ###
exec=/usr/local/sbin/crond

# Source function library.
. /etc/rc.d/init.d/functions
### functionsを読み込んだ後にPATHを設定 ###
PATH="/usr/local/bin:/usr/local/sbin":$PATH

更に、crondの起動時に-cオプションを付与し、クラスタ構成サポートモードで起動するようにします。

$ sudo vi /etc/sysconfig/crond
CRONDARGS=-c

では起動します。

$ sudo service crond restart
Stopping crond:                                            [  OK  ]
Starting crond:                                            [  OK  ]

$ ps aux | grep crond
root     15615  0.0  0.0 108420   668 ?        Ss   07:36   0:00 crond -c

ちゃんとcrond-cオプション付きで起動しましたね!

lsyncdのセットアップ

次に、/var/spool/cronを同期させるためのlsyncdをセットアップします。この作業はCronマスタだけで行います。

事前にec2-userでCronマスタからCronスレーブにssh接続できるように、鍵ファイルの設置などを済ませておきます。

/var/spool/cronはデフォルトではrootのみ参照可能な権限になっていますので、ec2-userをrootグループに所属させ、/var/spool/cronのパーミッションを変更します。

$ usermod -G root ec2-user
$ chmod 770 /var/spool/cron
$ ls -alF /var/spool/cron
drwxrwx--- 2 root     root     4096  5月  8 08:54 ./

この時点で一体手動でrsyncを実行し、ちゃんと動くことを確認しておきましょう。

$ sudo -s
# rsync -avn -e "/usr/bin/ssh -l ec2-user -i /home/ec2-user/.ssh/id_rsa" /var/spool/cron/ 172.31.200.10:/var/spool/cron/

ではlsyncdをインストールし、設定を行います。

$ sudo yum install --enablerepo=epel lsyncd

$ sudo vi /etc/lsyncd.conf
settings{
  logfile = "/var/log/lsyncd/lsyncd.log",
  statusFile = "/tmp/lsyncd.stat",
  statusInterval = 1,
  nodaemon = false,
}

sync {
  default.rsyncssh,
  source = "/var/spool/cron/",
  host = "172.31.200.10",
  targetdir = "/var/spool/cron/",
  rsync  = {
    rsh = "/usr/bin/ssh -l ec2-user -i /home/ec2-user/.ssh/id_rsa",
    _extra  = {
      "-rlOt",
    }
  },
}

lsyncdを起動します。

$ sudo service lsyncd start
Starting lsyncd:                                           [  OK  ]

それでは同期を確認します。まずはec2-userでcrontabを作成できるように、/etc/cron.allowにec2-userと追記します。

$ sudo -s
# echo ec2-user >> /etc/cron.allow
# exit

ではec2-userでcrontabを作成しましょう。

$ crontab -e
*/1 * * * * date >> /home/ec2-user/date.txt

$ ls -alF /var/spool/cron/
-rw------- 1 ec2-user ec2-user   25  5月  8 08:54 ec2-user

しばらく待ってからCronスレーブで確認すると、ちゃんとファイルが同期されています!

$ ls -alF /var/spool/cron/
-rw------- 1 ec2-user ec2-user   25  5月  8 08:54 ec2-user

監視スクリプトを仕込む

それでは監視スクリプトをCronマスタとCronスレーブ両方に仕込みます。

まずはCronマスタから。Cronスレーブに対してPingを実行し、応答が無ければcrontab -nコマンドによって.cron.hostnameファイルを作成します。

$ sudo -s
# vi /root/ping.sh
#!/bin/sh
ping 172.31.200.10 -c 1 -i 1 > /dev/null 2>&1
UP=$?

if [ $UP -ne 0 ]; then
  /usr/local/bin/crontab -n
fi

実行権限をつけておきましょう。

# chmod 755 ping.sh

次にCronスレーブ。こちらはCronマスタと違い、Cronマスタに対してPingを実行し応答が無ければcrontab -nコマンドによって.cron.hostnameファイルを作成し、Cronマスタが起動している時には/var/spool/cron/.cron.hostnameを削除します。

$ sudo -s
# vi /root/ping.sh
#!/bin/sh
ping 172.31.100.10 -c 1 -i 1 > /dev/null 2>&1
UP=$?

if [ $UP -ne 0 ]; then
  /usr/local/bin/crontab -n
else
  rm /var/spool/cron/.cron.hostname
fi

こちらも同じく実行権限を付与します。

# chmod 755 ping.sh

Cronマスタ、Cronスレーブともに、定期的に監視スクリプトが動くように、/etc/crontabに設定します。

# vi /etc/crontab
*/5 * * * * root /root/ping.sh

初期設定として、Cronマスタで/var/spool/cron/.cron.hostnameを作成しておきます。

$ sudo /usr/local/bin/crontab -n

$ ls -alF /var/spool/cron
-rw------- 1 root     root        7  5月  8 10:37 .cron.hostname
-rw------- 1 ec2-user ec2-user   44  5月  8 11:05 ec2-user

動作確認

通常時、Cronマスタには.cron.hostnameがあり、Cronスレーブには.cron.hostnameがありません。

■Cronマスタ
$ ls -alF /var/spool/cron
-rw------- 1 root     root        7  5月  8 10:37 .cron.hostname
-rw------- 1 ec2-user ec2-user   44  5月  8 11:05 ec2-user

$ sudo cat /var/spool/cron/.cron.hostname
cron-a

■Cronスレーブ
$ ls -alF /var/spool/cron
-rw------- 1 ec2-user ec2-user   44  5月  8 11:05 ec2-user

また、Cronマスタでのみ、/var/spool/cron/ec2-userが実行されています。

$ cat /home/ec2-user/date.txt
Fri May  8 11:16:01 UTC 2015

ここでCronマスタをStopしてみます。するとCronスレーブで.cron.hostnameが作成されます

■Cronスレーブ
$ ls -alF /var/spool/cron/
-rw------- 1 root     root        7  5月  8 11:17 .cron.hostname
-rw------- 1 ec2-user ec2-user   44  5月  8 11:05 ec2-user

$ sudo cat /var/spool/cron/.cron.hostname
cron-c

そしてCronスレーブで/var/spool/cron/ec2-userが実行され始めます。

■Cronスレーブ
$ cat /home/ec2-user/date.txt
Fri May  8 11:18:01 UTC 2015
Fri May  8 11:19:01 UTC 2015

再度CronマスタをStartすると、Cronスレーブの.cron.hostnameが削除され、/var/spool/cron/ec2-userが実行されなくなります。

これでMulti-AZに対応した高可用性Cronサーバが出来上がりました!

さいごに

まぁ、ここまでやるならLVS + Keepalivedにしようとか、crontabはGlusterFSで同期しちゃおうとか、商用ジョブスケジューリングソフトウェアを使おうとか、いろいろあるとは思うのですが、Cronの冗長化が手軽にできるのは素敵だと思いました!