systemdユニットファイルを正確に書く
一言でいうと
ユニットファイルで最も重要な1行は、Type= です。systemdが「このサービスはいつ起動が完了したか」を判断する基準だからです。
なぜ必要なのか
systemctl start myappが30秒間止まったあと、失敗します。ところが、プロセスは正常に起動しています。このような状況の原因は、たいていTypeです。
どう動くのか
ユニットファイルの場所と優先度
| パス | 優先度 | 用途 |
|---|---|---|
/etc/systemd/system/ |
高い | 管理者のカスタムユニット(オーバーライド) |
/run/systemd/system/ |
中 | ランタイム生成 |
/usr/lib/systemd/system/ |
低い | パッケージのインストールによるデフォルトユニット |
パッケージが提供するユニットを直接編集してはいけません。更新で上書きされて消えます。systemctl edit <유닛>で/etc/systemd/system/<유닛>.d/override.confを作るのが定石です(プレースホルダーはユニット名です)。
3つのセクション
[Unit]
Description=My Web Application
Documentation=https://wiki.internal/myapp
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
ConditionPathExists=/etc/myapp/config.yaml
[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=-/etc/myapp/env
ExecStartPre=/opt/myapp/bin/check-config --validate
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
TimeoutStartSec=30
StandardOutput=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
AfterとRequiresは違います。Afterは順序だけを決め、Requiresは依存を作ります。順序なしに依存だけを指定すると、2つのサービスが同時に起動することがあります。通常は、両方を一緒に使います。
EnvironmentFile=-のハイフンは「ファイルがなくても失敗しない」という意味です。
Typeの比較
| Type | 起動完了の判断 | 適したサービス |
|---|---|---|
simple(デフォルト) |
ExecStartのプロセスが開始された直後 | フォアグラウンドのデーモン |
exec |
バイナリのexec()が成功したとき | simpleより正確 |
forking |
ExecStartが終了して子プロセスが残ったとき | 従来型のforkデーモン。PIDFile=が必要 |
oneshot |
ExecStartが完全に終了したとき | 初期化スクリプト |
notify |
サービスがsd_notify(READY=1)を送ったとき |
準備完了を自分で知らせるサービス |
dbus |
D-Busの名前が登録されたとき | BusName=が必要 |
最もよくある間違いが、Type=simpleなのにプロセスがデーモン化する(forkして親が終了する)場合です。systemdは、親が死ぬことをサービスの終了とみなして失敗扱いにします。逆に、Type=forkingなのにフォアグラウンドで動くプログラムだと、systemdは親が死なないので永遠に待ちます。そしてTimeoutStartSecに引っかかって、強制終了します。「起動が30秒止まって失敗する」の典型的な原因です。
本番環境での推奨はType=notifyです。サービスが実際にリクエストを処理する準備ができた時点を正確に知れるので、依存するサービスの起動が安全になります。
よく起きる5つの間違い
- ExecStartでシェルの機能を使う。パイプ、リダイレクト、変数展開、ワイルドカードは動作しません。systemdがシェルなしでexecするからです。必要なら
ExecStart=/bin/bash -c '...'で包みます。 - 相対パス。
ExecStart=myappは失敗します。絶対パスである必要があります。 [Install]がない。そうすると、systemctl enableが何もしません。起動時の自動開始ができません。Restart=always+RestartSec=0。クラッシュループがシステムを麻痺させます。StartLimitIntervalSec/StartLimitBurstで上限を設けます。Type=forkingなのにPIDFile=がない。systemdがメインプロセスを見つけられず、追跡がずれます。
セキュリティ強化
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
ProtectSystem=strictは、/usr、/boot、/etcを読み取り専用にします。書き込みが必要なパスは、ReadWritePathsで例外にします。これを有効にしたあとでサービスが起動しなければ、たいてい書き込みパスの漏れです。
タイマー
cronの代わりに使えます。.timerと.serviceが対になります。
# labhub-backup.timer
[Unit]
Description=Nightly backup
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
Unit=labhub-backup.service
[Install]
WantedBy=timers.target
Persistent=trueは、システムが停止していて逃した実行を、起動後すぐに実行します。cronにはない機能です。
現場での姿
systemd-analyze verify <유닛>で構文と参照を検査できます(プレースホルダーはユニット名です)。デプロイ前にこれを実行するのは、よい習慣です。
次のラボですること
ユニットファイルを最初から作成し、タイマーのペアを作り、ユニット検証スクリプトを自分で作ります。採点ツールが、正常なユニットと不良のユニットの両方で実行します。