前回の
プロジェクトでcronを利用する
筆者は普段ゲーム開発のサーバサイドを担当していますが、
100行とはいかなくても、
crontabの記述とリポジトリ管理
では実際のプロジェクトでcrontabをどのように管理していけばよいのでしょうか。筆者は次の方針を立てています。
- crontabの記述にゆるやかな規約を設け、
リポジトリ管理する - crontabの自動テストを行う
- crontabの反映方法をなるべく自動化する
- crontabの反映漏れを検知する
crontabの記述にはRubyのwheneverのようなDSL
crontabの例
では実際にはどのようなcrontabを書けばよいのでしょうか? リスト1とリスト2に悪い例と良い例を示します。
37 2 * * * /opt/perl-5.18/bin/perl /home/app/proj/script/daily_partitition.pl > /dev/null 2>&1
7 2 * * * /opt/perl-5.18/bin/perl /home/app/proj/script/event_partition.pl > /dev/null 2>&1
リスト1には次の問題点があります。
- レイアウトがバラバラで読みづらい
- 重複した記述が多い
- 出力を/dev/
nullに捨ててしまっている
PATH=/usr/local/bin:/usr/bin:/bin
MYPROJ_RUN="/home/app/myproj/env-exec runcron"
LOGGER="/path/to/fluent-agent-lite"
### db partitioning
37 2 * * * $MYPROJ_RUN -- script/batch.pl DailyPartition 2>&1 | $LOGGER cron.daily_partition -
7 2 * * * $MYPROJ_RUN -- script/batch.pl EventPartition 2>&1 | $LOGGER cron.event_partition -
筆者が担当しているプロジェクトではリスト2のようにcrontabを記述し、
- レイアウトの工夫
- 環境変数の活用
- env-execとruncronを指定した起動方法の統一
- perlの場合はbatch.
plランチャをさらに経由 - 出力先の統一とfluent-agent-liteの活用
それぞれに関して以降で説明していきます。
レイアウトの工夫
当たり前ですが、
- ジョブの内容などで分類してコメントを入れ、
セクション分けする - セクションごとでよいので縦の位置をそろえて書く
環境変数の活用
環境変数は/usr/
に$PATH
が通っていないと不便なこともあるので、
また、$MYPROJ_
と$LOGGER
という変数を定義しています。
env-execとruncronを指定した起動方法の統一
$MYPROJ_
を前置して各種バッチを起動するように統一しています。$MYPROJ_
の中にはenv-exec
とruncron
が指定されています。
env-exec
はプロジェクト用にカスタマイズされたラッパ用のシェルスクリプトです。
#!/bin/sh
set -e
export USER=app
export HOME=/home/$USER
cd $(dirname $0)
export PATH="local/bin:/opt/perl-5.18/bin:$PATH"
export PERL5LIB="lib:local/lib/perl5"
export PLACK_ENV=production
exec "$@"
このシェルスクリプトでは次のことを行っています。
- appというアプリケーション実行ユーザの指定
- プロジェクトディレクトリへcd
- perl-buildでビルドしたPerlへの$PATHを通す
- 各種環境変数の設定
- 引数で指定したコマンドをexecで実行
このラッパシェルは、
runcron
については次回
perlの場合はbatch.plランチャをさらに経由
上記の例ではさらに、
#!/usr/bin/env perl
use strict;
use warnings;
use Module::Load;
my $name = shift @ARGV;
my $module = "MyApp::Batch::$name";
load $module;
$module->new_with_options->run;
リスト2ではDailyPartitionという引数を受け取っていますが、run
メソッドが呼ばれるようになっています。MouseX::Getoptを統一的に使っているため、new_
になっています。
このようにバッチ処理の実体をモジュール形式で書くことで、
出力先の統一とfluent-agent-liteの活用
cronにおいてジョブの出力をどのように扱うかは重要です。 /dev/
に捨ててしまう例を時折見かけますが、$LOGGER
という出力コマンド格納用の変数にfluent-agent-lite
を代入しています。
fluent-agent-liteは田籠聡さんが作成したFluentdプロトコル互換のツールです。Perlで書かれており、
fluent-agent-liteは標準入力を受け取り、{"message":"hello"}
といったJSON
loggerコマンドの代替としてFluentdがカジュアルに使え、
このような規約を設けることで、
crontabをテストする
cronはサービスの一部であり、
テスト項目には次のようなものが挙げられます。
- a.crontabにシンタックスエラーがないか
- b.意図しない危険な指定がされていないか
(うっかり毎分指定、 日付と曜日の重複指定など) - c.ログを捨てていないか
- d.規約に従って記述をしているか
- e.コマンドに実行パーミッションが付いているか
- f.指定コマンドやファイル名をtypoしていないか
- g.実行時間のチェック
(意図した時間に実行されるか、 実行されたくない時間に誤って実行されないか)
これだけのテスト要求項目があり、
Parse::Crontabを使ったcrontabのテスト
crontabのテストには拙作のParse::Crontabが便利です。先ほど取り上げたテスト項目を、
use strict;
use warnings;
use Test::More;
use File::Which qw/which/;
use Parse::Crontab;
# 11 2 * * * $MYPROJ_RUN script/batch.pl \
# DailyPartition 2>&1 | $LOGGER cron.daily_partition -
# 上記のように指定されているか確認する
my $crontab = Parse::Crontab->new(
file => 'data/crontab.txt'
);
# a crontab のシンタックスに誤りがないか
ok $crontab->is_valid
or diag $crontab->error_messages;
# b 危険な指定の確認
ok !$crontab->warning_messages
or diag $crontab->warning_messages;
# ジョブ一覧を取得する
for my $job ($crontab->jobs) {
my $command = $job->command;
# c $LOGGER でログを出力しているか
like $command, qr!2>&1\s+\|\s+\$LOGGER!, 'logger ok';
# d $MYPROJ_RUN を使う規約を守っているか
ok $command =~ m!^\$MYPROJ_RUN!;
my ($opt, $rest_command) =
$command =~
m!^\$MYPROJ_RUN\s*(*?)?\s*--\s*(.*)!;
my $cmd = +(split /\s+/, $rest_command)[0];
# e 実行権がちゃんと付与されているか
ok -e $cmd or $cmd = which($cmd);
ok -x $cmd;
if (my ($module) =
$rest_command =~
m!^script/batch\.pl\s+([A-Za-z0-9]+)!
) {
# f batch.pl への指定モジュールが存在するか
my $module_file = "lib/MyApp/Batch/$module.pm";
ok -e $module_file;
}
# g スケジュールオブジェクトを取り出して
# 必要に応じて実行時間のテストをする
my $schedule = $job->schedule;
ok $schedule->match(hour => 2, minutes => 15 ...);
}
done_testing;
aでcrontabのシンタックス確認、
crontabの本番環境
本番反映の方法
crontabコマンドは引数にファイルを指定すると、
次のようにしてcrontabを反映すればよいでしょう。筆者のプロジェクトではcronやワーカ類を動かす専用のサーバを立てているので、
% ssh myproj-batch01
# 差分確認
% diff -u <(crontab -l) data/crontab.txt
# 反映
% crontab data/crontab.txt
上記はデプロイ時に自動反映してもよいとは思いますが、
crontabの本番反映漏れを防ぐTips
さて、
そういうミスを防ぐために、
<続きの