Cron 式の読み方(そして思ったとおりに動く式の書き方)
Cron は決まった位置に並ぶ五つのフィールドと、四つの記号にすぎない。どんなスケジュールもひと目で読めるようになり、正しく見える式を誤った時刻に動かす二つの罠を避けよう。
プルリクエストに */15 9-17 * * 1-5 の一行が追加され、レビューは誰も声に出して答えられない問いで止まる。これは結局いつ動くのか。「15 分ごと?」と誰かが当て、「日中の、平日、たぶん」と別の誰かが足す。どちらも近いが、確信は誰も持っていない。
Cron 式は暗号のように見えるが、覚えることはほとんどない。決まった位置に並ぶ五つのフィールドと、四つの記号だ。位置さえ読めれば、*/15 9-17 * * 1-5 は同じ cron 方言の中ではただ一つの意味しか持たず、しかも毎回そのとおりに動く。人がつまずくのは構文ではない。正しく読めるスケジュールが誤った時刻に動く、二つの罠のほうだ。どちらも後で扱う。
五つのフィールド、位置がすべて
標準の cron 行は、スペースで区切った五つのフィールドだ。意味は置かれた位置だけで決まる。ラベルはないので、30 6 と 6 30 は別の時刻であって、同じ時刻の二通りの書き方ではない。
左から右へ:
| 位置 | フィールド | 範囲 |
|---|---|---|
| 1 | 分 | 0–59 |
| 2 | 時 | 0–23 |
| 3 | 日 | 1–31 |
| 4 | 月 | 1–12 |
| 5 | 曜日 | 0–7(0 と 7 はどちらも日曜) |
だから 30 6 * * * は「6:30」だ。分 30、時 6、あとの三つの星は任意の日・任意の月・任意の曜日を表す。cron を読むコツは、この五つの枠を順にたどり、一つずつ声に出すことだ。何分・何時・何日・何月・何曜日に動くのか、と。
このうち二つのフィールドは意外な形で干渉し合い、それが最初の大きな罠になる。ひとまず覚えておいて、下の「日付フィールド」の節で詳しく見る。
たいていのパーサーでは、月と曜日は三文字の名前も受け付ける。JAN–DEC と SUN–SAT だ。0 9 * * MON は 0 9 * * 1 より読みやすく、名前で書けばプラットフォーム間の番号の違いも避けられる(後述)。
四つの記号がすべてを担う
どのフィールドも同じ四つの記号を使う。まずは、この四つを押さえておけば基本形は読める。
*(星):任意の値。時フィールドの*は毎時、五つの星なら毎日毎分動く。,(カンマ):値の並び。分フィールドの0,30は毎時の 0 分と 30 分。MON,WED,FRIはその三日だ。-(ハイフン):範囲。時フィールドの9-17は 9 時から 17 時まで、17 時も含む。MON-FRIは平日だ。/(スラッシュ):ステップ。分フィールドの*/15は 15 分ごと、つまり 0、15、30、45。範囲の中でも刻める。9-17/2は 9 時から 17 時まで、2 時間おきだ。
最後の一つがいちばん誤解を生むので、はっきりさせておく。
5 * * * * # 毎時 5 分、1 時間に 1 回
*/5 * * * * # 5 分ごと、1 時間に 12 回
5 は一つの点だ。5 分、それも 5 分だけ。*/5 はすべての値をステップで進む。0、5、10……。片方は点、片方はリズムだ。取り違えると、「5 分ごと」が気づかぬうちに「1 時間に 1 回」に化ける。
同じ間違いの、もっと分かりにくい版もある。ステップはフィールドの範囲の先頭から数える。「今」からではない。しかもその範囲は毎時リセットされる。だから分フィールドの */35 は「35 分ごと」ではない。0 分と 35 分に動き、次の時に入るとまた 0 から始まる。つまり「35 分あけて、次は 25 分」がずっと交互に続く。ステップが範囲を割り切れなければ必ずこうなる。本当に 35 分間隔がほしいなら、一つの cron フィールドでは表せない。
冒頭の例もこれで読める。*/15 9-17 * * 1-5:15 分ごと、9 時から 17 時の間、任意の日、任意の月、曜日 1 から 5、標準 cron では月曜から金曜だ。17 時は含まれる点に注意。だから 17:45 まで動き続ける。09:00 から 17:45 までであって、17:00 で止まるわけではない。頭の中で換算したくなければ、Cron 式ツール に貼ればいい。平易な説明を出し、各フィールドを有効範囲つきで分解し、選んだタイムゾーンで次に動く時刻を並べてくれる。既定は 5 件、最大 50 件だ。
数値の曜日は移植できない。標準 cron と GitHub Actions は
0から数え、0が日曜だ。ところが Cloudflare Workers は1から数え、1が日曜から7が土曜まで、Kubernetes は0が日曜だ。だから同じ1-5でも、Linux crontab では月曜から金曜、Cloudflare では日曜から木曜になる。プラットフォームが受け付けるなら曜日は名前で書けばいい。MON-FRI、FRIのように。そうすれば番号の違いは気にしなくてよい。
日付フィールドの罠:「日」と「曜日」が同時に出るとき
多くの人が一度ははまる落とし穴だ。次の行はいつ動くだろうか。
0 0 13 * FRI
0 分、0 時、13 日、任意の月、金曜。「毎月 13 日の深夜、しかも金曜なら」に見える。違う。標準の Unix cron、つまり多くの Linux crontab が使う Vixie/ISC 系では、「日」と「曜日」の両方が制限されている、どちらも * ではないとき、cron はそのどちらかが一致した時点で動く。だから 0 0 13 * FRI は毎月 13 日の深夜に動き、さらに毎週金曜の深夜にも動く。「かつ」ではなく「または」だ。
一文でいうと、片方の日付フィールドが * ならもう片方がそのまま効く。だが両方に値を書いた瞬間、両者は「かつ」ではなく「または」で結ばれる。だから「13 日かつ金曜」を表す簡単な cron 行は存在しない。二つの条件を同時には要求できないのだ。
この二つのフィールドは実装ごとの食い違いが最も大きい場所でもある。だから「または」は普遍の保証ではなく、よくある既定とみなしておくのがよい。両方の日付フィールドに値があるときは、対象プラットフォームのドキュメントを確認しよう。ツール は両フィールドが制限されているのを見つけると専用の注意を出すので、噛まれる前に気づける。
もう一つの罠:どのタイムゾーンで動くのか
cron 式の中にタイムゾーンは入っていない。0 9 * * * は「9 時」だが、どこの 9 時だろうか。答えは式の外で決まる。ここを間違えると、「朝 9 時」のジョブが深夜 2 時に人を叩き起こす。
昔ながらの crontab はサーバーのローカルタイムゾーンで動く。Cronie 互換の crontab なら、ファイルの先頭に CRON_TZ を置いてスケジュールのタイムゾーンを指定できる。ただ、普通の TZ 環境変数を設定すればスケジュールのタイムゾーンが変わる、と思い込んではいけない。そしてどちらの接頭辞も Kubernetes の spec.schedule の中には入れないこと。Kubernetes はそれを拒否し、別の spec.timeZone フィールドを使う。サーバーを引っ越すか、データセンターの既定が UTC なら、同じ行が違う時刻に動く。多くのマネージドスケジューラはこの曖昧さを避けるため、すべてを UTC に固定する。GitHub Actions の schedule: トリガーも Cloudflare Workers の cron トリガーも、式を UTC で解釈する。例外はない。
夏時間はもう一層を加える。ただしそれは「ローカルの壁時計時刻で照合する」実装、つまり Vixie/Cronie 系の crontab に限る。そこでは深夜 2:30 に設定したジョブは、春の切り替え日には動かない。その壁時計時刻が存在しないからだ。秋の切り替え日には二度動きうる。UTC ベースのプラットフォームにはそもそもローカルの夏時間の飛びがなく、変わるのは同じ実行を自分のゾーンに換算して表示したときの時刻だけだ。これが実際に影響しそうなら、頭の中で計算しないこと。ツール でタイムゾーンを選べば、次の各実行をそのゾーンと UTC の両方で並べてくれるので、夏時間のずれもひと目で分かる。そもそも UTC・オフセット・ゾーンの違いについては、UTC、GMT、ISO 8601、Unix タイムスタンプの違いは? を見てほしい。
五つのフィールド、六つのフィールド、そして @daily などの短縮形
伝統的な Unix cron は五つのフィールドだ。スケジューラによっては先頭に秒のフィールドを足す。Spring や多くのプログラミング言語の cron ライブラリは六フィールド形式を使い、Quartz は六つの必須フィールドに加えて任意の七つ目「年」を持つ。だから */30 * * * * *(六フィールド)は「30 秒ごと」、*/30 * * * *(五フィールド)は「30 分ごと」だ。何よりまずフィールド数を数えること。先頭に一つ増えるだけで、後ろのフィールドの意味がすべてずれる。
Cronie や Vixie 系の crontab は、五つのフィールドをまるごと置き換える名前つきの短縮形も理解する。
@yearly # 0 0 1 1 * 年に一度、1 月 1 日の深夜
@monthly # 0 0 1 * * 毎月 1 日の深夜
@weekly # 0 0 * * 0 毎週日曜の深夜
@daily # 0 0 * * * 毎日深夜(@midnight も同じ)
@hourly # 0 * * * * 毎時ちょうど
@reboot # 起動時に一度
便利だが万能ではない。一部の crontab 実装だけが対応する拡張だ。とりわけ @reboot は「起動」というライフサイクルを前提にするので、それを持たないマネージドスケジューラはたいてい受け付けない。拒否されるか、そもそも対応していない。短縮形をツールで調べたいときは、まず五フィールド形式に展開しよう。ツールが解析するのは五・六フィールドの数値構文なので、0 0 * * * ならフィールド分解と次回実行の一覧が出るが、@daily 単体では出ない。
? L W # とそのほかの方言拡張
?、L、W、# を見かけたら、それは汎用 cron ではなく方言ごとの拡張構文だ(? は「指定なし」、L は「最後」、W は「最も近い平日」、# は「その月の第何何曜日」)。Quartz と結びつけて語られることが多いが、Quartz 専用ではない。たとえば Cloudflare Workers は L・W・# の一部に対応する。しかも同じ文字でも意味が違いうる。Kubernetes は ? を受け付けるが、単に * として扱い、Quartz の「指定なし」ではない。だから意味を一つに決めてかからず、使う予定のプラットフォームのドキュメントに従うこと。
これこそ本稿の本当の要点だ。構文はやさしい。だが意味を決めるのは「方言」のほうだ。一行を投入する前に、実際に動く場所と突き合わせておこう。
| 対象 | フィールド数 | タイムゾーン | 曜日番号 | 拡張 |
|---|---|---|---|---|
| Linux / Cronie | 5 | サーバー、または CRON_TZ | 0–7、0 と 7 は日曜 | @ 短縮形 |
| GitHub Actions | 5 | UTC | 0–6、0 は日曜 | 秒なし |
| Cloudflare Workers | 5 | UTC | 1 は日曜……7 は土曜 | L W # の一部 |
| Kubernetes CronJob | 5 | .spec.timeZone | 0 は日曜 | ? は * として扱う |
| Quartz | 6–7 | スケジューラ設定 | 方言による | ? L W # |
表に収まらない実用的な注意が二つ。GitHub Actions のスケジュールはベストエフォートで、負荷が高いと遅れることがあるので、精密なタイマーとしては使わないこと。そして Windows には cron が存在しない。タスクスケジューラか schtasks を使う。
読み方チェックリスト
cron の一行が目の前に来たら、次のとおり。
- まずフィールドを数える。五つが標準、六つなら先頭は秒、
@dailyのような短縮形は別物だ。 - 五つの位置を順に読む。分、時、日、月、曜日を一つずつ声に出す。
- 記号を展開する。
*は毎回、,は並び、-は範囲、/はステップ。5(点)は*/5(リズム)ではない。 - 二つの日付フィールドを確認する。「日」と「曜日」の両方に値があれば、標準 cron は「または」で扱い、どちらか一致すれば動く。実際は対象プラットフォーム次第だ。
- 実行環境がどのタイムゾーンを使うか確かめる。サーバーローカルか UTC か。式そのものは何も言わないからだ。
これだけで、どの cron 行もひと目で読めるようになる。計算せず確かめたいとき、とくに日付フィールドとタイムゾーンの罠は、式を Cron 式ツール に入れればいい。平易な説明、各フィールドの範囲、そして選んだタイムゾーンでの次回実行時刻が出る。
そして、正しいと確かめた行が、それでも何もしないなら、それは別の問題だ。構文ではなく、環境、crontab の置き場所、あるいはホストそのもの。その半分の実地ガイドが「なぜ cron ジョブが動かないのか」だ。