RHEL 中級 #6 ジョブスケジューリング — cron、systemd timer、at
運用していると時間ベースで回さなければならない作業が果てしなく出てきます。毎日早朝のバックアップ、毎週レポートメール、毎分ヘルスチェック、1 時間後に 1 度だけ回すスクリプト。RHEL 9 にはこうした作業を扱うツールが cron、anacron、at、systemd timer に分かれています。この記事ではそれぞれをいつ使うかが要点です。
RHEL 中級 シリーズでこの記事の位置:
- #1 SELinux 入門 — Enforcing/Permissive、ラベル、トラブルシューティング
- #2 LVM — PV/VG/LV、スナップショット、拡張
- #3 ストレージ深化 — Stratis、NFS、Samba
- #4 ネットワーキング — NetworkManager (nmcli)、bonding、teaming
- #5 ログ管理 — journald、rsyslog、log rotation
- #6 ジョブスケジューリング — cron、systemd timer、at ← この記事
- #7 コンテナ入門 — Podman/Buildah/Skopeo (Docker との違い)
4 つのツールを 1 行で #
| ツール | 用途 | マシンが切れていた時間 | 備考 |
|---|---|---|---|
| cron | 反復作業 (毎分/毎時/毎日/…) | そのまま飛ばす | 最も伝統、最も単純 |
| anacron | マシンが切れて再度オンになっても保証された日/週/月単位の実行 | 逃した作業を補う | ノート PC・在宅サーバー必須 |
| at | 単発予約 (1 時間後に 1 度実行) | マシンがオンになった後即時実行 | 1 回限り |
| systemd timer | 反復 + 依存性 + ログ統合 | Persistent=true で補う | モダン標準 |
新しい作業を掴むときのフロー:
- 反復 + 単純 → cron
- 反復 + マシンがオンか不確実 → anacron または systemd timer (
Persistent=true) - 反復 + 依存性 / journald 統合が必要 → systemd timer
- きっちり 1 度 → at
cron — 伝統の出発点 #
cronie パッケージが RHEL 9 にデフォルトインストールされており、crond サービスが常に実行されます。
$ systemctl status crond
● crond.service - Command Scheduler
Loaded: loaded (/usr/lib/systemd/system/crond.service; enabled; ...)
Active: active (running)cron が見るファイルは 2 系統:
/etc/crontab ← システム全域 (慣習)
/etc/cron.d/<name> ← システム全域 (drop-in、推奨)
/etc/cron.{hourly,daily,weekly,monthly}/ ← ディレクトリにスクリプトを置くだけで自動実行
/var/spool/cron/<user> ← ユーザー別 crontab (直接編集 ✗、crontab -e で)crontab 文法 #
5 つの欄の時刻 + コマンド。最初は目が回りますがよく使うと手に馴染みます。
* * * * * コマンド
│ │ │ │ │
│ │ │ │ └─ 曜日 (0-7、0 と 7 は日曜日)
│ │ │ └─── 月 (1-12)
│ │ └───── 日 (1-31)
│ └─────── 時 (0-23)
└───────── 分 (0-59)よく使うパターン:
# 毎分
* * * * * /usr/local/bin/healthcheck.sh
# 毎日早朝 3 時 15 分
15 3 * * * /usr/local/bin/backup.sh
# 平日 (月〜金) 午前 9 時
0 9 * * 1-5 /usr/local/bin/morning-report.sh
# 5 分間隔
*/5 * * * * /usr/local/bin/ping.sh
# 9〜18 時、30 分間隔
0,30 9-18 * * * /usr/local/bin/business-hours.sh
# 毎月 1 日 0 時
0 0 1 * * /usr/local/bin/monthly-cleanup.sh
# 略式表現
@reboot /usr/local/bin/on-startup.sh
@hourly /usr/local/bin/each-hour.sh
@daily /usr/local/bin/each-day.sh
@weekly /usr/local/bin/each-week.sh
@monthly /usr/local/bin/each-month.sh
@yearly /usr/local/bin/each-year.shユーザー crontab #
各ユーザーが自分の作業を登録できます。
# 登録/修正 (エディタで開く)
$ crontab -e
# 見る
$ crontab -l
# 空にする
$ crontab -r
# 別のユーザーのもの (root のみ)
$ sudo crontab -u alice -lcrontab -e で編集する理由: /var/spool/cron/<user> を直接 vim で開くと cron が変更を検知できません。crontab -e は変更後 cron にシグナルを送ります。
システム crontab vs ユーザー crontab #
/etc/crontab と /etc/cron.d/* は ユーザーのカラムが 1 つ多い です。
# 分 時 日 月 曜日 ユーザー コマンド
*/10 * * * * root /usr/local/bin/sync.sh運用推奨:
- ユーザー作業 →
crontab -e - パッケージ/システム作業 →
/etc/cron.d/<name>(drop-in ファイル) /etc/crontab直接修正は避ける (慣習上システム専用、パッケージが上書きする可能性)
cron 環境変数 #
cron が回すときシェル環境は ログインシェルではありません。 $PATH も異なり $HOME も制限的です。手動で実行したときうまくいったスクリプトが cron で失敗する最もよくある理由。
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
15 3 * * * root /usr/local/bin/backup.shMAILTO= が空でなければ cron が作業の stdout/stderr をメールで送ります。メール送信が遮断されている環境では、スクリプト内で直接ログファイルにリダイレクトするのが標準です。
15 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1権限制御 #
/etc/cron.allow と /etc/cron.deny でユーザー別 cron 使用を制御します。
cron.allowがあれば → そこに書かれたユーザーのみ使用可能cron.allowがなくcron.denyがあれば → そこに書かれて いない ユーザーのみ使用可能- 両方ない場合 (RHEL 9 デフォルト) → root のみ使用可能
運用では cron.allow を使うホワイトリスト方式が一般的です。
anacron — マシンが切れて再度オンになっても保証 #
cron の弱点: マシンが早朝 3 時に切れていたら早朝 3 時のバックアップは そのまま飛ばされます。 翌日まで待たなければなりません。ノート PC、在宅サーバー、たまにオンにするワークステーションで致命的。
anacron は「最後にこの作業が実行されたのはいつか」をディスクに記録します。マシンがオンになるたびに anacron が作動して、最後の実行から指定期間が過ぎていれば即座に実行します。
SHELL=/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
RANDOM_DELAY=45
START_HOURS_RANGE=3-22
# 周期 遅延(分) ジョブ ID コマンド
1 5 cron.daily nice run-parts /etc/cron.daily
7 25 cron.weekly nice run-parts /etc/cron.weekly
@monthly 45 cron.monthly nice run-parts /etc/cron.monthly読み方:
1 5 cron.daily ...— 毎日 (period 1)、マシンがオンになった後 5 分後、/etc/cron.daily/*のすべてのスクリプト実行7 25 ...— 毎週 (period 7)RANDOM_DELAY=45— 負荷分散用ランダム遅延 0〜45 分
RHEL 9 の動作: anacron は cronie パッケージに一緒に入って systemd timer (anacron.timer) で実行されます。なので /etc/cron.daily/* にスクリプトを置くだけで cron + anacron が一緒に作動してマシンが切れていても次回起動後に補充実行されます。
サーバークラスのマシンはどのみち 24/7 オンで anacron に大きな意味がないですが、知らずに 2 度実行される事故を防ぐには cron.daily 作業の冪等性 (idempotency) を常に気にしなければなりません。
at — 1 度だけ予約 #
「30 分後にこのコマンドを 1 度だけ」のような単発予約は at の領域。
# パッケージ (RHEL 9 デフォルトインストール ✗ → 直接インストール)
$ sudo dnf install -y at
$ sudo systemctl enable --now atd
# 30 分後
$ at now + 30 minutes
at> /usr/local/bin/restart-app.sh
at> <Ctrl-D>
# 特定時刻
$ at 23:30
$ at 23:30 today
$ at 9am tomorrow
$ at 11:00 2026-05-01
# キューを見る
$ atq
# キュー内容を見る (作業番号で)
$ at -c 3
# 取り消し
$ atrm 3at も cron のように権限制御ファイルがあります: /etc/at.allow、/etc/at.deny。
at は batch という変形も提供します。システム負荷が低いとき (load average < 1.5) に実行されるモード。
$ batch
at> /usr/local/bin/heavy-job.sh
at> <Ctrl-D>夜間バックアップのような重い作業をシステムが暇なときだけ回したいときに有用です。
systemd timer — モダン標準 #
cron が 1975 年に作られたツールなら、systemd timer は 2010 年代のモダンな代替です。RHEL 9 のパッケージたちが徐々に cron から timer に移っています。logrotate、dnf-makecache、fstrim、すべて timer で実行されます。
timer が cron より良い点 #
- journald 統合 — すべての出力が journald に自動記録 (cron はメールまたは直接 redirect)
- 依存性 —
After=network-online.targetのような systemd 依存性をそのまま使用 - リソース制御 —
MemoryMax=、CPUQuota=など service unit の制御オプションそのまま Persistent=true— マシンが切れていた時間を anacron のように補充OnCalendar=— cron より表現力が広く人が読みやすい- trigger 検証 —
systemd-analyze calendarで次の実行時刻をあらかじめ確認
timer + service ペア #
systemd timer は常に 2 つの unit がペアを成します。.service (実際にやること) + .timer (いつ実行するか)。
[Unit]
Description=Daily backup
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Group=backup
Nice=10[Unit]
Description=Run daily backup at 03:15
Requires=backup.service
[Timer]
OnCalendar=*-*-* 03:15:00
RandomizedDelaySec=15min
Persistent=true
Unit=backup.service
[Install]
WantedBy=timers.targetアクティベーションは timer のみ enable します (service は timer がトリガー)。
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now backup.timer
$ systemctl list-timers --allOnCalendar 文法 #
cron の * * * * * と比較すると systemd の方がはるかに明確です。
# 毎分
*-*-* *:*:00
# 毎日 03:15
*-*-* 03:15:00
03:15 # 略式
# 平日 09:00
Mon..Fri *-*-* 09:00:00
Mon..Fri 09:00 # 略式
# 5 分間隔
*-*-* *:00/5:00
*:0/5 # 略式
# 毎月 1 日
*-*-01 00:00:00
monthly # 別名
# 別名たち
hourly / daily / weekly / monthly / yearly文法検証と次の実行時刻プレビュー:
$ systemd-analyze calendar 'Mon..Fri 09:00'
Original form: Mon..Fri 09:00
Normalized form: Mon..Fri *-*-* 09:00:00
Next elapse: Mon 2026-04-27 09:00:00 KST
From now: 4 days leftルールを適用する前に意図した時刻か確認する習慣が運用事故を多く防ぎます。
timer トリガーの種類 #
OnCalendar 以外にもいくつかのトリガーがあります。
| トリガー | 意味 |
|---|---|
OnCalendar= | カレンダー時刻 (cron のような絶対時刻) |
OnBootSec= | 起動後 N 秒 |
OnStartupSec= | systemd 開始後 N 秒 |
OnUnitActiveSec= | service が最後にアクティベートされた後 N 秒 |
OnUnitInactiveSec= | service が終了した後 N 秒 |
例えば OnUnitActiveSec=10min は「前回実行後 10 分後」 — cron の */10 * * * * に似ていますが 前回実行時間基準 なので作業が長引けば次の実行が自然に押されます。cron はそのまま 2 度実行します。
Persistent — anacron の役割まで吸収 #
[Timer]
OnCalendar=daily
Persistent=truePersistent=true は「最後の実行時刻をディスクに記録し、マシンが切れていて時刻を逃したら起動後に補充実行」 の意味。anacron がやっていたことを timer がそのまま吸収します。
ユーザー timer #
ルート権限なしで自分の作業のみ timer で回すことも可能です。
$ mkdir -p ~/.config/systemd/user/
# ~/.config/systemd/user/myjob.service、myjob.timer 作成
$ systemctl --user daemon-reload
$ systemctl --user enable --now myjob.timer
$ systemctl --user list-timersユーザーセッションが終了した後も作業が回るようにするには loginctl enable-linger <user> で lingering をオンにする必要があります。
よく見るシステム timer #
$ systemctl list-timers
NEXT LEFT LAST ... UNIT
Wed 2026-04-23 04:00:00 KST 5h 12min left Tue 2026-04-22 04:00:00 KST ... dnf-makecache.timer
Wed 2026-04-23 06:30:00 KST 7h 42min left Tue 2026-04-22 06:30:00 KST ... logrotate.timer
Wed 2026-04-23 ... ... fstrim.timer
...dnf メタデータ更新、logrotate、fstrim、anacron — RHEL 9 デフォルトインストールだけでも timer がいくつもすでに回っています。
cron vs systemd timer — どちらを? #
| 基準 | cron 有利 | timer 有利 |
|---|---|---|
| 単純な「毎分/毎日」 | ✓ | |
| シェル 1 行の作業 | ✓ | |
| journald に出力統合 | ✓ | |
| ネットワーク/マウント依存性 | ✓ | |
| リソース制限 (CPU/メモリ) | ✓ | |
| パッケージ標準ツール | ✓ (RHEL 9 トレンド) | |
| 学習曲線 | ✓ (5 分で十分) |
運用推奨:
- 既存作業が cron で回っていて単純ならそのまま置く
- 新規追加するシステム作業は timer で 作成
- journald 統合と依存性制御が必要なら timer 早期導入
デバッグ — 作業が回らないとき #
cron 作業が回らないとき #
# 1. crond が生きているか
$ systemctl status crond
# 2. 作業が登録されているか
$ crontab -l # ユーザー
$ sudo cat /etc/cron.d/* # システム
# 3. cron 自体のログ
$ sudo journalctl -u crond --since "1 hour ago"
# 4. 作業の stdout/stderr をどこへ送っているか確認
# (MAILTO 空ならメール来ない、redirect なしなら出力消える)
# 5. PATH 問題 — 手動はうまくいくのに cron だけ失敗するなら 99% これ
# crontab 内で PATH= 明示するかコマンドに絶対パス使用systemd timer 作業が回らないとき #
# 1. timer が active か
$ systemctl status backup.timer
$ systemctl list-timers backup.timer
# 2. 次回実行予定が意図した時刻か
$ systemd-analyze calendar 'OnCalendar 表現式'
# 3. service が実際に回ったとき何が起きたか
$ journalctl -u backup.service --since "1 day ago"
# 4. 手動で 1 度 trigger してみる
$ sudo systemctl start backup.service
# 5. timer 自体のログ
$ journalctl -u backup.timer手動トリガーは 「timer の問題か、service 自体の問題か」 を分ける最速の検証。
よくある落とし穴 #
- cron + 環境変数: 手動シェルと cron シェルが異なる点を忘れるとデバッグが長くなります。絶対パス + crontab 内の
PATH=が最も安全。 - タイムゾーン: cron はシステムタイムゾーン (
/etc/localtime) で動作します。コンテナで UTC に設定されたホストに KST 想定の作業を入れると 9 時間ずれます。 - DST (サマータイム): KST にはありませんが他の地域のマシンは毎年 2 度問題が起きます。
OnCalendarは DST 切り替え処理ルールが明示されていて cron より安全。 - timer の
Persistent=trueをオンにすると、マシンを数日切ってからオンにしたときに、たまっていた作業が一度に実行されます。バックアップが同時に複数回回ると困るならロック (flock) または冪等処理。 - at 作業は atd が死んでいるとそのまま消えます。
systemctl enable --now atd確認。
覚えておくコマンド #
| 作業 | コマンド |
|---|---|
| ユーザー crontab 編集 | crontab -e |
| ユーザー crontab を見る | crontab -l |
| すべての timer を見る | systemctl list-timers --all |
| timer の次回実行時刻確認 | systemd-analyze calendar '<expr>' |
| at キューを見る | atq |
| at 作業の取り消し | atrm <id> |
| cron ログを見る | journalctl -u crond |
| timer ペアの service ログ | journalctl -u <name>.service |
| 手動トリガー | systemctl start <name>.service |
まとめ #
- cron — 単純な反復作業の伝統ツール。
crontab -eでユーザー作業、/etc/cron.d/*でシステム作業。 - anacron — cron がマシンが切れていて逃した日/週/月作業を起動後に補充。RHEL 9 デフォルト。
- at — 1 度だけ予約。
at now + 30 minutesのような単発。atd サービスが必要。 - systemd timer —
.timer+.serviceペア。journald 統合、依存性、リソース制御、Persistent=trueで anacron 吸収。新しい作業は可能ならここへ。 - デバッグの要点: cron 環境変数、timer は service 単独トリガーで分離検証。
次 — コンテナ入門 #
ここまでが RHEL 9 で時間ベースの作業を運用するツールでした。次は 1 台のマシン内で隔離された環境を複数立ち上げるコンテナへ移ります。
#7 コンテナ入門 — Podman/Buildah/Skopeo (Docker との違い) では RHEL 9 のコンテナ標準である Podman を扱います。Docker とほぼ同じコマンドを使いながらもデーモンなしで動作する構造、rootless コンテナ、Buildah でイメージをビルドするフロー、Skopeo でレジストリの間を移す作業まで 1 サイクルで整理します。