Cron式パーサー

Cronスケジュール式の解析と説明

minute (0-59) | hour (0-23) | day (1-31) | month (1-12) | weekday (0-6)

よく使う例

DOM/DOW の OR 罠: '0 0 13 * 5' が 13 日の金曜日にならない理由

cron で最も誤解されているルールは POSIX crontab(5) に明記されています。day-of-month フィールドと day-of-week フィールドの両方が制限されている(どちらも * でない)場合、ジョブはどちらか一方が一致した時点で実行されます。したがって 0 0 13 * 5 は毎月 13 日の深夜と毎週金曜日の深夜の両方に発火します。年に 1〜3 回の「13 日の金曜日」を期待していたのに、実際には年間およそ 60 回実行されるのです。2 つのトリガー条件はどちらも単体ではもっともらしく見えるため、このバグはコードレビューを普通に通過し、「月次」レポートが先週 4 回届いた理由を誰かが尋ねたときに初めて表面化します。 AND の意味論を確実に得る方法は、2 つのフィールドのうち片方だけを式で制限し、もう片方をコマンド内で検証することです。day-of-month を 13 に固定し、シェルに曜日を確認させます。date +%u は ISO 曜日(1 = 月曜、7 = 日曜)を出力するので、5 と比較すれば金曜日だけを選べます。ジョブは最大でも年 12 回の実行になり、金曜日でない 13 日にはガードが静かに終了します。 このコマンドラインにはもう 1 つの罠が潜んでいます。crontab のエントリでは、エスケープされていない % は特殊文字です。最初の % でコマンドが打ち切られ、それ以降の内容はコマンドの標準入力として渡され、さらに後続の % は改行に変換されます。date +%Y-%m-%d は対話シェルでは完璧に動きますが、cron ではジョブを黙って切り詰めます。すべての % を \% にエスケープするか、日付フォーマットをスクリプト側へ移して cron はそのスクリプトを呼ぶだけにしてください。「cron は動いたのに何も起きなかった」というチケットの多くは、この 1 文字に行き着きます。
# OR semantics: fires every Friday AND every 13th (~60x/year)
0 0 13 * 5 /opt/report.sh

# AND semantics: pin DOM, test DOW in the command (max 12x/year)
0 0 13 * * [ "$(date +\%u)" = 5 ] && /opt/report.sh

# Unescaped % silently truncates the command line
0 1 * * * echo "backup-$(date +\%Y\%m\%d)" >> /var/log/runs.log

夏時間(DST): cron ジョブが死ぬ場所

時計が前に進むタイムゾーンで 02:30 にジョブを設定すると、壁時計は 02:00 から一気に 03:00 へジャンプし、ジョブが実行されるべき時刻はその夜には存在しなくなります。その後の挙動は実装によって異なります。vixie-cron とその後継の cronie はジャンプを検知し、飛ばされたジョブを遷移直後に実行しますが、コンテナに入っているミニマルな cron クローンの多くは完全にスキップします。時計が戻る秋はさらに厄介です。01:00〜02:00 の 1 時間が 2 回発生し、素朴なスケジューラーは両方でジョブを発火させます。そのジョブが請求書を発行したりカードに課金したりするなら、年に 1 回、顧客への二重請求が起きることになります。 防御策は 2 つです。ローカル時刻のスケジュールを安全な時間帯(03:00 以降かつ 02:00 より前)に収めて遷移の影響を受けないようにするか、スケジューラー自体を遷移が存在しない UTC で動かすかです。 ホスト型スケジューラーには独自の意味論が加わります。GitHub Actions の schedule: cron は UTC のみで解釈され、負荷が高いときには実行が数分遅れたり完全に欠落したりし得るとドキュメントに明記されています。毎時 0 分のような人気の分が最も影響を受けるので、17 分や 43 分のような半端な分を選びましょう。Kubernetes CronJob は 1.27 で spec.timeZone フィールドが stable になりました。あわせて startingDeadlineSeconds(開始がどこまで遅れても許容されるか — 期限を過ぎるとその実行は missed として扱われる)と、前のジョブがまだ実行中のときに次のジョブと共存させる Allow(デフォルト)、ブロックする Forbid、殺して置き換える Replace を選ぶ concurrencyPolicy も設定してください。
# Kubernetes CronJob with explicit time zone (stable in 1.27)
apiVersion: batch/v1
kind: CronJob
spec:
  schedule: "30 4 * * *"
  timeZone: "Asia/Seoul"
  startingDeadlineSeconds: 300
  concurrencyPolicy: Forbid

*/N は「N ごと」ではない: ステップ値の本当の挙動

ステップ値 */N は「このフィールドの範囲内で、範囲の先頭から N 刻みの値」に展開されます。「経過時間 N 単位ごと」ではありません。分フィールドでは 2 つの解釈がたまたま一致します。*/15 は 0,15,30,45 になり、1 時間はちょうど 60 分なので間隔は均一です。day-of-month で幻想が崩れます。1-31/10 は 1 日、11 日、21 日、31 日に展開されますが、翌月はまた 1 日から始まるため、「31 日の翌日に 1 日」という 2 日連続の実行が起きます。 つまり本当の「10 日ごと」は cron では表現不可能です。日付カウンターは月の境界ごとにリセットされ、cron は前回いつ実行したかを覚えていません。同じ理由で 0 0 */2 * *(「1 日おき」)は 1,3,...,29,31 日に実行された後、翌月 1 日にも実行されます。31 日まである月では 2 日連続の実行になり、これは年に 7 回起こります。 月境界をまたぐ固定間隔が本当に必要なら、ジョブを毎日実行してスクリプト内で間引いてください。epoch 日数を N で割った剰余を比較するか、前回実行のタイムスタンプをファイルに保存して早期終了します。あるいは systemd タイマーの monotonic スケジューリング(OnUnitActiveSec=10d)なら前回の起動時点から数えるので、カレンダーを完全に無視できます。
*/15 * * * *      # minutes 0,15,30,45 — uniform, as expected
0 0 1-31/10 * *   # days 1,11,21,31 — NOT "every 10 days"
0 0 */2 * *       # 1,3,...,29,31 then the 1st: twice in a row

# real "every 10 days": run daily, gate on the epoch day
0 3 * * * [ $(( $(date +\%s) / 86400 \% 10 )) -eq 0 ] && /opt/task.sh

1 つの構文、多くの方言: Quartz、EventBridge、systemd タイマー

5 フィールドの POSIX 形式(分・時・日・月・曜日)は基準線にすぎません。無数の Java サービスに組み込まれているスケジューラー Quartz(Spring の @Scheduled もそうです)は、秒が先頭、オプションの年が末尾に付く 6〜7 フィールドを使い、DOM と DOW の両方が具体値の式を拒否します。どちらか一方は「特定の値なし」を意味する ? でなければなりません。その代わりに本当に便利な演算子が手に入ります。L は月の最終日(6L なら最後の金曜日)、W は指定日に最も近い平日(15W)、# は第 n 曜日で MON#2 なら第 2 月曜日 — 素の cron ではそもそも表現できないものばかりです。 AWS EventBridge は年フィールド(1970-2199)で終わる独自の 6 フィールド方言を話し、Quartz と同様に DOM と DOW の同時 * を拒否します。そのため crontab の行を EventBridge ルールにコピーすると、片方のフィールドを ? に替えるまでバリデーションに落ちることがよくあります。 古典的な cron は @reboot、@daily、@hourly といったニックネームも受け付けます。便利ですが非標準でサポートもまちまちなので、移植性が要る場面では避けてください。秒単位の分解能や取りこぼしの追い付き(catch-up)が必要なら、systemd タイマーが現代的な答えです。OnCalendar は秒を受け付け(Mon..Fri 09:00:30)、Persistent=true は最後のトリガー時刻を記録して次回起動時に逃した実行を発火させます。cron に欠けていることで有名な挙動であり、スケジュール時刻に眠っているノート PC のために anacron が存在する理由でもあります。
0 9 * * 1-5            # POSIX: weekdays at 09:00
0 0 9 ? * MON-FRI      # Quartz: seconds first, ? required in DOM
0 0 9 ? * MON#2        # Quartz: second Monday of the month
0 9 ? * MON-FRI *      # EventBridge: year field appended

# systemd timer unit equivalent
OnCalendar=Mon..Fri 09:00:00
Persistent=true        # run missed jobs after downtime

運用の衛生学: 多重実行、消える出力、静かに止まるジョブ

cron には追い付き実行がありません。ジョブが実行されるべき時刻にホストが落ちていたりデーモンが止まっていたりすれば、その実行は失われます。キューもリトライもありません。1 回の欠落が問題になるジョブ(ログローテーション、証明書更新、請求処理)は、Persistent=true を使う systemd タイマーへ移すか、ジョブ自身のロジックが空白期間に耐えられるようにしてください。 次の驚きは出力です。cron はジョブが stdout や stderr に書いたものすべてを、ローカル MTA 経由で crontab の所有者にメールします。MTA が設定されていない現代のサーバーでは、その出力はただ消えます。本当に通知が欲しければ MAILTO を設定し、そうでなければ >> /var/log/job.log 2>&1 で明示的にリダイレクトしてください。そしてそのファイルはローテーションを。cron は何年でも喜んで追記し続けます。 遅いジョブが次回の起動と重なるのは、古典的なデータ破壊の原因です。同じディレクトリへの rsync が 2 本、同じデータベースへのマイグレーションが 2 本。コマンドを flock -n /path/lock で包めば、2 つ目のインスタンスは積み上がる代わりに即座に終了します。thundering herd にも注意してください。世界中の crontab の途方もない割合が 0 0 * * * と書かれているため、共有データベースや API は深夜 0 時ちょうどに叩かれます。cronie は RANDOM_DELAY を尊重し、systemd には RandomizedDelaySec があり、ホスト名のハッシュから分を導出してフリート全体を自然に分散させる手もあります。 最後に、最も危険な故障モードは静かに動かなくなったジョブです。誤った編集、無効化されたアカウント、満杯のディスク。cron は教えてくれません。監視を反転させて dead-man's-switch を使いましょう。ジョブは成功のたびに URL へ ping を送り(healthchecks.io のパターン)、何かがエラーを報告したときではなく、ping が届かなくなったときにアラートが鳴るようにするのです。
[email protected]
RANDOM_DELAY=15    # cronie: spread the start by 0-15 minutes

# no overlap, logged output, dead-man's-switch ping
17 3 * * * flock -n /run/backup.lock /opt/backup.sh \
  >> /var/log/backup.log 2>&1 \
  && curl -fsS https://hc-ping.com/UUID > /dev/null
最終更新:

ツールについて

cron 式パーサーは Unix cron / GitHub Actions / Kubernetes CronJob などで使われる 5 フィールド構文を自然言語に翻訳し、次回実行時刻も表示します。フィールドは分・時・日・月・曜日で、ワイルドカード(*)、範囲、リスト、ステップが利用できます。

使い方

  1. Type a cron expression like 0 9 * * 1-5 into the input.
  2. Read the human-readable description that appears below — it confirms what your expression actually means.
  3. Inspect the next execution times to verify edge cases (month boundaries, leap years, etc.).
  4. Click any example expression to load it as a starting point.
  5. Copy the validated expression into your crontab, GitHub Actions workflow, or Kubernetes manifest.

主な使用例

  • Scheduling a daily database backup at 3 AM (0 3 * * *).
  • Running a CI workflow every Monday morning (0 9 * * 1).
  • Triggering a cleanup job every 15 minutes (*/15 * * * *).
  • Sending a weekly digest email Friday afternoon (0 17 * * 5).
  • Defining a Kubernetes CronJob to rotate logs at midnight on the first of each month.
  • Validating a tricky expression that uses ranges and steps before deploying it.

よくある質問

Q. Why does my expression run at unexpected times?

A. Cron interprets day-of-month and day-of-week as OR (not AND) when both are restricted. * * 15 * 1 fires on the 15th OR every Monday — not just Mondays falling on the 15th.

Q. What time zone does cron use?

A. Whatever the host or container is configured for. GitHub Actions cron runs in UTC. Kubernetes CronJobs default to the cluster controller manager time zone unless you set spec.timeZone.

Q. Are 6-field cron expressions supported?

A. Some schedulers (Quartz, AWS EventBridge) add a seconds or year field. This parser uses the standard 5-field POSIX format.

Q. How do I run a job once at a specific time?

A. Cron itself does not have a one-shot mode. Use systemd timers, an at job, or an event-based trigger instead.