32blogby StudioMitsu

cron & systemd timer実践ガイド

cronとsystemd timerをゼロから解説。crontabの5フィールド構文、@rebootショートカット、.timer/.serviceファイルの作り方、OnCalendar式、本番で使えるパターンまで。

21 min read
目次

cronは指定した時間にコマンドを自動実行する。毎分、毎週火曜の3時、月初の深夜0時——なんでもスケジューリングできる。systemd timerは同じことをやりつつ、ログ管理・依存関係・実行漏れのリカバリまでカバーする。この2つがあれば、Linuxでの定期実行はほぼ全部まかなえる。

要するに、cronはcrontabファイルに5フィールドの時刻指定(分 時 日 月 曜日)とコマンドを書くだけ。systemd timerは .timer.service の2ファイル構成で、カレンダー指定・起動後指定の両方に対応し、journaldでログを一元管理できる。サクッと設定するならcron、本番で監視したいならsystemd timer。

cronの基本:5フィールド構文

cronジョブは crontab ファイルの1行で定義する。書式はこう:

bash
# ┌───────── 分 (0-59)
# │ ┌─────── 時 (0-23)
# │ │ ┌───── 日 (1-31)
# │ │ │ ┌─── 月 (1-12 または jan-dec)
# │ │ │ │ ┌─ 曜日 (0-7, 0と7=日曜, または sun-sat)
# │ │ │ │ │
  * * * * * 実行するコマンド

各フィールドで使える記号:

記号意味
*すべての値* * * * * = 毎分
,リスト1,15,30 * * * * = 1分、15分、30分に実行
-範囲0 9-17 * * * = 9時〜17時の毎時0分
/間隔*/5 * * * * = 5分ごと

組み合わせも自由にできる。0 */2 * * mon-fri なら「平日、2時間ごとの0分に実行」。

僕がよく使うパターン:

bash
# 毎日 3:30 AM にバックアップ
30 3 * * * /home/omitsu/scripts/backup-db.sh

# 毎週月曜 9:00 AM にレポート生成
0 9 * * 1 /home/omitsu/scripts/weekly-report.sh

# 平日の業務時間中、15分ごとに稼働チェック
*/15 9-18 * * mon-fri /home/omitsu/scripts/check-uptime.sh

# 毎月1日の深夜0時にクリーンアップ
0 0 1 * * /home/omitsu/scripts/monthly-cleanup.sh

# 6時間ごとにデータ同期
0 */6 * * * /home/omitsu/scripts/sync-data.sh

@ショートカット:5フィールドの省略記法

5フィールドを全部書かなくても、@ プレフィックスで簡潔に書ける:

ショートカット等価表現いつ実行されるか
@rebootシステム起動時に1回
@hourly0 * * * *毎時0分
@daily0 0 * * *毎日深夜0時
@weekly0 0 * * 0毎週日曜の深夜0時
@monthly0 0 1 * *毎月1日の深夜0時
@yearly0 0 1 1 *毎年1月1日の深夜0時

@reboot は実際かなり便利で、systemdのサービスファイルを書くほどでもない監視スクリプトやSSHトンネルの自動起動に使っている。

cronジョブの管理と落とし穴

crontabコマンド

bash
crontab -e          # crontabを編集($EDITORが開く)
crontab -l          # 現在の設定を表示
crontab -r          # crontab全体を削除(注意!)
crontab -u omitsu -l  # 他ユーザーのcrontab表示(root権限必要)

システム全体のcrontabは /etc/crontab にあり、時刻フィールドとコマンドの間にユーザー名が入る:

bash
# /etc/crontab — ユーザー名フィールドあり
*/5 * * * * root /usr/local/bin/system-health-check.sh

crontab -e で編集するユーザー個別のcrontabにはユーザー名は不要。

環境変数の罠

cronは最小限の環境で実行される。ターミナルで動くスクリプトがcronで動かないのは大体これが原因だ。crontabの先頭で環境変数を設定できる:

bash
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=omitsu@32blog.com

# MAILTOを空にするとメール送信なし
# MAILTO=""

30 3 * * * /home/omitsu/scripts/backup.sh

特に重要な変数:

  • SHELL: コマンドを実行するシェル。デフォルトは /bin/sh(bashではない)
  • PATH: 実行ファイルの検索パス。cronのデフォルトPATHは極端に限定されている。絶対パスを使うか、PATHを明示的に設定する
  • MAILTO: stdout/stderrの送信先。空文字列ならメールなし
  • CRON_TZ: このcrontabのタイムゾーンを上書き(対応していないディストロもある)

cronのログを確認する

ディストリビューションによってログの場所が違う:

bash
# Debian/Ubuntu
grep CRON /var/log/syslog

# RHEL/CentOS/Fedora
grep CRON /var/log/cron

# systemdベース(journald)
journalctl -u cron.service --since "1 hour ago"

cronのよくある落とし穴

1. PATHの問題。 ターミナルでは動くのにcronで動かない。cronは .bashrc.profile も読み込まないので、コマンドは絶対パスで書く。python3 ではなく /usr/bin/python3

2. パーミッション。 スクリプトに実行権限(chmod +x script.sh)が必要。また、スクリプトが読み書きするファイルにcron実行ユーザーがアクセスできるか確認する。

3. 「日」と「曜日」の同時指定。 日フィールドと曜日フィールドを両方指定すると、どちらかの条件に合えば 実行される(AND条件ではなくOR条件)。0 0 15 * fri は「15日かつ金曜日」ではなく「15日に実行、さらに毎週金曜にも実行」という意味になる。

4. ジョブの重複実行。 ジョブに10分かかるのに5分間隔で設定すると、前のインスタンスが終わる前に次が始まる。flock で排他制御する:

bash
*/5 * * * * flock -n /tmp/my-job.lock /home/omitsu/scripts/slow-job.sh

systemd timer:モダンな代替手段

systemd timerは2つのファイルで構成される:

  1. .timer ユニット — いつ実行するか
  2. .service ユニット — 何を実行するか

慣例として同じベース名を使う。backup.timerbackup.service をトリガーする。

初めてのtimer:毎日のバックアップ

まずserviceファイル(実行する処理の定義):

ini
# /etc/systemd/system/backup.service
[Unit]
Description=Daily database backup

[Service]
Type=oneshot
ExecStart=/home/omitsu/scripts/backup-db.sh
User=omitsu

次にtimerファイル(スケジュールの定義):

ini
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 3:30 AM

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true

[Install]
WantedBy=timers.target

有効化して開始:

bash
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

これだけ。Persistent=true は「3:30 AMにシステムが停止していた場合、起動後すぐに実行する」という意味。cronにはこの機能がない。

モノトニックタイマーとリアルタイムタイマー

systemd timerには2種類ある:

リアルタイムタイマーOnCalendar= で特定の時刻に実行する(cronと同じ考え方):

ini
[Timer]
OnCalendar=Mon..Fri *-*-* 09:00:00

モノトニックタイマー はイベントからの相対時間で実行する——起動後、アクティベーション後、前回のサービス完了後:

ini
[Timer]
# 起動5分後
OnBootSec=5min

# timerがアクティブになってから1時間後
OnActiveSec=1h

# 前回のサービス完了30分後
OnUnitInactiveSec=30min

モノトニックタイマーは、cronでは実現できない「35分ごと」問題を解決する。OnUnitInactiveSec=35min なら「前回完了から35分後」なので、重複実行もカレンダー計算の面倒もない。

1つのtimerファイルにモノトニックとリアルタイムのトリガーを混在させることもできる。たとえば OnBootSec=5minOnCalendar=daily を同時に指定すれば「起動5分後に実行、かつ毎日深夜0時にも実行」になる。

OnCalendar構文とスケジュール例

OnCalendar のフォーマットはcronの5フィールドよりも表現力が高い:

曜日 年-月-日 時:分:秒

各パートは省略可能。cronとの対応表:

cron表現OnCalendar表現意味
0 * * * **-*-* *:00:00 または hourly毎時0分
0 0 * * **-*-* 00:00:00 または daily毎日深夜0時
0 0 * * 0weekly または Sun *-*-* 00:00:00毎週日曜
0 0 1 * *monthly または *-*-01 00:00:00毎月1日
0 9 * * 1-5Mon..Fri *-*-* 09:00:00平日9時
*/15 * * * **-*-* *:00/15:0015分ごと
0 0 1 1 *yearly または *-01-01 00:00:00毎年1月1日

もっと複雑な例:

ini
# 平日の9時と18時
OnCalendar=Mon..Fri *-*-* 09,18:00:00

# 四半期ごと(1月、4月、7月、10月の1日)
OnCalendar=*-01,04,07,10-01 00:00:00

# 月末? systemdには「月の最終日」の指定はない。
# 回避策:28-31日に実行して、スクリプト内で月末かチェック。

systemd-analyzeで事前検証

timerをデプロイする前に、式が正しいか確認できる:

bash
$ systemd-analyze calendar "Mon..Fri *-*-* 09:00:00"
  Original form: Mon..Fri *-*-* 09:00:00
Normalized form: Mon..Fri *-*-* 09:00:00
    Next elapse: Mon 2026-03-23 09:00:00 JST
       (in UTC): Mon 2026-03-23 00:00:00 UTC
       From now: 14h left

$ systemd-analyze calendar "hourly" --iterations=5
  Original form: hourly
Normalized form: *-*-* *:00:00
    Next elapse: Mon 2026-03-23 20:00:00 JST
       Iter. 2: Mon 2026-03-23 21:00:00 JST
       Iter. 3: Mon 2026-03-23 22:00:00 JST
       Iter. 4: Mon 2026-03-23 23:00:00 JST
       Iter. 5: Tue 2026-03-24 00:00:00 JST

これはsystemd timerの圧倒的な利点の1つ。有効化する前に「いつ発火するか」を正確に確認できる。

ポイント:テスト時は必ず --iterations=5 を付ける。次の5回の発火時刻を見れば、オフバイワンエラーやタイムゾーンの罠を事前に発見できる。

cron vs systemd timer:どっちを使う?

機能cronsystemd timer
セットアップの手間crontabに1行2ファイル(.timer + .service)
ログsyslog/メールjournald(ユニット単位で絞り込み可)
実行漏れ消えるPersistent=true で復旧
起動後実行@reboot のみOnBootSecOnStartupSec
間隔ベース不可OnUnitInactiveSec
重複防止手動(flock)Type=oneshot で標準対応
リソース制限なしcgroup完全対応(CPU・メモリ・IO)
依存関係なしAfter=Requires=Wants=
ランダムジッタRANDOM_DELAY(限定的)RandomizedDelaySec
ユーザーレベルcrontab -e~/.config/systemd/user/

cronを使うとき:

  • 30秒で設定を終わらせたいとき
  • systemdがない環境(コンテナ、古いディストロ、macOS)
  • 監視不要のちょっとした自動化

systemd timerを使うとき:

  • ログを検索可能にしたいとき(journalctl -u backup.service
  • システム停止中にスケジュールが来ても実行したいとき
  • リソース制限が必要なとき(暴走したバックアップにRAMを食い尽くされたくない)
  • 他のサービス(ネットワーク、DB、マウントポイント)への依存がある場合
  • 「前回完了から30分後」のような間隔ベースのスケジューリング

32blogの運用では、軽いチェック(死活監視、証明書期限アラート)にはcron、DB操作やデプロイパイプラインに関わるものにはsystemd timerを使っている。判断基準はシンプルで、「深夜2時にこれをデバッグする可能性があるか?」。あるならsystemd timer。journalctl -u my-service.service はsyslogを掘り返すより圧倒的に楽だから。

本番で使えるパターンとデバッグ

パターン1:重複実行の防止

cronでは flock が必要だけど、systemdなら Type=oneshot が標準で重複を防ぐ。さらに細かく制御するなら:

ini
[Service]
Type=oneshot
# 1時間以上かかったら強制終了
TimeoutStartSec=3600
# 一時的な失敗時の再起動
Restart=on-failure
RestartSec=60

パターン2:失敗通知

cronは失敗時にメールを送る(メールが設定されていれば)。systemdはもっと柔軟にできる:

ini
# /etc/systemd/system/backup.service
[Unit]
Description=Daily database backup
OnFailure=notify-failure@%n.service

汎用の失敗通知サービスを作っておく:

ini
# /etc/systemd/system/notify-failure@.service
[Unit]
Description=Send failure notification for %i

[Service]
Type=oneshot
ExecStart=/home/omitsu/scripts/notify-slack.sh "Unit %i failed on %H"
User=omitsu

これで、どのサービスでも OnFailure=notify-failure@%n.service を追加するだけでSlack通知が飛ぶ。

パターン3:分散システムでのランダム遅延

50台のサーバーが全部3:00 AMにバックアップを実行したら、バックアップサーバーがパンクする。ジッタを追加:

ini
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=1800
# 3:00〜3:30 AMの間でランダムに実行

パターン4:ユーザーレベルのtimer(root不要)

root権限なしでもtimerは作れる。ユーザーレベルのsystemdを使う:

bash
mkdir -p ~/.config/systemd/user/
ini
# ~/.config/systemd/user/sync-notes.timer
[Unit]
Description=Sync notes every 30 minutes

[Timer]
OnUnitInactiveSec=30min
OnBootSec=5min

[Install]
WantedBy=timers.target
ini
# ~/.config/systemd/user/sync-notes.service
[Unit]
Description=Sync notes with remote

[Service]
Type=oneshot
ExecStart=%h/scripts/sync-notes.sh
bash
systemctl --user daemon-reload
systemctl --user enable --now sync-notes.timer
systemctl --user list-timers

デバッグチェックリスト

スケジュールしたジョブが動かないとき:

cronの場合:

  1. cronが動いているか確認:systemctl status cron
  2. crontab確認:crontab -l
  3. syslog確認:grep CRON /var/log/syslog | tail -20
  4. スクリプトを手動実行:/path/to/script.sh
  5. PATHの確認——cronのPATHは最小限
  6. スクリプトとアクセス先のパーミッション確認

systemd timerの場合:

  1. timerステータス確認:systemctl status backup.timer
  2. 次回実行時刻確認:systemctl list-timers backup.timer
  3. サービスログ確認:journalctl -u backup.service --since "1 hour ago"
  4. サービスを手動実行:systemctl start backup.service
  5. timerファイルの構文チェック:systemd-analyze verify backup.timer

FAQ

システム上の全スケジュールジョブを一覧するには?

cronは複数箇所を確認する必要がある:crontab -l(現在のユーザー)、sudo crontab -l -u root(root)、ls /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/(システムレベル)。systemd timerは systemctl list-timers --all で全timerの次回実行時刻と前回実行時刻を表示できる。

cronで30秒ごとに実行できる?

cronの最小単位は1分。定番の回避策は2行書く方法:

bash
* * * * * /path/to/script.sh
* * * * * sleep 30 && /path/to/script.sh

systemdなら OnUnitInactiveSec=30s のモノトニックタイマーを使う。ただし、サブ秒のスケジューリングが必要なら、常駐デーモンの方が適切かもしれない。

cronジョブをsystemd timerに移行するには?

2つのファイルを作る:.service ファイルにコマンド(ExecStart= がcronのコマンドに相当)、.timer ファイルに OnCalendar= でcronと同じスケジュール。実行漏れ復旧が必要なら Persistent=true を追加。systemd-analyze calendar でスケジュールが一致しているか確認してから systemctl enable --now your.timer

システム停止中のcronジョブはどうなる?

スキップされて終わり。cronには「後追い実行」の概念がない。毎日3 AMのバックアップが設定されていて、サーバーが2 AM〜5 AMの間ダウンしていたら、その日のバックアップは実行されない。systemd timerの Persistent=true はこの問題を解決する——システム復帰後すぐに実行してくれる。

Dockerコンテナ内でcronは使える?

技術的にはYes。でも大抵は間違ったアプローチだ。コンテナは単一プロセスで動くように設計されている。代わりに、ホスト側のcronやsystemd timerから docker exec container_name command を実行するか、コンテナオーケストレーションのスケジューリング機能(Kubernetes CronJobs、ECS Scheduled Tasks)を使う。

cronジョブのタイムゾーンを変えるには?

一部のcron実装では crontab の先頭に CRON_TZ=America/New_York と書ける。対応していない場合は、スクリプト内で TZ=America/New_York date のようにラップする。systemd timerの場合、serviceファイルに Environment=TZ=America/New_York を設定するか、timedatectl set-timezone でシステム全体を変更する。

systemd timerのAccuracySecとは?

AccuracySec= はtimerの発火精度を制御する。デフォルトは1分——つまり03:00:00に設定したtimerが03:00:45に発火することもある。これは意図的な設計で、systemdが複数のtimerをまとめて起こすことで電力を節約している。精度が必要なら AccuracySec=1s を設定するが、ほとんどのジョブではデフォルトで十分。

cronジョブの重複実行を防ぐには?

flock を使う:*/5 * * * * flock -n /tmp/my-job.lock /path/to/job.sh-n フラグはロックが取れなければ即座に終了する(待機しない)。systemdでは Type=oneshot が標準で重複を防ぐ——サービスがアクティブな間は新しいインスタンスを起動しない。

まとめ

cronとsystemd timerは同じ問題を解決する——コマンドをスケジュール実行する——が、それぞれ違うツールだ。

cron は30秒セットアップ。crontabを編集して1行ペーストすれば終わり。1970年代からこれをやっていて、どの環境でも動く。5フィールド構文は覚えておく価値がある。CI/CDの設定、Kubernetes CronJobs、GitHub Actions、クラウドのスケジューラーなど、至るところで出てくるからだ。

systemd timer は本番グレード。ログ管理、実行漏れ復旧、リソース制限、依存関係の制御、間隔ベースのスケジューリング。2ファイル構成は少し重たいけど、systemctl list-timersjournalctl -u my.service のデバッグ体験はsyslogを掘り返すのとは比べものにならない。

まずはcronでサクッと動かす。信頼性・可観測性・単純な定期実行以上のものが必要になったら、systemd timerに移行する。

CLI自動化の他のツールは、タスクランナーならmake、並列実行ならxargs、全体像はCLIツール完全マップを参照。