zaki work log

作業ログやら生活ログやらなんやら

[Linux] systemdのユニットファイルを自分で書いてプログラムをdaemon動作させてみる

systemdのユニットファイルを作成してみる。
rcスクリプト育ちだったのでsystemctlコマンドは慣れるのに時間はかかったといえさすがにもう息を吸うように使うようになったけど、systemdのユニットファイル周りは基本的な部分があまりわかってないのでお試し。

記載については、Red Hatのドキュメントを確認すれば必要な情報は揃う。

access.redhat.com

access.redhat.com

本エントリ中の作業ホストは意表をついてUbuntu 20.04で確認。

root@ubuntu-node:~# cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.2 LTS"

お題

daemonで動く適当なアプリケーションとして、以下で作ったPerlのソケットサーバーを使う。

qiita.com

ベースのソースはこちら。

github.com

実行するとTCP/8080でListenしてなんちゃってHTTPを喋るサーバーとして動作する。

コードの修正

systemdで停止を行う際に使用するための、実行プロセスのPIDをファイル出力するようにする。

open W, "> /var/run/sockserv.pid" or die;
print W $$;
close W;

Perlでは$$でプロセスIDを取得できる。
この値をファイル出力するコードを、for()で接続待ちのループに入る前に追加。

(forkしないスクリプトなので、実はこのファイルは不要だった)

ユニットファイルの作成

コアな部分の最低限必要そうな記述を行うと以下の通り。

[Unit]
Description=Perl Socket Server Sample
After=network.target

[Service]
Type=simple
PIDFile=/var/run/sockserv.pid
ExecStart=/usr/local/tcp-probe-example/src/sockserv.pl

[Install]
WantedBy=multi-user.target

パラメタ

man systemd.directivesで(どのmanを見ればいいかが)確認できる。

Description

概要。
systemctl status hogehogeとかの出力に使用される。

After

依存するユニット。
ここで指定したユニットがactiveになってから起動される。
ネットワークを使うプログラムであればnetworkを指定しておけばよさげ。

Type

今回のお題のスクリプトのように、実行するとフォアグラウンドで動作するスクリプトであればsimpleを指定する。
スクリプト自体がバックグラウンドで動作するようなものはforkingを使う。

詳しくは表10.10 [Service] セクションの重要なオプションを参照。

PIDFile

ユニットファイルで指定するプログラムが出力するPIDが記録されるファイルのパス。
停止時にこのPIDに対してkillする。

なんだけど、forkしない1プロセスで処理するスクリプトだとドキュメント見る限り不要っぽい。

ExecStart

プログラムの起動コマンド。
引数がある場合もここに全部書ける模様。

WantedBy

WantedByというか、[Install]セクションの設定は、systemctl enableを使った時の動作を定義。
WantedBy=multi-user.targetだと/etc/systemd/system/multi-user.target.wants/以下にsymlinkを作成する動作になる。

ユニットファイルの作成場所

どこにファイルを作ればいいかはman systemdで確認できる。

System unit directories
The systemd system manager reads unit configuration from various directories. Packages that want to install unit files shall place them in the directory returned by pkg-config systemd --variable=systemdsystemunitdir. Other directories checked are /usr/local/lib/systemd/system and /usr/lib/systemd/system. User configuration always takes precedence. pkg-config systemd --variable=systemdsystemconfdir returns the path of the system configuration directory. Packages should alter the content of these directories only with the enable and disable commands of the systemctl(1) tool. Full list of directories is provided in systemd.unit(5).

平たく言うと、「どこのディレクトリに置くと有効かはman systemd.unitで確認できる。主に使うのは/usr/lib/systemd/system/usr/local/lib/systemd/system」とのこと(超意訳)。

ということでパッケージインストールするやつじゃなくて自前のプログラムなので、/usr/local/lib/systemd/system以下に作ってみる。

# mkdir -p /usr/local/lib/systemd/system
# vi /usr/local/lib/systemd/system/sockserv.service

ここに前述の内容のユニットファイルを作成。

今回はsockservという名前のサービスとして登録してみるということで、sockserv.serviceというファイル名でファイル作成。

systemctl

status

root@ubuntu-node:~# systemctl status sockserv
● sockserv.service - Perl Socket Server Sample
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; disabled; vendor preset: enabled)
     Active: inactive (dead)

この時点でstatusで内容確認できる。

start

root@ubuntu-node:~# systemctl start sockserv
root@ubuntu-node:~# echo $?
0
root@ubuntu-node:~# systemctl status sockserv
● sockserv.service - Perl Socket Server Sample
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; disabled; vendor preset: enabled)
     Active: failed (Result: exit-code) since Sat 2021-04-10 11:39:21 UTC; 6s ago
    Process: 198244 ExecStart=/usr/local/tcp-probe-example/src/sockserv.pl (code=exited, status=203/EXEC)
   Main PID: 198244 (code=exited, status=203/EXEC)

Apr 10 11:39:21 ubuntu-node systemd[1]: Started Perl Socket Server Sample.
Apr 10 11:39:21 ubuntu-node systemd[1]: sockserv.service: Main process exited, code=exited, status=203/EXEC
Apr 10 11:39:21 ubuntu-node systemd[1]: sockserv.service: Failed with result 'exit-code'.

あら?

journalctl -xeで確認すると以下の通り。
スクリプトchmod 755してなかった笑

Apr 10 11:39:21 ubuntu-node systemd[1]: Started Perl Socket Server Sample.
-- Subject: A start job for unit sockserv.service has finished successfully
-- Defined-By: systemd
-- Support: http://www.ubuntu.com/support
-- 
-- A start job for unit sockserv.service has finished successfully.
-- 
-- The job identifier is 7009.
Apr 10 11:39:21 ubuntu-node systemd[198244]: sockserv.service: Failed to execute command: Permission denied
Apr 10 11:39:21 ubuntu-node systemd[198244]: sockserv.service: Failed at step EXEC spawning /usr/local/tcp-probe-example/src/sockserv.pl: Permission denied

修正

root@ubuntu-node:~# chmod 755 /usr/local/tcp-probe-example/src/sockserv.pl 

再実行

root@ubuntu-node:~# systemctl start sockserv
root@ubuntu-node:~# echo $?
0
root@ubuntu-node:~# systemctl status sockserv
● sockserv.service - Perl Socket Server Sample
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; disabled; vendor preset: enabled)
     Active: active (running) since Sat 2021-04-10 11:42:29 UTC; 17s ago
   Main PID: 198358 (sockserv.pl)
      Tasks: 1 (limit: 4619)
     Memory: 3.0M
     CGroup: /system.slice/sockserv.service
             └─198358 /usr/bin/perl /usr/local/tcp-probe-example/src/sockserv.pl

Apr 10 11:42:29 ubuntu-node systemd[1]: Started Perl Socket Server Sample.
Apr 10 11:42:29 ubuntu-node sockserv.pl[198358]: [DBG] server listening ... 0.0.0.0:8080
root@ubuntu-node:~# curl http://localhost:8080
Running Socket Server.
root@ubuntu-node:~# cat /var/run/sockserv.pid 
198358

systemctl stop sockservすることでこのプロセスIDを見てkillしてくれる。
停止時はこのファイルは削除される。

ちなみにPIDFileの指定が無い場合でも、systemctlが把握してるPIDは面倒みてくれるっぽくてちゃんと停止する模様。
今回はsimpleなので不要だったけど、forkingタイプだと、無いとkillされないかも。(未確認)

enable

デフォルトでは以下の通り。

root@ubuntu-node:~# systemctl is-enabled sockserv
disabled

OSブート時にも起動するにはenableを使う。

root@ubuntu-node:~# systemctl enable sockserv
Created symlink /etc/systemd/system/multi-user.target.wants/sockserv.service → /usr/local/lib/systemd/system/sockserv.service.

この通り、/etc/systemd/system/multi-user.target.wants/sockserv.serviceにsymlinkが作成される。

daemon-reload

動作に影響しないけど、ユニットファイルのDescriptionの内容をちょっと変更してみる。
(末尾に!追加)

[Unit]
Description=Perl Socket Server Sample!

この状態で、systemctl status sockservを実行すると、

root@ubuntu-node:~# systemctl status sockserv
Warning: The unit file, source configuration file or drop-ins of sockserv.service changed on disk. Run 'systemctl daemon-reload' to reload units.
● sockserv.service - Perl Socket Server Sample
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; enabled; vendor preset: enabled)

:
:

このように「ディスク上のunitファイルの内容が更新されとるで」と、systemctl daemon-reloadするように警告が表示さる。

root@ubuntu-node:~# systemctl daemon-reload 
root@ubuntu-node:~# systemctl status sockserv
● sockserv.service - Perl Socket Server Sample!
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; enabled; vendor preset: enabled)
:
:

更新されました。


ということで、自前のプログラムを自作のユニットファイルでdaemonとして動作させることができた。

というか実は、このユニットファイルのパスがどこかを知れたのが今回の最大の収穫だった。
今までなんとなく/etc/systemd/system/multi-user.target.wants/にあるファイルをチェックしてたけど、これはenableになってる分だけで、実体は(基本的に)/lib/systemd/system/にあって、他の場所もman systemd.unitで検索パスのリストを確認できる。