cronジョブをsystemdタイマーに置き換える
cronジョブをsystemdタイマーに置き換える方法を解説。serviceとtimerユニットの作成、OnCalendarの確認、実行漏れの補完、ログと失敗の確認を紹介します。
cronジョブをsystemdタイマーに置き換えるには、2つのユニットファイルを作成します。スクリプトを実行する .service と、実行タイミングを指定する .timer です。あとは systemctl enable --now でタイマーを有効化するだけです。
朝9時に /var/log/backup.log を開いて、昨夜の実行が成功したのかどうか判断できなかった経験があれば、cronから移行したくなる理由はすでにおわかりでしょう。本ガイドでは、実際のcrontabの1行をタイマーへ移行します。
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
各セクションで、この行の各要素を1つずつ置き換えていきます。スケジュールは OnCalendar= に、リダイレクトはjournalに、そして「実行されたか?」の確認は systemctl status に置き換わります。cron自体の基礎については、cronジョブを自動化する方法を参照してください。本記事のコマンドは、現行のsystemdベースのディストリビューションであればどれでも動作します。
重要なポイント
- systemdタイマーとは、
[Timer]セクションで対応する.serviceの実行タイミングを決める.timerユニットです。何を実行するかはサービスが定義し、有効化するのはタイマーのほうです。 - タイマーから起動されるサービスには
[Install]セクションは不要です。タイマー自身がWantedBy=timers.targetによってブート時に組み込まれます。 systemd-analyze calendarを使うと、OnCalendar=の式が正しく解析できるかを確認し、次回の実行予定時刻を表示できます。Persistent=trueを指定すると、マシンの電源が切れていたために実行されなかったジョブが、タイマーが再びアクティブになった時点ですぐに実行されます。- systemdタイマーには、cronの
MAILTOに相当する組み込み機能はありません。失敗時の通知にはOnFailure=を使用します。
cronにできないこと
cronはスケジュールに従ってコマンドを起動するだけで、それ以外のことはほとんどしません。以下のような機能の欠如が、ジョブを移行する一般的な理由です。
- メール送信される出力以外にログがない。 出力を手動でファイルへリダイレクトするか、
MAILTOに頼るしかありません。タイマーのジョブは自動的にjournalへ書き込まれます(「ログと最終実行ステータスの確認」を参照)。 - ジョブ間の依存関係を定義できない。 タイマーのジョブは通常のユニットなので、他のsystemdユニットに依存させることができます。
- スケジュール時刻にマシンが停止していると何も実行されない。
Persistent=trueを使えば、実行されなかった分を後から実行できます(「Persistent=true」のセクションを参照)。 - 昨夜の実行結果を手軽に確認する方法がない。
systemctl statusで最終実行とその終了コードを確認できます。
systemdタイマーを一から構築する
動作するタイマーは、/etc/systemd/system/ に置く2つのファイルと、1つのコマンド sudo systemctl enable --now backup.timer で構成されます。まずはスクリプトから始めましょう。
#!/usr/bin/env bash
# /usr/local/bin/backup.sh
set -euo pipefail
SRC=/srv/app/data
DEST=/var/backups/app
tar -czf "$DEST/app-$(date +%F).tar.gz" -C "$SRC" .
find "$DEST" -name 'app-*.tar.gz' -mtime +14 -delete
set -euo pipefail により、いずれかのコマンドが失敗した時点でスクリプトは非ゼロの終了コードで終了し、その終了コードがsystemdに記録されます。スクリプトに実行権限を付与し、保存先ディレクトリを作成します。パーミッションビットに馴染みがない場合は、Linuxファイルパーミッションガイドで解説しています。
sudo chmod +x /usr/local/bin/backup.sh
sudo install -d -o deploy -m 750 /var/backups/app
次に、サービスファイル /etc/systemd/system/backup.service を作成します。
[Unit]
Description=Nightly tarball of /srv/app/data
[Service]
Type=oneshot
User=deploy
ExecStart=/usr/local/bin/backup.sh
systemd.service(5) の Type=oneshot は、最後まで実行して終了するスクリプトに適しています。systemdは、スクリプトが終了して初めてユニットが起動完了したとみなします。RemainAfterExit= を追加しない限り、ユニットはその後すぐにinactiveに戻り、「active」と表示されることはありません。deploy は、SRC を読み取り DEST に書き込める既存の非rootアカウントに置き換えてください。User= を指定しない場合、ジョブはrootとして実行されます。このサービスには [Install] セクションもタイマーへの参照もありません。その理由は Arch Wikiのタイマーのページで説明されているとおり、有効化するのはタイマーであり、タイマーが同じ名前のサービスを起動するからです。
続いて、タイマーファイル /etc/systemd/system/backup.timer を作成します。
[Unit]
Description=Run backup.service daily at 02:30
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
timers.target は、systemdがブート時に、稼働させるべきすべてのタイマーを起動するために使用する特殊なターゲットです。systemdをリロードし、ジョブを一度手動で実行してから、スケジュールを登録します。
sudo systemctl daemon-reload
sudo systemctl start backup.service # test run now; returns when the script exits
systemctl status backup.service # check the result
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
タイマーで実行されるジョブはすべて通常のサービスなので、systemctl start backup.service でいつでも実行でき、02:30を待たずにデバッグできます。enable 単体ではシンボリックリンクを作成するだけです。systemctl(1) の --now スイッチを付けると、タイマーの起動も同時に行います。list-timers はユニットのパターンを受け付け、--all を付けると非アクティブなタイマーも表示されます。実行と実行の間、サービスはinactiveと表示されますが、これは正常な状態です。
OnCalendarとcronの5フィールド形式の比較
OnCalendar= の式は、タイマーファイルに記述する前に必ず systemd-analyze calendar で確認しましょう。式が正しく解析できることを確認し、次回の実行予定時刻を表示してくれます。systemd.time(7) で定められた文法は DayOfWeek Year-Month-Day Hour:Minute:Second です。日付と時刻の各フィールドには、すべてに一致する *、カンマ区切りの値のリスト、または .. で記述する範囲を指定できます。曜日は完全に省略可能です。曜日を指定する場合は、* ではなく、Mon や Monday のような英語の曜日名を使います。
よく使われるcronのスケジュールと OnCalendar= の式の対応は以下のとおりです。表のcron側については Linux cronチートシートで解説しています。
| スケジュール | cron | OnCalendar |
|---|---|---|
| 毎日02:30 | 30 2 * * * | *-*-* 02:30:00 |
| 平日22:30 | 30 22 * * 1-5 | Mon..Fri 22:30 |
| 毎週日曜03:00 | 0 3 * * 0 | Sun *-*-* 03:00:00 |
| 毎月1日の午前0時 | 0 0 1 * * | *-*-01 00:00:00 |
| 15分ごと | */15 * * * * | *:00/15 |
cronの 30 22 * * 1-5 は OnCalendar=Mon..Fri 22:30 になります。cronの曜日番号1〜5が月曜〜金曜にあたるためです。/15 という接尾辞は「15単位ごとに繰り返す」ことを意味します。式の検証には systemd-analyze calendar を使用します。
systemd-analyze calendar "Mon..Fri 22:30" --iterations=3
出力例(日付はプレースホルダーです):
Original form: Mon..Fri 22:30
Normalized form: Mon..Fri *-*-* 22:30:00
Next elapse: Mon YYYY-MM-DD 22:30:00 CEST
(in UTC): Mon YYYY-MM-DD 20:30:00 UTC
From now: 5h 12min left
Iteration #2: Tue YYYY-MM-DD 22:30:00 CEST
Iteration #3: Wed YYYY-MM-DD 22:30:00 CEST
Normalized formの行には、短縮形を完全な文法に展開した結果が表示されます。(in UTC) の行は、ローカル時刻がUTCでない場合にのみ表示されます。systemd 253より前のリリースでは、追加の実行予定は Iteration #2 ではなく Iter. #2 と表示されます。--iterations= にはsystemd 242以降が必要です。
精度に関する注意点が1つあります。タイマーの AccuracySec はデフォルトで1分なので、02:30:00のジョブがその秒ちょうどに開始される保証はありません。systemdで可能な限り厳密なタイミングが必要な場合は、[Timer] セクションに AccuracySec=1us を追加してください。
Persistent=true:ノートPC向けの機能
Persistent=true を指定すると、マシンの電源が切れていたためにスケジュール実行を逃したタイマーは、タイマーが再びアクティブになった時点ですぐにサービスを起動します。通常は次回のブート時です。素のcronにはこれに相当する機能はありません。02:30にノートPCがシャットダウンされていれば、cronはそのジョブをスキップします。実行を逃したジョブを後から実行するための別ツールとしてanacronがあります(RHEL系のシステムではcronieに同梱されています)が、スケジュールは日単位でしか指定できず、時刻は指定できません。systemd.timer(5) によれば、systemdは各永続タイマーが最後に実行された時刻を記録します。Persistent= は OnCalendar= を持たないタイマーには効果がなく、また後追い実行も RandomizedDelaySec= による遅延を待ってから行われます。
OnBootSec= や OnUnitActiveSec= などのモノトニックタイマーは動作が異なります。これらは WakeSystem= が設定されていない限り、システムのサスペンド中はカウントを停止します。時刻に紐づくジョブには OnCalendar= と Persistent=true を組み合わせて使用してください。
ログと最終実行ステータスの確認
昨夜のジョブが成功したかどうかを確認するには、systemctl status backup.service を実行します。サービスが最後に実行された時刻、成功したか失敗したか、そしてプロセスの終了コードが表示されます。スクリプトが出力した内容はすべて journalctl(1) で確認できます。
systemctl status backup.service
journalctl -u backup.service --since yesterday
journalctl -u backup.service --since "-2h"
--since には、yesterday、today、絶対タイムスタンプ、および - を前置した相対オフセットを指定できます。yesterday は昨日の午前0時を意味するため、出力には今日のエントリも含まれる点に注意してください。
systemdタイマーのジョブが失敗する様子を、一度意図的に確認しておくとよいでしょう。スクリプトを SRC=/srv/app/missing に書き換えてから実行します。
sudo systemctl start backup.service
systemctl status backup.service
journalctl -u backup.service -n 20
tar が失敗し、set -e によってスクリプトが非ゼロで終了すると、ユニットはfailed状態になり、ステータスにその終了コードが表示されます。tar のエラーメッセージ自体はjournalに記録されています。確認が終わったら SRC を元に戻してください。
cronのままで問題ないのはどんなときか
依存関係のない単一のジョブであれば、cronのままで問題なく、むしろそのほうがシンプルです。ジョブはcrontabの1行で済み、cronはほぼどこでも利用でき、依存関係のない毎晩1回のジョブなら問題なく動作します。タイマーには2つのファイルといくつかの systemctl コマンドが必要です。また、MAILTO に相当する組み込み機能もありません。ジョブの失敗時に通知を受け取るには、サービスの OnFailure= ディレクティブに、通知を送信するユニットを指定してください。
まとめ
cronからsystemdタイマーに移行すると、素のcronでは得られない3つのメリットがあります。オンデマンドで実行・確認できるジョブ、リダイレクトなしでjournalに記録される出力、そしてダウンタイム後の後追い実行です。まずは、最も気がかりなcrontabのジョブを1つ選びましょう。そのサービスとタイマーを作成し、systemd-analyze calendar でスケジュールを検証し、systemctl start でテストします。そして systemctl status でスケジュール実行の成功が確認できたら、crontabの行を削除してください。
よくある質問
root権限なしで、一般ユーザーとしてsystemdタイマーを実行できますか?
はい、できます。.serviceファイルと.timerファイルを~/.config/systemd/user/に配置し、systemctl --userで管理します。たとえばsystemctl --user enable --now backup.timerのように実行します。デフォルトでは、ユーザーのsystemdインスタンスはそのユーザーの最後のログインセッションが閉じると停止するため、タイマーも実行されなくなります。sudo loginctl enable-lingerに続けてユーザー名を指定して実行すると、ユーザーインスタンスがブート時に起動し、セッションがなくても稼働し続けるようになります。
AccuracySecとRandomizedDelaySecの違いは何ですか?
AccuracySecは、systemdがタイマーイベントをまとめて処理し、CPUを起こす回数を減らすための猶予を与えます。タイマーはスケジュール時刻からAccuracySec後までの任意の時点で実行される可能性があり、この幅はデフォルトで1分です。RandomizedDelaySecは、このまとめ処理とは逆の働きをします。各実行を、設定した値(デフォルトは0)を上限とするランダムな時間だけ遅らせるため、同じスケジュールのタイマーが一斉に実行されず分散されます。両方を設定した場合、ランダムな遅延が適用された後に、systemdがそのイベントを他のイベントと揃えます。
ユニットファイルを書かずに、systemdで単発のジョブをスケジュールするにはどうすればよいですか?
タイマーオプションを付けてsystemd-runを使用します。たとえばsudo systemd-run --on-calendar='Mon 14:00' /usr/bin/cleanup.shのように実行します。これにより、一時的なタイマーとサービスのペアが作成されます。すぐに起動するのはタイマーだけで、サービスはタイマーの時刻が到来したときに実行されます。--on-active=や--on-boot=などのフラグを使うと、モノトニックな一時タイマーが作成されます。一時ユニットは再起動後には残らないため、繰り返し実行するジョブはやはりユニットファイルで定義すべきです。
タイマーファイルは、起動するサービスと同じ名前にする必要がありますか?
必須ではありませんが、それがデフォルトの動作です。タイマーは接尾辞を除いた同名のサービスを起動するため、backup.timerはbackup.serviceを起動します。別の名前のユニットを起動するには、[Timer]セクションでUnit=を設定します。たとえばUnit=nightly-backup.serviceのように指定します。この値に別の.timerユニットを指定することはできません。とはいえ、systemd.timer(5)では、タイマーとそれが起動するユニットに同じベース名を付けることが推奨されています。