cron式を解明する:5つのフィールド、範囲、ステップ、曜日のORトラップ
5つのフィールドと有効な値の範囲
標準的なcron式は、左から右に読む空白区切りの5つのフィールドで構成されます:分(0–59)、時(0–23)、日(1–31)、月(1–12)、曜日(0–7)。分は常に最も左のフィールドです——cron行を右から左に読むのは一般的なミスで、意味が完全に逆になります。曜日フィールドは0と7の両方を日曜日として受け付けます。これはUnixの伝統から受け継がれた意図的な冗長性です。
Vixie cronの慣例に従う実装では、月フィールドはjanからdecの3文字の英語略語も受け付け、曜日フィールドはsunからsatを受け付けます。環境間の移植性のためには数値を使う方が安全です。Quartz Scheduler(Javaアプリケーションで使用)は最左に秒フィールドを追加し、AWS EventBridgeなどのクラウドスケジューラはオプションの7番目の年フィールドを追加します——これらの拡張フォーマットはPOSIXの5フィールド標準と互換性がありません。
ワイルドカード、カンマリスト、ハイフン範囲、スラッシュステップ
4つの特殊文字がフィールドのマッチングを制御します:アスタリスク(*)はそのフィールドのすべての有効な値にマッチします——* * * * *は毎分実行されます。カンマ(,)は離散値のリストを作成します:月フィールドの1,3,5は1月、3月、5月を意味します。ハイフン(-)は包含範囲を定義します:時フィールドの9-17は9時から17時まで(両端含む)の各時にマッチします。これら3つの文字は組み合わせることができます:曜日フィールドの1-5,0は月曜日から金曜日と日曜日にマッチします。
スラッシュ(/)はステップ値を導入します。アスタリスクの後に使用すると、分フィールドの*/15は集合{0, 15, 30, 45}を生成します——フィールドの最小値から15の倍数ごと。範囲の後に使用すると、0-30/5は{0, 5, 10, 15, 20, 25, 30}を生成します。ステップは常に範囲の左の境界(*の場合は0)から始まり、ランダムなオフセットからではありません。これが分フィールドの*/5が、cronデーモンがいつ起動されたかに関わらず、0、5、10……で実行される理由です。
日と曜日のORトラップ
cronで最も驚くべき動作は、両方の日フィールドと曜日フィールドにワイルドカード以外の値が設定されている場合に発生します。直感的には、0 2 15 * 5は「毎月15日かつ金曜日の午前2時に実行」——AND条件——を意味するように思えます。実際には、Vixie cronとdcronを含むほとんどのcron実装はこれをOR条件として扱います:タスクは毎月15日の午前2時に実行され、また日付に関わらず毎週金曜日の午前2時にも実行されます。cron(5)のマニュアルページにはこれが明示されています:「DOMとDOWの両方が指定された場合、いずれかのフィールドが現在の時刻にマッチしたときにコマンドが実行される。」
この動作は、複合的な日付条件を表現しようとする開発者を驚かせます。「毎月第3金曜日」が本当に必要な場合、cronは直接表現できません。慣用的な解決策は、毎週金曜日に実行するようにスケジュールし(0 2 * * 5)、コマンドの先頭にシェルテストを追加することです:[ $(date +\%d) -ge 15 ] && [ $(date +\%d) -le 21 ] && /path/to/script。または、カレンダー繰り返しルール(RFC 5545 RRULE)をネイティブにサポートする上位レベルのスケジューラ、例えばsystemdタイマーのOnCalendar=ディレクティブを使用します。
定義済みショートカット:@reboot、@hourly、@daily、@weekly、@monthly
Vixie cronは、一般的なスケジュールの5フィールド構文を置き換える名前付きショートカットのセットを導入しました。@rebootはcronデーモン自体の起動直後に1回タスクを実行します——軽量な起動タスクに便利ですが、本番サービスには適切なinitシステム(systemdユニットなど)が望ましいです。@hourlyは0 * * * *に相当します。@daily(エイリアス@midnight)は0 0 * * *に相当し、@weeklyは0 0 * * 0(日曜日の真夜中)、@monthlyは0 0 1 * *(毎月1日の真夜中)、@yearly(エイリアス@annually)は0 0 1 1 *(1月1日の真夜中)に相当します。
これらのショートカットはcrontabファイルの可読性を向上させますが、cronデーモンが@構文を認識することに依存します——POSIXはこれらを定義していないため、一部の最小限または組み込みcron実装では使用できません。クラウドスケジューラ(AWS EventBridge、Google Cloud Scheduler、GitHub Actions)向けにcron式を書く際は、ほとんどのクラウドプラットフォームが@ショートカットを実装しておらず、明示的な5フィールド形式が必要なため、プロバイダーのドキュメントを確認してください。
ステップ値と「今から何分ごとではない」という落とし穴
ステップ構文についてよくある誤解は、*/Nを「このジョブが作成されてからN単位ごと」として扱うことです。そうではありません。ステップは絶対的な時間値に対するモジュロフィルターです。分フィールドの*/5は集合{x : x mod 5 = 0, 0 ≤ x ≤ 59} = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}の省略形——1時間あたり12個の固定された時計の位置。cronデーモンはこれらの位置を現在の時計の分と比較します。ジョブがいつ登録されたかの記憶はありません。
これにより0 */6 * * *で微妙な問題が生じます。この式は時0、6、12、18——真夜中、午前6時、正午、午後6時——に1日ちょうど4回実行されます。午後3時にこのジョブを追加して「6時間後の午後9時に最初の実行」を期待すると驚くことになります:次の実行は午後6時(時18)です。午後9時に実行が必要な場合は、明示的なリスト0 9,15,21,3 * * *を使用するか、0 3/6 * * *で開始時間を変更します——ただし開始/ステップの表記はVixie cronの拡張機能であり、どこでも使えるわけではありません。最大の移植性のためには、常に明示的なカンマ区切りリストを使用してください。
タイムゾーン:cronスケジュールに潜む見えないバグ
従来のcronデーモンはすべてのジョブをその実行マシンのシステムタイムゾーンで実行します。UTCに設定されたサーバー——ほとんどのDockerコンテナベースイメージと多くのクラウドVMのデフォルト——では、0 8 * * 1-5はあなたの現地時間の午前8時を意味しません。UTC午前8時を意味します。チームがUTC+9(日本標準時)にいる場合、このジョブは現地時間の午後5時に実行されます。AWS CloudWatch Events(現在のEventBridge)のcron式はUTCのみと文書化されています。GitHub Actionsのon: schedule:もUTCです。
夏時間(DST)はさらに2つの故障モードをもたらします。時計が進む(例:01:59から03:00へ)と、スキップされた時間帯にスケジュールされたcronジョブはその日単純に実行されません。時計が戻る(02:59から02:00へ)と、同じ分が2回現れます。ジョブが1回実行されるか2回実行されるかはデーモンの実装に依存します。最も安全な本番戦略は、cronサーバーを常にUTCで動作させ、アプリケーション層で現地時間に変換するか、明示的なTimeZoneパラメータを受け付けるスケジューラを使用することです。例えばTimeZone=を持つsystemdタイマー、.spec.timeZoneが追加されたKubernetes 1.25以降のCronJob、またはIANAタイムゾーン名を受け付けるAWS EventBridge Schedulerです。