🤖

[Linux] systemdのユーザインスタンスで、サーバ起動後にユーザ固有のジョブを自動実行する

2022/07/09に公開

systemdは、いつものsudoつきでシステムワイドに実行する他にも、Linuxユーザごとに固有のサービス起動を指定・実行することができます。

本記事では、これを利用して、サーバ(再)起動後の初回ログイン時[1]に、自動で特定ユーザ用のジョブを実行させる例を示します。
(筆者のsystemdの理解がそこまで深くないので、手順をなぞる寄りの記事になります。)

この記事でやること

systemdユニットファイルに、やりたいジョブの具体的な実行方法(スクリプトやコマンドの実行など)をまとめます。

また、このユニットファイルが自動実行されるように設定します。

対象読者

sudo systemctlは使ったことがあるけれど、sudoなし&--userオプション付きでsystemdを使うことは知らなかった人

筆者の使用例

  • sudo apt update -yを走らせる

  • .ssh/configを、外部のプライベートGitリポジトリから持ってきてセッティングする

systemdうま味

  • systemctlコマンドで、いろんなサービスを同じ操作で実行できる

  • 何を用意したか一覧できる:

    • systemctl --user list-dependenciesなどで確認する

    • ~/.config/systemd/user/配下のユニットファイルをlsなどで見る

  • ログも、journalctlコマンドで、同じ操作で確認できる

まとめ

初めてこの記事を読む際は、ここは適当に読み流して、実装例の項を眺めた方が読みやすいと思います。

やり方要点

  1. 「~/.config/systemd/user/<サービス名>.service」というユニットファイルを作る

    ユニットファイル例

    単発で実行するコマンド例なので、Type=oneshotと指定しています。

    [Unit]
    Description=Apt Update
    
    [Service]
    Type=oneshot
    ExecStart=/usr/bin/sudo /usr/bin/apt update -y
    
    [Install]
    WantedBy=default.target
    
    • 典型的には、[Service]セクションのExecStartにて、実行内容をワンライナーで指定する

      • コマンド等のパスは、絶対パスで指定するのがおすすめ(PATHは、通常のユーザログイン時のものとは異なるため[2]PATH通し忘れを回避するために)

      • 複雑な実行内容は、シェルスクリプトにまとめて、/usr/local/sbinなどに配置し、それを実行するようにする

    • [Install]セクションでは、WantedBy=default.targetを指定する

    • ユニットファイルの詳細は、注釈のリンク[3][4]などを参照

  2. オプション--userつき(&sudoなし)で、systemctlコマンドで色々する

    • ユニットファイルを更新したので、daemon-reloadする

      $ systemctl --user daemon-reload
      
    • サービスの起動start・停止stop・再起動restart

    • サーバ(再)起動後初回ログイン時[1:1]にサービスを実行したいので、enableする

      $ systemctl --user enable <サービス名>.service
      
    • list-dependenciesで、実行ツリーにサービスが含まれているかどうかを確認する

      $ systemctl --user list-dependencies
      
  3. (おまけ)journalctlコマンドで、実行結果を確認する

    $ journalctl --user -xeu <サービス名>.service
    

    オプションの意味(journalctl --user --helpより):

    • -x:説明文を追加する

    • -e:ページャの末尾に移動した状態にする

    • -u:指定したユニットのログを表示する

サービス内で環境変数を使う際の注意点

systemdのサービス自動実行は、ユーザのログインシェルセッションと独立しています[2:1]

したがって、Linuxログイン時に.bashrcなどで自動的に設定されるような環境変数(例:PATH)を使いたい場合、何らかの方法で設定してあげる必要があります。

無難と思われる例:

  • environment.d ディレクトリを使用する[5]

  • ユニットファイル中で呼び出すシェルスクリプトにPATHの設定を書いてしまう

    • ちょっと〇〇のランタイムだけ必要なんだよね、くらいなら
(ついでに)サービスを手動実行する場合

(今回の主旨とすこしずれるけれど、ついでに)自動実行ではなく、ログインシェル上で、systemctl --user startコマンドなどでサービスを手動実行する場合:

  • systemctl --user import-environmentを使う

実装例

ここでは、以下2つの「単発実行ジョブを走らせる」サービスを、サーバ起動時の実行ユーザサービスとして設定することにします:

  1. sudo apt update -yするサービス

  2. 実行時刻とenvコマンドの結果を、~/record_env_info.txtというファイルに追記するサービス

1. sudo apt update -yするサービスの実装

1-1. ユニットファイルの作成

ユニットファイル「~/.config/systemd/user/apt-update.service」を作成します。

ポイントは、

  • Type=onshotにした[4:1]

  • 実行コードをそのままExecStartに指定した

  • 実行コマンドは絶対パスで指定した[2:3]

  • WantedBy=default.target

~/.config/systemd/user/apt-update.service
[Unit]
Description=Apt Update

[Service]
Type=oneshot
ExecStart=/usr/bin/sudo /usr/bin/apt update -y

[Install]
WantedBy=default.target

1-2. ユーザユニットファイルの変更を反映

daemon-reloadサブコマンドで、ユニットファイルの変更を反映します:

$ systemctl --user daemon-reload

反映ついでに、今作成したapt-update.serviceの状態確認をしておきましょう:

# status
$ systemctl --user status apt-update.service
○ apt-update.service - Apt Update
     Loaded: loaded (/home/ubuntu/.config/systemd/user/apt-update.service; disabled; vendor preset: enabled)
     Active: inactive (dead)

# is-enabled
$ systemctl --user is-enabled apt-update.service
disabled

また、list-dependenciesサブコマンドで、実際にブート後に実行されるサービスの一覧が確認できます:

$ systemctl --user list-dependencies
default.target
● └─basic.target
●   ├─paths.target
●   ├─sockets.target
●   │ ├─dbus.socket
●   │ ├─dirmngr.socket
●   │ ├─gpg-agent-browser.socket
●   │ ├─gpg-agent-extra.socket
●   │ ├─gpg-agent-ssh.socket
●   │ ├─gpg-agent.socket
●   │ ├─pk-debconf-helper.socket
●   │ └─snapd.session-agent.socket
●   └─timers.target

今作成したapt-update.serviceは、まだenable設定していない現時点では、この一覧に含まれていないことがわかります。

1-3. 手動実行して動作確認

restart(今回はType=oneshotなので、startでもよい)サブコマンドで、一度手動実行してみます。

$ systemctl --user restart apt-update.service
# 内容がsudo apt updateなので、少し待ち時間が生じる

systemctl --user statusjournalctl --userで、実行ログを確認できます:

$ systemctl --user status apt-update.service
$ journalctl --user -xeu apt-update.service
# ページャが開く(qキーで閉じる)

オプションの意味(journalctl --user --helpより):

  • -x:説明文を追加する

  • -e:ページャの末尾に移動した状態にする

  • -u:指定したユニットのログを表示する

エラーがない場合

エラーが起きなければ、特に出力なく終わります。

詳細

statusサブコマンドで実行ログを確認したり、

$ systemctl --user status apt-update.service
○ apt-update.service - Apt Update
     Loaded: loaded (/home/ubuntu/.config/systemd/user/apt-update.service; disabled; vendor preset: enabled)
     Active: inactive (dead)

Jul 08 14:00:14 ip-172-31-1-136 systemd[786]: Starting Apt Update...
Jul 08 14:00:14 ip-172-31-1-136 sudo[882]:   ubuntu : PWD=/home/ubuntu ; USER=root ; COMMAND=/usr/bin/apt update -y
Jul 08 14:00:14 ip-172-31-1-136 sudo[882]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=1000)
Jul 08 14:00:20 ip-172-31-1-136 sudo[882]: pam_unix(sudo:session): session closed for user root
Jul 08 14:00:20 ip-172-31-1-136 systemd[786]: Finished Apt Update.
Jul 08 14:00:20 ip-172-31-1-136 systemd[786]: apt-update.service: Consumed 4.463s CPU time.

あるいは、journalctlで詳細を確認できます。

$ journalctl --user -xeu apt-update.service
# ページャが開く(qキーで閉じる)

エラーがある場合

エラーがある場合は、その旨のメッセージが出ます。

詳細
メッセージ例
$ systemctl --user restart apt-update.service
Job for apt-update.service failed because the control process exited with error code.
See "systemctl --user status apt-update.service" and "journalctl --user -xeu apt-update.service" for details.

エラーメッセージが書いてくれているように、systemctl --user status

$ systemctl --user status apt-update.service
× apt-update.service - Apt Update
     Loaded: loaded (/home/ubuntu/.config/systemd/user/apt-update.service; disabled; vendor preset: enabled)
     Active: failed (Result: exit-code) since Fri 2022-07-08 14:02:53 UTC; 14min ago
    Process: 1228 ExecStart=/usr/bin/sudoo /usr/bin/apt update -y (code=exited, status=203/EXEC)
   Main PID: 1228 (code=exited, status=203/EXEC)
        CPU: 769us

Jul 08 14:02:53 ip-172-31-1-136 systemd[786]: Starting Apt Update...
Jul 08 14:02:53 ip-172-31-1-136 systemd[1228]: apt-update.service: Failed to locate executable /usr/bin/sudoo: No such file or directory
Jul 08 14:02:53 ip-172-31-1-136 systemd[1228]: apt-update.service: Failed at step EXEC spawning /usr/bin/sudoo: No such file or directory
Jul 08 14:02:53 ip-172-31-1-136 systemd[786]: apt-update.service: Main process exited, code=exited, status=203/EXEC
Jul 08 14:02:53 ip-172-31-1-136 systemd[786]: apt-update.service: Failed with result 'exit-code'.
Jul 08 14:02:53 ip-172-31-1-136 systemd[786]: Failed to start Apt Update.

journalctl --user -xeu

$ journalctl --user -xeu apt-update.service
# ページャが開く(qキーで閉じる)

エラー内容を確認して、ユニットファイルを修正しましょう。

修正が完了したら、

$ systemctl --user daemon-reload

で変更を反映したあと、

$ systemctl --user restart apt-update.service

で再実行して、再度様子を確認してみます。

(注意点として、systemctl --user statusjournalctl --userの結果には、前回の失敗時のログも混ざっています。時刻で判断しましょう。)

$ systemctl --user status apt-update.service
○ apt-update.service - Apt Update
     Loaded: loaded (/home/ubuntu/.config/systemd/user/apt-update.service; disabled; vendor preset: enabled)
     Active: inactive (dead)

Jul 08 14:17:48 ip-172-31-1-136 systemd[1353]: apt-update.service: Failed at step EXEC spawning /usr/bin/sudoo: No such file or directory
Jul 08 14:17:48 ip-172-31-1-136 systemd[786]: apt-update.service: Main process exited, code=exited, status=203/EXEC
Jul 08 14:17:48 ip-172-31-1-136 systemd[786]: apt-update.service: Failed with result 'exit-code'.
Jul 08 14:17:48 ip-172-31-1-136 systemd[786]: Failed to start Apt Update.
# ここから下が今回の再実行のログ(実際は境目はないので、時刻の部分で判断する)
Jul 08 14:27:43 ip-172-31-1-136 systemd[786]: Starting Apt Update...
Jul 08 14:27:43 ip-172-31-1-136 sudo[1421]:   ubuntu : PWD=/home/ubuntu ; USER=root ; COMMAND=/usr/bin/apt update -y
Jul 08 14:27:43 ip-172-31-1-136 sudo[1421]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=1000)
Jul 08 14:27:45 ip-172-31-1-136 sudo[1421]: pam_unix(sudo:session): session closed for user root
Jul 08 14:27:45 ip-172-31-1-136 systemd[786]: Finished Apt Update.
Jul 08 14:27:45 ip-172-31-1-136 systemd[786]: apt-update.service: Consumed 1.503s CPU time.

1-4. ブート後に実行されるサービスとして設定

問題なさそうであれば、enableします:

$ systemctl --user enable apt-update.service
Created symlink /home/ubuntu/.config/systemd/user/default.target.wants/apt-update.service → /home/ubuntu/.config/systemd/user/apt-update.service.

状態を確認してみましょう。特に、list-dependenciesの一覧に作成したapt-update.serviceが加わっていることが重要です:

  • status

    $ systemctl --user status apt-update.service
    ○ apt-update.service - Apt Update
         Loaded: loaded (/home/ubuntu/.config/systemd/user/apt-update.service; enabled; vendor preset: enabled)
         Active: inactive (dead)
    # Loaded行の、ユニットファイルのパスの次が、enabledになっている
    
  • is-enabled

    $ systemctl --user is-enabled apt-update.service
    enabled
    
  • list-dependencies大事!

    $ systemctl --user list-dependencies
    default.target
    ○ ├─apt-update.service # ここに追加されている!
    ● └─basic.target
    ●   ├─paths.target
    ●   ├─sockets.target
    ●   │ ├─dbus.socket
    ●   │ ├─dirmngr.socket
    ●   │ ├─gpg-agent-browser.socket
    ●   │ ├─gpg-agent-extra.socket
    ●   │ ├─gpg-agent-ssh.socket
    ●   │ ├─gpg-agent.socket
    ●   │ ├─pk-debconf-helper.socket
    ●   │ └─snapd.session-agent.socket
    ●   └─timers.target
    

1-5. サーバを再起動して、動作確認

enabledになっていることを確認したら、実際にサーバを再起動してみましょう:

$ sudo reboot

再起動後ログインし、実行結果を確認してみましょう:

$ systemctl --user status apt-update.service
○ apt-update.service - Apt Update
     Loaded: loaded (/home/ubuntu/.config/systemd/user/apt-update.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Fri 2022-07-08 15:03:04 UTC; 1min 17s ago
    Process: 796 ExecStart=/usr/bin/sudo /usr/bin/apt update -y (code=exited, status=0/SUCCESS)
   Main PID: 796 (code=exited, status=0/SUCCESS)
        CPU: 4.351s

Jul 08 15:02:57 ip-172-31-1-136 systemd[789]: Starting Apt Update...
Jul 08 15:02:58 ip-172-31-1-136 sudo[796]:   ubuntu : PWD=/home/ubuntu ; USER=root ; COMMAND=/usr/bin/apt update -y
Jul 08 15:02:58 ip-172-31-1-136 sudo[796]: pam_unix(sudo:session): session opened for user root(uid=0) by (uid=1000)
Jul 08 15:03:04 ip-172-31-1-136 sudo[796]: pam_unix(sudo:session): session closed for user root
Jul 08 15:03:04 ip-172-31-1-136 systemd[789]: Finished Apt Update.
Jul 08 15:03:04 ip-172-31-1-136 systemd[789]: apt-update.service: Consumed 4.351s CPU time.
$ journalctl --user -xeu apt-update.service
# ページャが開く(qキーで閉じる)

2. 実行時刻とenvコマンドの結果を、~/record_env_info.txtというファイルに追記するサービスの実装

systemdユーザインスタンス自動実行時のPATHやらなんやらを確認したい、というモチベーションのサービス、ということにします: サービス内で環境変数を使う際の注意点 < やり方要点まとめ

1. sudo apt update -yするサービスの実装と同じフローの部分は省略気味にします。

2-1. ユニットファイルの作成

まず、ユニットファイル「~/.config/systemd/user/record-env-info.service」を作成します。

ポイントは、

  • Type=onshotにした[4:2]

  • 実行コードは、ワンライナーで収まらないので、シェルスクリプトにした

    • スクリプトは/usr/local/sbin配下に配置した[6][7]
  • 実行スクリプトは絶対パスで指定した[2:4]

  • WantedBy=default.target

~/.config/systemd/user/record-env-info.service
[Unit]
Description=Record env Information

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/record_env_info.sh

[Install]
WantedBy=default.target

2-2. シェルスクリプトの作成

次に、呼び出すシェルスクリプト「/usr/local/sbin/record_env_info.sh」を作成します。

/usr/local/sbin[7:1]配下のファイルなので、sudoつきでエディタを起動する必要があります。
(~配下などで一旦作成して、sudo mvしてもよいです。)

/usr/local/sbin/record_env_info.sh
#!/bin/bash

RECORD_STR="[$(date "+%Y/%m/%d %H:%M:%S")]\n$(env)"
echo -e $RECORD_STR >> ~/record_env_info.txt

一応、実行権限の設定をしておきます。

$ sudo chmod 755 /usr/local/sbin/record_env_info.sh

$ ls -l /usr/local/sbin/record_env_info.sh
-rwxr-xr-x 1 root root 110 Jul  9 02:03 /usr/local/sbin/record_env_info.sh

2-3. ユーザユニットファイルの変更を反映

daemon-reloadサブコマンドで、ユニットファイルの変更を反映します:

# ユーザユニットファイルの変更を反映
$ systemctl --user daemon-reload
状態確認 (status, is-enabled, list-dependencies)
# status
$ systemctl --user status record-env-info.service
○ record-env-info.service - Record env Information
     Loaded: loaded (/home/ubuntu/.config/systemd/user/record-env-info.service; disabled; vendor preset: enabled)
     Active: inactive (dead)

# is-enabled
$ systemctl --user is-enabled record-env-info.service
disabled

# (ついでに確認)現時点では「record-env-info.service」はログイン直後の実行サービスに含まれていない
$ systemctl --user list-dependencies
default.target
○ ├─apt-update.service
● └─basic.target
●   ├─paths.target
●   ├─sockets.target
●   │ ├─dbus.socket
●   │ ├─dirmngr.socket
●   │ ├─gpg-agent-browser.socket
●   │ ├─gpg-agent-extra.socket
●   │ ├─gpg-agent-ssh.socket
●   │ ├─gpg-agent.socket
●   │ ├─pk-debconf-helper.socket
●   │ └─snapd.session-agent.socket
●   └─timers.target

2-4. 手動実行して動作確認

restart(今回はType=oneshotなので、startでもよい)サブコマンドで、一度手動実行してみます。

$ systemctl --user restart record-env-info.service

成功した場合は、エラーが出ない他に、「~/record_env_info.txt」というファイルが作成されていることも確認できるはずです。

$ ls ~
record_env_info.txt

$ cat ~/record_env_info.txt
[2022/07/09 02:21:09]
SHELL=/bin/bash QT_ACCESSIBILITY=1 PWD=/home/ubuntu LOGNAME=ubuntu SYSTEMD_EXEC_PID=1393 HOME=/home/ubuntu LANG=C.UTF-8 INVOCATION_ID=e9b31fd383a9477b967ff9c75551cf8d MANAGERPID=808 USER=ubuntu SHLVL=0 XDG_RUNTIME_DIR=/run/user/1000 JOURNAL_STREAM=8:22384 XDG_DATA_DIRS=/usr/local/share/:/usr/share/:/var/lib/snapd/desktop PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus _=/usr/bin/env

ログ確認のコマンド

$ systemctl --user status record-env-info.service
$ journalctl --user -xeu record-env-info.service
# ページャが開く(qキーで閉じる)

2-5. ブート後に実行されるサービスとして設定

問題なさそうであれば、enableします:

$ systemctl --user enable record-env-info.service
Created symlink /home/ubuntu/.config/systemd/user/default.target.wants/record-env-info.service → /home/ubuntu/.config/systemd/user/record-env-info.service.

status, is-enabled, list-dependenciesで、うまく設定できたかを確認しましょう。

確認 (status, is-enabled, list-dependencies)
  • status

    $ systemctl --user status record-env-info.service
    ○ record-env-info.service - Record env Information
       Loaded: loaded (/home/ubuntu/.config/systemd/user/record-env-info.service; enabled; vendor preset: enabled)
       Active: inactive (dead)
    # Loaded行の、ユニットファイルのパスの次が、enabledになっている
    
  • is-enabled

    $ systemctl --user is-enabled record-env-info.service
    enabled
    
  • list-dependencies大事!

    $ systemctl --user list-dependencies
    default.target
    ○ ├─apt-update.service
    ○ ├─record-env-info.service # ここに追加されている!
    ● └─basic.target
    ●   ├─paths.target
    ●   ├─sockets.target
    ●   │ ├─dbus.socket
    ●   │ ├─dirmngr.socket
    ●   │ ├─gpg-agent-browser.socket
    ●   │ ├─gpg-agent-extra.socket
    ●   │ ├─gpg-agent-ssh.socket
    ●   │ ├─gpg-agent.socket
    ●   │ ├─pk-debconf-helper.socket
    ●   │ └─snapd.session-agent.socket
    ●   └─timers.target
    

2-6. サーバを再起動して、動作確認

enabledになっていることを確認したら、実際にサーバを再起動してみましょう:

$ sudo reboot

再起動後ログインし、実行結果を確認してみましょう:

$ systemctl --user status record-env-info.service
○ record-env-info.service - Record env Information
     Loaded: loaded (/home/ubuntu/.config/systemd/user/record-env-info.service; enabled; vendor preset: enabled)
     Active: inactive (dead) since Sat 2022-07-09 02:51:30 UTC; 6s ago
    Process: 800 ExecStart=/usr/local/sbin/record_env_info.sh (code=exited, status=0/SUCCESS)
   Main PID: 800 (code=exited, status=0/SUCCESS)
        CPU: 4ms

Jul 09 02:51:30 ip-172-31-1-136 systemd[792]: Starting Record env Information...
Jul 09 02:51:30 ip-172-31-1-136 systemd[792]: Finished Record env Information.
$ journalctl --user -xeu record-env-info.service
# ページャが開く(qキーで閉じる)
$ ls ~
record_env_info.txt
$ cat ~/record_env_info.txt
# 過去の実行時のデータ(省略)
# ...
[2022/07/09 02:51:30]
SHELL=/bin/bash QT_ACCESSIBILITY=1 PWD=/home/ubuntu LOGNAME=ubuntu SYSTEMD_EXEC_PID=800 HOME=/home/ubuntu LANG=C.UTF-8 INVOCATION_ID=c96d6336a9744ee4a6d51835c53f6ecc MANAGERPID=792 USER=ubuntu SHLVL=0 XDG_RUNTIME_DIR=/run/user/1000 JOURNAL_STREAM=8:18804 XDG_DATA_DIRS=/usr/local/share/:/usr/share/:/var/lib/snapd/desktop PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus _=/usr/bin/env

ついでに、このときのファイル「~/record_env_info.txt」の中身(envコマンドの結果)から、systemdユーザインスタンス自動実行時のPATHの状態なども確認できるので、よかったら眺めてみてください。
(~/.profileや~/bash_profile, ~/.bashrcに設定したPATHは、反映されていないことがわかる: サービス内で環境変数を使う際の注意点 < やり方要点まとめ

(おわり)

脚注
  1. ログアウトしてもユーザプロセスは終了しないので、次回のユーザサービスが実行されるのは、サーバ再起動後のタイミングになります:ログアウト時にユーザープロセスを終了 < systemd/ユーザー (Arch Wiki) ↩︎ ↩︎

  2. 環境変数 < 基本設定 < systemd/ユーザー (Arch Wiki) ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  3. ユーザーユニットを書く < systemd/ユーザー (Arch Wiki) ↩︎

  4. 10.6. systemd のユニットファイルの作成および変更 (Red Hat Customer Portal) ↩︎ ↩︎ ↩︎

  5. ENVIRONMENT.D(5) (Arch Linux Manual Pages) ↩︎

  6. /usr/local : Local hierarchy (Filesystem Hierarchy Standard) ↩︎

  7. Linux豆知識 214 「/bin」「/usr/bin」「/usr/local/bin」ディレクトリの使い分け (LinuC) ↩︎ ↩︎

Discussion