systemdユニットファイルの作成
目標
systemdのユニットファイルを最初から作成し、タイマーのペアを作り、ユニット検証スクリプトを自分で作ります。
なぜ重要なのか
ユニットファイルで最も重要な1行はType=です。Type=simpleなのにプロセスがデーモン化すると、systemdは親が死ぬことをサービスの終了とみなして失敗扱いにします。逆にType=forkingなのにフォアグラウンドで動くと、systemdは永遠に待ち続けたあと、タイムアウトで強制終了します。「起動が30秒止まって失敗する」の典型的な原因です。
そして[Install]セクションがないと、systemctl enableが何もせず、起動時の自動開始ができません。これは、再起動するまで誰も気づきません。
このラボはAlmaLinux 9の仮想マシンで動きます。systemdがPID 1として実際に動いているので、ユニットファイルを書いたあと、systemctl enable --nowですぐに起動して試せます。ファイルの1行がどのような動作につながるかを、目で確認しながら学びます。\n\n以前はPodで動いていたため、systemctl自体がなく、作成と検証までがすべてでした。最初の起動には30秒ほどかかります。
ステップ
/etc/systemd/system/labhub-api.serviceを作成し、[Unit]、[Service]、[Install]の3つのセクションを入れてください。/root/unitの作業ディレクトリも作成してください。[Unit]に、Description、Documentation、After=network-online.target、Wants=network-online.targetの4行を入れてください。[Service]に、Type=notify、ExecStart=/usr/local/bin/labhub-api --config /etc/labhub/api.yaml、User=labhub、Group=labhub、WorkingDirectory=/opt/labhubを入れてください。ExecStartは絶対パスである必要があります。\n\n ラボのプログラムがシェルスクリプトなので、NotifyAccess=allも一緒に入れます。デフォルト値のmainは、MainPIDが送った通知だけを受け付けますが、systemd-notifyはスクリプトの子なので、PIDが違います。そうすると、systemdが準備完了のシグナルを受け取れず、待ったあげくにJob for labhub-api.service failed because a timeout was exceededで終わります。ユニットファイルには何も問題がないように見えます。- 再起動ポリシーを追加してください。
[Service]にRestart=on-failureとRestartSec=5を、[Unit]にStartLimitIntervalSec=300とStartLimitBurst=5を入れます。この2つは、systemd 230で[Service]から[Unit]へ移りました。[Service]に書くと、Unknown key nameだけを残して、黙って無視され、起動回数の制限がかかりません。 - セキュリティディレクティブを追加してください。
NoNewPrivileges=true、ProtectSystem=strict、ProtectHome=true、PrivateTmp=true、ReadWritePaths=/var/lib/labhub /var/log/labhub [Install]にWantedBy=multi-user.targetを入れ、systemctl enableが作るシンボリックリンクを自分で作成してください。パスは/etc/systemd/system/multi-user.target.wants/labhub-api.serviceで、元のユニットを指している必要があります。- タイマーのペアを作成してください。
/etc/systemd/system/labhub-backup.service(Type=oneshot、ExecStartは絶対パス)と、/etc/systemd/system/labhub-backup.timer([Timer]にOnCalendar=*-*-* 02:30:00、Persistent=true、Unit=labhub-backup.service、[Install]にWantedBy=timers.target)です。 /root/unit/lint.shを作成してください。第1引数でユニットファイルのパスを受け取り、次の3つのルールを検査して、違反がなければ終了コード0、あれば0以外の値で終了する必要があります。- ルール1:
[Unit]、[Service]、[Install]の3つのセクションがすべてある必要があります - ルール2:
ExecStartの値が絶対パス(/で始まる)である必要があります - ルール3:
Type=forkingなら、PIDFile=がある必要があります 採点ツールが、作成したlabhub-api.service(合格する必要があります)と/opt/fixtures/unit/bad-sample.service(不合格になる必要があります)の両方で実行します。
- ルール1:
参考
- シンボリックリンクは
ln -sf /etc/systemd/system/labhub-api.service /etc/systemd/system/multi-user.target.wants/labhub-api.serviceで作ります。ディレクトリを先に作成してください。 ExecStartでは、シェルの機能(パイプ、リダイレクト、ワイルドカード)が動作しません。必要なら/bin/bash -c '...'で包みます。- このラボはAlmaLinux 9のVMで動き、systemdがPID 1です。そのため、書いたユニットを
systemctl startですぐに起動して試せ、ステップ8が実際にそのように採点します。以前はPodで動いていたため、systemctl自体がありませんでした。 - ステップ6のシンボリックリンクは、手で作ってもかまいませんが、
systemctl enableが作るものと結果が同じであることを、一度確認しておいてください。あとで「enableしたのに、なぜ起動しないのか」に出会ったときに、まずシンボリックリンクを見るようになります。 - よくある間違い1: ステップ8のスクリプトが、セクションヘッダーを部分文字列でマッチさせ、
[Unit]と[Unite]を区別できない場合です。 - よくある間違い2: ステップ6で、シンボリックリンクではなくファイルをコピーする場合です。systemdは、シンボリックリンクで管理します。
3つのセクションの骨格
/etc/systemd/system/labhub-api.serviceを作成し、[Unit]、[Service]、[Install]の3つのセクションを入れてください。/root/unitの作業ディレクトリも作成してください。
角括弧で囲んだセクション名が3つ必要です。順序にも慣例があります。
[Unit]セクションを埋める
[Unit]に、Description、Documentation、After=network-online.target、Wants=network-online.targetの4行を入れてください。
説明、ドキュメント、順序、依存の4つを書きます。順序と依存は、別のディレクティブです。
[Service]の実行設定
[Service]に、Type=notify、ExecStart=/usr/local/bin/labhub-api --config /etc/labhub/api.yaml、User=labhub、Group=labhub、WorkingDirectory=/opt/labhubを入れてください。ExecStartは絶対パスである必要があります。\n\n ラボのプログラムがシェルスクリプトなので、NotifyAccess=allも一緒に入れます。デフォルト値のmainは、MainPIDが送った通知だけを受け付けますが、systemd-notifyはスクリプトの子なので、PIDが違います。そうすると、systemdが準備完了のシグナルを受け取れず、待ったあげくにJob for labhub-api.service failed because a timeout was exceededで終わります。ユニットファイルには何も問題がないように見えます。
タイプと実行コマンドが核心です。実行パスは必ず絶対パスである必要があります。
再起動ポリシー
再起動ポリシーを追加してください。[Service]にRestart=on-failureとRestartSec=5を、[Unit]にStartLimitIntervalSec=300とStartLimitBurst=5を入れます。この2つは、systemd 230で[Service]から[Unit]へ移りました。[Service]に書くと、Unknown key nameだけを残して、黙って無視され、起動回数の制限がかかりません。
無限ループを防ぐ上限のディレクティブが2つあります。間隔も0であってはいけません。
セキュリティ強化
セキュリティディレクティブを追加してください。NoNewPrivileges=true、ProtectSystem=strict、ProtectHome=true、PrivateTmp=true、ReadWritePaths=/var/lib/labhub /var/log/labhub
読み取り専用にするディレクティブと、例外のパスを指定するディレクティブを、一緒に使います。
[Install]とenableの結果
[Install]にWantedBy=multi-user.targetを入れ、systemctl enableが作るシンボリックリンクを自分で作成してください。パスは/etc/systemd/system/multi-user.target.wants/labhub-api.serviceで、元のユニットを指している必要があります。
enableが作るシンボリックリンクのパスを自分で作ってみると、構造が理解できます。
タイマーのペアの作成
タイマーのペアを作成してください。/etc/systemd/system/labhub-backup.service(Type=oneshot、ExecStartは絶対パス)と、/etc/systemd/system/labhub-backup.timer([Timer]にOnCalendar=*-*-* 02:30:00、Persistent=true、Unit=labhub-backup.service、[Install]にWantedBy=timers.target)です。
.timerと.serviceが対です。逃した実行に追いつくためのディレクティブも入れてください。
ユニット検証スクリプト
/root/unit/lint.shを作成してください。第1引数でユニットファイルのパスを受け取り、次の3つのルールを検査して、違反がなければ終了コード0、あれば0以外の値で終了する必要があります。
- ルール1:
[Unit]、[Service]、[Install]の3つのセクションがすべてある必要があります - ルール2:
ExecStartの値が絶対パス(/で始まる)である必要があります - ルール3:
Type=forkingなら、PIDFile=がある必要があります 採点ツールが、作成したlabhub-api.service(合格する必要があります)と/opt/fixtures/unit/bad-sample.service(不合格になる必要があります)の両方で実行します。
採点ツールが、正常なユニットとフィクスチャの不良ユニットの両方で実行します。3つのルールを検査してください。