ベストプラクティス cron

cron の一時停止で問題が発生したのを契機に、差分管理等の話が某所で出たので cron のベストプラクティスに関して示す。

※ とりあえず話聞いて思ってた事、思いついた事を書いただけなので、そのうち他の項目も思い出したり「やっぱこれ違ってた」になるかもしれない。
※ 俺の cron 的な Tips なので、適当なカスタマイズやプロジェクト毎の最適化推奨。

※ かつての記事のリライトのため、現状の環境とは異なる部分があります。

開発

まずは開発 ( cron実装/設定者 ) 寄りのプラクティスから。

/etc/cron.d を使う

利用している環境でサポートしている事が前提になりますが、可能であれば /etc/cron.d/ を利用しましょう。
※ crontab -e で直接 crontab を編集しない。

具体的には /etc/cron.d/ 下に複数の crontab 設定ファイルを配置します。

ls /etc/cron.d/
system-a
system-b

system-a の例

# system-a cron jobs
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
0 * * * *  sysauser sysa-task01
30 * * * * sysauser sysa-task02

これにより

  • crontab 設定を意味のある形でグループ化できる。
  • 個々の設定に適切なパーミッションを付与する事で、他システムの crontab 設定を不要に編集しないようにする事も可能。
  • cron 用のシェルスクリプト ( 後述 ) と同様にバージョン管理する場合によりよく機能する。

個々のユーザが利用する cron 設定を許容する ( ユーザが crontab -e で設定を編集する ) のもOKですが、多くの場合、個々のユーザが cron を利用する・しなければならないユースケースは少ないと思われます。

タスクを実行するシェルスクリプトを利用する

簡単なコマンドやワンライナーで実行可能な処理の場合に、crontab に直接システムやミドルウェアのコマンドを記載する事は可能ですが、 組織で管理するタスクを実行する場合には、必ずタスクを実行するシェルスクリプトを作成し、このファイルに処理を実装。crontab にはシェルスクリプトを実行するように指定する方法をお勧めします。
こうする事で以下のようなメリットが生まれます。

  • cron 実行時にはユーザがログインした場合とは環境変数の扱いが異なるためにトラブルが発生する事がある。シェルスクリプト化する事で、コマンド実行前に必要な環境変数を設定したり、場合によっては変更する事が容易になる。
  • シェルスクリプト自体をバージョン管理する事で、処理内容の変更履歴をトラッキングしたり、PR/MRプロセスを挟む事で適切なレビューを行うといった事が可能になる。
  • cron で指定したスクリプトから別の ( 環境変数の設定や通知、ログ出力等の ) 共通処理用スクリプトを呼び出してから実処理を呼び出すといった事も容易になる。

スクリプトのコーディングルールを決める

がっちりルール化する必要はないかと思いますが、判りやすいテンプレートを用意するなどして、記載レベルや処理内容をできる範囲で統一した方がよいでしょう。

  • 適度な粒度でコメントを書く
  • 変更される可能性のある値は変数化
  • 共通処理を決めてこれを使う。
    共通スクリプト化して呼び出す等

バージョン管理する

バージョン管理をする理由としては、問題発生時に元に戻したり、差分を確認したりする事ももちろんですが、これ以外に以下の点も目指す。

  • CI/CD によるデプロイ
    現状 アプリケーションのデプロイに関しては CI/CD によるデプロイを行っているプロジェクトも増えてきていると思いますが、Linux のスクリプトや cron 設定等もバージョン管理システムで管理し、CI/CD によるデプロイを行うようにすべきです。
    The Twelve Factor App で提唱されている内容等にも繋がりますね。

  • PR/MR によるレビュー
    リリース/変更絡みでインシデントが発生した場合に、

    • 一人で作業した。
    • チェック体制がなかった。機能していなかった。
    • レビューを行っていなかった。
      ので対策として「これからはちゃんとする」。

    といった話を何度も聞いている気がしますが、この手の対策はスローガンだけ掲げてもまたそのうち元に戻る事が多い。
    常に PR/MR (プルリクエスト/マージリクエスト) を行う形にして、必ずチェックが入るようにシステム/ツールの力を借りる方が改善効果が高いです。

※ GitLab でも push 可能な権限を制限できるはず。MR必須の運用にするならば、指定ブランチへの直pushを制限する等 ( スローガンではなく ) ツールで制約を。

多重起動を防ぐ

アプリケーションを実装する場合には、処理言語系 ( Java 等 ) で多重起動を防ぐための仕組みを利用するのも可だが、Linux の標準ツールである flock を使う事で多重起動を防ぐ事が可能。

*/5 * * * *  appuser  flock -n /var/lock/cron-task.lock cron-task

実際の例を以下に示す。

スクリプト

処理内容を含むスクリプトを作る。
/root/scripts/sleep.sh を以下で作成。

#!/bin/bash

INTERVAL=300

sleep $INTERVAL

echo  "$INTERVAL sec slept"

インターバルで指定した秒数スリープした後 echo でメッセージ出力するだけ。
300 秒 ( 5分間 ) ジョブ継続するように指定。

cron 設定

cron 設定は /etc/cron.d/test ファイルを作る。

# put some comment here
* * * * * root flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh

flock で多重起動されないようにしています。
実行は 1 分間隔。

実行結果

/var/log/cron の出力

Feb  8 16:07:01 mosaos-dev CROND[7083]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:08:01 mosaos-dev CROND[7087]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:09:01 mosaos-dev CROND[7089]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:10:01 mosaos-dev CROND[7091]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:11:01 mosaos-dev CROND[7093]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:12:01 mosaos-dev CROND[7082]: (root) CMDOUT (300 sec slept)
Feb  8 16:12:01 mosaos-dev CROND[7096]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:13:01 mosaos-dev CROND[7100]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:14:01 mosaos-dev CROND[7102]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:15:01 mosaos-dev CROND[7104]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:16:01 mosaos-dev CROND[7108]: (root) CMD (flock -n /var/lock/cron-test-sleep.lock /root/scripts/sleep.sh)
Feb  8 16:17:01 mosaos-dev CROND[7095]: (root) CMDOUT (300 sec slept)

1 分間隔で cron ジョブが実行されていますが、実際の処理 ( echo ) は5分間隔になっているのが確認できます。

タイムアウトを使う

タスクの種類によってはタイムアウトを利用するのが有用な場合があります。
通常 cron で実行されるジョブは時間制限なしで実行されますが、場合によっては望ましくない場合があります。
一定間隔で実行しているジョブの場合に、一定時間で終了しないと重複してジョブ実行される等。

こういった場合 timeout ( /usr/bin/timeout ) を利用する等して、実行時間を制限できないか試してみるとよいでしょう。

*/5 * * * *  appuser  timeout 10s cron-task

flock と同様、実際の例を以下に示す。

スクリプト

#!/bin/bash

while true
do
  date
  sleep 1
done

1秒間隔で date 出力

cron 設定

/etc/cron.d/test ファイルを編集。

# put some comment here
* * * * * root timeout 10s /root/scripts/loop.sh

10秒でタイムアウト。

実行結果

Feb  8 16:49:01 mosaos-dev CROND[7261]: (root) CMD (timeout 10s /root/scripts/loop.sh)
Feb  8 16:49:01 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:01 JST)
Feb  8 16:49:02 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:02 JST)
Feb  8 16:49:03 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:03 JST)
Feb  8 16:49:04 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:04 JST)
Feb  8 16:49:05 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:05 JST)
Feb  8 16:49:06 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:06 JST)
Feb  8 16:49:07 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:07 JST)
Feb  8 16:49:08 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:08 JST)
Feb  8 16:49:09 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:09 JST)
Feb  8 16:49:10 mosaos-dev CROND[7260]: (root) CMDOUT (2022年  2月  8日 火曜日 16:49:10 JST)
Feb  8 16:50:01 mosaos-dev CROND[7287]: (root) CMD (timeout 10s /root/scripts/loop.sh)
Feb  8 16:50:01 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:01 JST)
Feb  8 16:50:02 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:02 JST)
Feb  8 16:50:03 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:03 JST)
Feb  8 16:50:04 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:04 JST)
Feb  8 16:50:05 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:05 JST)
Feb  8 16:50:06 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:06 JST)
Feb  8 16:50:07 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:07 JST)
Feb  8 16:50:08 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:08 JST)
Feb  8 16:50:09 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:09 JST)
Feb  8 16:50:10 mosaos-dev CROND[7286]: (root) CMDOUT (2022年  2月  8日 火曜日 16:50:10 JST)

10秒で処理が中断されているのが判る。タスクによっては途中終了するのは大分まずい可能性があるので、使いどころは注意する事。

運用

運用寄りのプラクティス。

権限を絞る

cron ジョブを実行する場合には実行ユーザ名を適切に指定し権限を制限する。

0 * * * *  root  cron-task

上記の crontab 設定は root ユーザで実行するように指定していますが、rootで実行された場合、例えば実行ジョブがシステムディレクトリの重要部分を破壊するものであっても、制限を行うのが困難になります。 アプリケーション毎に必要最小限の権限を有するユーザを作成し、これらユーザを指定する方がよいでしょう。

0 * * * *  appuser  cron-task

出力を捨てない

*/5 * * * *  appuser  cron-task >/dev/null 2>&1

/dev/null にリダイレクトしない。
ネットのサンプル等を見てそのまま記載した場合に、よく判らずに出力を捨ててしまっている場合があるかもしれません。
が、問題が発生した場合の手がかりを捨てる事になるので、特別な理由がない限り出力は捨てないでおきましょう。


その他

  • インシデント管理では対策は行われず対応したのみでチケットクローズしてる現場も多数見てきたのだが、その手の環境では、不注意でやっちゃいました系は今後も起こり続ける可能性が高いだろう。
    似たような問題が頻発する場合、その手の問題が発生しにくいプラットフォーム (ジョブ管理ツール) の導入を検討した方がよい。
  • 商用管理ツールは高い... という場合、オープンソースという手もある。DolphinScheduler とか。
    使ったことないけど、画面イメージ見た感じはいい感じかも。big data distributed workflow scheduling system の謳い文句通りであれば今後暫くは使えそう。
  • 今後、コンテナ化を推進するのであれば k8s の CronJob を利用する等。
    プラットフォームによって最適な選択肢は変わってくる可能性も考慮すべし。
  • ベストプラクティスがあっても、実装する個々の開発者や運用担当者が実施しなければ意味はない。
    プロジェクトや担当者レベルで対応するよりは企業全体である程度の方針を示してベストプラクティスは横展開すべき。