systemd を使ってジョブを定期実行させる備忘録


🍐はじめに

cron を使わずとも systemd を使ってジョブを定期的に実行できる。
一般的なLinuxディストリビューションは systemd を使っているため、こちらの利用方法を備忘録しておく。

今回やる systemd による定期実行については、

  1. 実行したいジョブを systemd サービスとして設定

  2. systemd.timer から上記サービスを実行

とすればいい。

🐕‍🦺 systemd サービスとして実行ジョブを設定

  1. 実行するジョブをスクリプトにする

  2. 上記スクリプトを実行する systemd.service を作成

今回作成する systemd.servicesystemd のユーザーモードで動かす(systemctl --user <subcommand>)ことを前提にする。

ジョブをスクリプトとして作成

systemd.service にコマンドを直接記述する場合はいくつか注意すべき点がある[1]
よって、実行したいジョブはあらかじめシェルスクリプトにしておくことを推奨。

Example 1. ジョブのスクリプトを作成
引数を標準出力に返すだけのテスト用スクリプト
mkdir -p ~/.local/bin    (1)
tee ~/.local/bin/hello.sh <<'EOF'
#!/bin/bash
echo "Hello," "$@" "!"
EOF

chmod u+x ~/.local/bin/hello.sh    (2)
1 systemd ユーザーモードで使うため、スクリプトは ~/.local/bin に配置した。
2 実行権限を与えることを忘れずに。

直接コマンドを記述する場合は、こういうことに注意する。
systemd.service Exec

シェル構文は変数の展開(${…​})ぐらいしか使えない(なおネストされた変数展開はうまく機能しない)。
リダイレクト(>, >> <, <<)やパイプ(|)を使いたい場合は シェル(sh, bash)に引数としてコマンド文を渡すしかない。
(スクリプトファイルとして作成しておいて、それを実行するほうが筋としては良さそう)

ExecStart=bash -c "echo 'hoge' > /tmp/hoge.txt"
systemd.service Exec

コマンドが絶対パスで与えられていない場合、systemd はシステムごとに決められたパスからコマンドを検索する。
基本的には絶対パスを使うことが推奨される。

systemd がコマンドを検索するパスの確認
systemd-path search-binaries-default | tr ":" " "
/usr/local/sbin /usr/local/bin /usr/sbin /usr/bin /sbin /bin

systemd サービスを作成

Example 2. systemd.service を作成
systemd.service を新規作成
systemctl edit --user --force --full demo@.service
~/.config/systemd/user/demo@.service
[Unit]
Description=Demo service

[Service]
Type=oneshot
ExecStart=%h/.local/bin/hello.sh %i   (1)

[Install]
WantedBy=multi-user.target
1 ユニット指定子を利用している。
%h → このサービスを実行したユーザーのホームディレクトリパス。
%i → インスタンス名(ユニット名の @ 記号から拡張子までの間にある文字列)。
service をテスト起動
systemctl --user start demo@sample.service
systemctl --user status demo@sample.service
● demo@sample.service - Demo service
     Loaded: loaded (/home/hoge/.config/systemd/user/demo@.service; disabled; vendor preset: enabled)
     Active: inactive (dead)

Oct 23 22:12:38 raspberrypi systemd[747]: Starting Demo service...
Oct 23 22:12:38 raspberrypi hello.sh[15180]: Hello, sample !
Oct 23 22:12:38 raspberrypi systemd[747]: demo@sample.service: Succeeded.
Oct 23 22:12:38 raspberrypi systemd[747]: Started Demo service.
ユニットファイルの拡張子(.service.socket)の直前に @ 記号をつけるとテンプレートユニットになる。
引数を与えてユニットファイルの挙動を一部変更させたいときに便利。

systemd.timer を使って定期的に systemd.service を実行

Example 3. systemd.timer で定期実行
systemd.timer を新規作成
systemctl edit --user --force --full demo@schedule.timer
~/.config/systemd/user/demo@schedule.timer
[Timer]
OnCalendar=*-*-* *:00/10:00   (1)
AccuracySec=1m  (2)
#Unit=demo@sample.service   (3)
1 10分毎に定期実行。書式については後述
2 精度。ここの例では指定した時刻から1分以内のどこかで実行されることになる。
小さくすればするほどCPU負荷が高くなるので注意(デフォルト値は1分)。
3 この timer で実行するユニットを指定する(本例ではコメントアウト)。
指定がなければ timer と同名の service を実行する。
タイマーをテスト起動
systemctl --user start demo@schedule.timer
systemctl --user status demo@schedule.timer
● demo@schedule.timer
     Loaded: loaded (/home/suzutsuki/.config/systemd/user/demo@schedule.timer; static)
     Active: active (waiting) since Sun 2021-10-24 00:05:15 JST; 9s ago
    Trigger: Sun 2021-10-24 00:10:00 JST; 4min 35s left
   Triggers: ● demo@schedule.service

Oct 24 00:05:15 raspberrypi systemd[747]: Started demo@schedule.timer.
ログを確認
journalctl --user --unit demo@schedule.service
Oct 24 00:10:22 raspberrypi systemd[747]: Starting Demo service...
Oct 24 00:10:22 raspberrypi hello.sh[16109]: Hello, schedule !
Oct 24 00:10:22 raspberrypi systemd[747]: demo@schedule.service: Succeeded.
Oct 24 00:10:22 raspberrypi systemd[747]: Started Demo service.
Oct 24 00:20:22 raspberrypi systemd[747]: Starting Demo service...
Oct 24 00:20:22 raspberrypi hello.sh[16125]: Hello, schedule !
Oct 24 00:20:22 raspberrypi systemd[747]: demo@schedule.service: Succeeded.
Oct 24 00:20:22 raspberrypi systemd[747]: Started Demo service.
Oct 24 00:30:22 raspberrypi systemd[747]: Starting Demo service...
Oct 24 00:30:22 raspberrypi hello.sh[16133]: Hello, schedule !
Oct 24 00:30:22 raspberrypi systemd[747]: demo@schedule.service: Succeeded.
Oct 24 00:30:22 raspberrypi systemd[747]: Started Demo service.
Oct 24 00:40:22 raspberrypi systemd[747]: Starting Demo service...

作成した systemd.timer が期待通り動作するようならOK。
あとは自動起動を有効化しておく。

タイマーの自動起動を有効化
systemctl --user enable demo@schedule.timer

😎👍

🖊️補足事項

OnCalendar の値について

systemd.time(7) — Arch manual pages を参考。
書式としては YYYY-MM-DD hh:mm:ss の形になる(曜日指定もできるけど割愛)。

この値についての検証は systemd-analyze calendar コマンドを利用する。

Example 4. OnCalendar の値について検証例
systemd-analyze calendar '4h'
Failed to parse calendar specification '4h': Invalid argument

systemd-analyze calendar '00,04,08,12,16,20:15:00'
  Original form: 00,04,08,12,16,20:15:00
Normalized form: *-*-* 00,04,08,12,16,20:15:00
    Next elapse: Sun 2021-10-24 00:15:00 JST
       (in UTC): Sat 2021-10-23 15:15:00 UTC
       From now: 1h 58min left
Table 1. OnCalendarで使える記号
記号 説明 使用例

*

ワイルドカード
(任意の値)

*-*-* *:30:00

時刻が30分を指すたびに実行。

,

複数指定

*-*-10,20 00:00:00

10日と20日の午前0時に実行。

..

範囲指定

*-*-05..10 12:00:00

5日から10日にかけて、12時に実行。

/

繰り返し間隔

*-*-* *:00/15:00

15分ごとに実行。
(00, 15, 30, 45分時にそれぞれ実行)

~

月末最終日から

*-10~03 00:00:00

10月末から数えて3日目、
つまり10月29日の午前0時に実行。

システム終了時に systemd.service を実行したい場合

shutdown.target 前に実行される systemd.service として実装すればいい(systemd.timer は使わない)。

この方法では電源喪失(停電など)に対応できないことに注意。
通常のシャットダウン操作が実行された時のみ有効となる。
Example 5. シャットダウン時に実行させたい場合の例
シャットダウン時に実行される systemd.service
sudo systemctl edit --force --full run-on-shutdown.service
[Unit]
Description=Run scripts on shutdown
Before=shutdown.target   (1)
DefaultDependencies=no   (2)

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/run-on-shutdown.sh

[Install]
WantedBy=shutdown.target (3)
1 シャットダウンする前にこのサービスを実行させる。
2 起動初期やシャットダウン時に実行させる場合は no にしたほうがいいらしい。
3 シャットダウン時に実行させるための指定。
有効化
sudo systemctl daemon-reload
sudo systemctl enable run-on-shutdown.service

1. bash の環境変数が使えないとか変数展開がうまくいかないとか。