Systemdユニットの理解:サービス設定の深掘り

systemdサービスユニットの仕組みを学びます。Unit、Service、Install、オーバーライド、再起動、ログについて解説します。

Systemdユニットの理解:サービス設定の深掘り

Systemdユニットファイルは、サービスの起動方法、依存関係、実行ユーザー、障害発生時の動作を決定する小さなテキストファイルです。systemctl restart myapp.serviceが特定のアプリで動作し、別のアプリで動作しない理由を疑問に思ったことがあるなら、その答えは通常ユニットファイルにあります。

このガイドでは、管理者や開発者が最も頻繁に編集する.serviceユニットに焦点を当てます。同じシステムはソケット、タイマー、マウント、デバイス、パス、ターゲットも管理しますが、運用上のミスが最も顕著に現れるのはサービスファイルです。

Systemdユニットファイルとは?

Systemdユニットファイルは、特定のユニットの設定ディレクティブを含む単純なテキストファイルです。ユニットはsystemdによって管理されるリソースを表します。最も一般的なタイプはサービスユニットで、バックグラウンドプロセスやアプリケーションの起動、停止、再起動、管理方法を定義します。

ユニットファイルはセクションに整理され、各セクションは角括弧([])で示されます。サービスユニットの最も重要なセクションは以下の通りです:

  • [Unit]:ユニットに関するメタデータ、依存関係、順序を含みます。
  • [Service]:サービスの動作自体を定義し、実行方法を含みます。
  • [Install]:ユニットを有効または無効にする方法を指定し、通常はターゲットユニットにリンクします。

Systemdはいくつかの標準ディレクトリでユニットファイルを検索します。最も一般的なものは以下の通りです:

  • /etc/systemd/system/:ローカルで設定されたユニット用。デフォルトを上書きします。
  • /usr/lib/systemd/system/:多くのディストリビューションでパッケージによってインストールされたユニット用。
  • /lib/systemd/system/:Debian系のシステムでパッケージ提供のユニットに使用されます。

ユニットを検査する必要がある場合、パスを推測しないでください。代わりに以下を使用します:

systemctl cat nginx.service
systemctl show -p FragmentPath nginx.service

systemctl catは特に便利です。ベースユニットとドロップインオーバーライドの両方を表示するためです。これがsystemdが実際に使用しているバージョンです。

.serviceユニットファイルの構造

典型的な.serviceユニットファイルを分解して、その構成要素を理解しましょう。

[Unit]セクション

このセクションは、説明的な情報を提供し、ユニット間の関係を定義します。

  • Description=:サービスの人間が読める説明。
  • Documentation=:サービスのドキュメントへのURLまたはパス。
  • After=:このユニットがリストされたユニットの起動完了に起動することを指定します。
  • Requires=After=と似ていますが、リストされたユニットを必須にします。必須ユニットが起動に失敗した場合、このユニットも失敗します。
  • Wants=:より弱い依存関係の形式です。このユニットはそのWantsユニットの起動を試みますが、それらの失敗がこのユニットの起動を妨げることはありません。
  • Conflicts=:このユニットと同時に実行できないユニットを指定します。

[Unit]セクションの例:

[Unit]
Description=My Custom Web Server
Documentation=https://example.com/docs/my-web-server
After=network.target

これは、カスタムWebサーバーがネットワークが利用可能になった後に起動することを示しています。

よくある落とし穴:After=は順序を制御し、要件を制御しません。After=postgresql.serviceと記述した場合、PostgreSQLとこのサービスが両方ともトランザクションの一部である場合、systemdはPostgreSQLの後にサービスを起動しますが、自動的にPostgreSQLを引き込みません。アプリが同じトランザクションでPostgreSQLを本当に起動する必要がある場合は、Wants=postgresql.service、またはハードな依存関係の場合はRequires=postgresql.serviceも使用します。

それでも、依存関係はヘルスチェックではありません。After=network.targetは、DNSが機能すること、リモートAPIに到達可能であること、データベースが接続を受け入れていることを保証しません。アプリケーションには依然として適切なリトライ動作が必要です。

[Service]セクション

ここにサービスを実行するためのコアロジックが含まれます。

  • Type=:プロセスの起動タイプを定義します。一般的なタイプは以下の通りです:
    • simple(デフォルト):メインプロセスはExecStart=で起動されたものです。Systemdは、ExecStart=プロセスがフォークされた直後にサービスが起動したと見なします。
    • forking:子プロセスをフォークして終了する従来のデーモンに使用されます。Systemdは親プロセスが終了するのを待ちます。
    • oneshot:単一のコマンドを実行して終了するタスク用。
    • notify:サービスは起動完了時にsystemdに通知を送信します。
    • dbus:D-Bus名を取得するサービス用。
  • ExecStart=:サービスを起動するために実行するコマンド。
  • ExecStop=:サービスを停止するために実行するコマンド。
  • ExecReload=:サービスを再起動せずに設定をリロードするために実行するコマンド。
  • Restart=:サービスを再起動するタイミングを定義します。オプションには、no(デフォルト)、on-successon-failureon-abnormalon-watchdogon-abortalwaysがあります。
  • RestartSec=:サービスを再起動する前に待機する時間。
  • User= / Group=:サービスを実行するユーザーとグループ。
  • WorkingDirectory=:実行されるプロセスの作業ディレクトリ。
  • Environment= / EnvironmentFile=:サービスの環境変数を設定します。

[Service]セクションの例:

[Service]
Type=simple
ExecStart=/usr/local/bin/my-web-server --config /etc/my-web-server.conf
User=www-data
Group=www-data
Restart=on-failure
RestartSec=5

この設定は、Webサーバーを起動し、www-dataユーザーとグループで実行し、失敗した場合は5秒の遅延で自動的に再起動します。

Type=は特に注意が必要です。多くの壊れたユニットは、古いinitスクリプトがデーモンモードを使用していたためにType=forkingを使用しています。フォアグラウンドで動作する最新のアプリケーションの場合、Type=simpleが通常は正しいです。プロセスがバックグラウンドにフォークするが、systemdに実際のメインプロセスを識別する方法が伝えられていない場合、ステータスレポートと再起動が誤解を招く可能性があります。

1回限りのジョブの場合は、Type=oneshotを使用し、完了したアクションをアクティブとしてカウントする必要がある場合は、多くの場合RemainAfterExit=yesを使用します。たとえば、ファイアウォールルールを準備したり、特別なリソースをマウントしたりするユニットは正常に終了するかもしれませんが、それでも気にする状態を表します。

[Install]セクション

このセクションは、ユニットを有効または無効にするときに使用されます。ユニットがsystemdのターゲットユニットとどのように統合されるかを定義します。

  • WantedBy=:有効にされたときに、このユニットを「欲する」ターゲットを指定します。起動時に開始する必要があるサービスの場合、multi-user.targetが一般的に使用されます。

[Install]セクションの例:

[Install]
WantedBy=multi-user.target

systemctl enable my-custom-service.serviceを実行すると、systemdは/etc/systemd/system/multi-user.target.wants/からサービスファイルへのシンボリックリンクを作成し、システムがマルチユーザーランレベルに達したときにサービスが確実に起動するようにします。

ユニットに[Install]セクションがない場合でも、完全に有効である可能性があります。別のインストールメカニズムが存在しない限り、systemctl enableで直接有効にすることはできません。一部のユニットは、手動で有効にするのではなく、依存関係、ソケット、タイマー、またはターゲットによって引き込まれることを意図しています。

カスタムサービスユニットの作成と管理

カスタムサービスユニットを作成するプロセスを順を追って見ていきましょう。

ステップ1:ユニットファイルの作成

/etc/systemd/system/.service拡張子の新しいファイルを作成します。例として、/etc/systemd/system/my-app.serviceを作成しましょう。

[Unit]
Description=My Custom Application Service
After=network.target

[Service]
Type=simple
ExecStart=/opt/my-app/bin/run-app --port 8080
User=appuser
Group=appgroup
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

重要な考慮事項:

  • ExecStartコマンドが、アクセス可能で実行権限を持つ実行可能スクリプトまたはバイナリを指していることを確認します。
  • 指定されたUserGroupが存在しない場合は作成します(sudo useradd -r -s /bin/false appusersudo groupadd appgroupsudo usermod -a -G appgroup appuser)。
  • 指定されたコマンドを使用してアプリケーションを正しく起動および停止できることを確認します。

ExecStart=にコマンドを入れる前に、可能であれば同じユーザーとして手動で実行します:

sudo -u appuser /opt/my-app/bin/run-app --port 8080

これにより、systemdが関与する前に、不足している実行ビット、不足しているディレクトリ、間違った相対パス、権限の問題をキャッチできます。手動で動作したら、ユニットに移動してsystemdに監視を任せます。

ステップ2:Systemd設定のリロード

ユニットファイルを作成または変更した後、systemdに設定をリロードするように指示する必要があります。

sudo systemctl daemon-reload

このコマンドは、新規または変更されたユニットファイルをスキャンし、systemdの内部状態を更新します。

ステップ3:サービスの有効化と起動

サービスをすぐに起動し、起動時に開始するように設定するには:

sudo systemctl enable my-app.service  # 起動時に開始するためのシンボリックリンクを作成
sudo systemctl start my-app.service   # 今すぐサービスを開始

ステップ4:サービスの管理

systemctlコマンドを使用してサービスを管理します:

  • ステータスの確認:

    sudo systemctl status my-app.service
    

    これにより、サービスがアクティブかどうか、そのプロセスID、最近のログエントリなどが表示されます。

  • サービスの停止:

    sudo systemctl stop my-app.service
    
  • サービスの再起動:

    sudo systemctl restart my-app.service
    
  • サービスのリロード(ExecReload=が定義されている場合):

    sudo systemctl reload my-app.service
    
  • サービスの無効化(起動時に開始しないようにする):

    sudo systemctl disable my-app.service
    

ステップ5:journalctlでログを表示する

Systemdはログ記録のためにjournaldと緊密に統合されています。journalctlを使用してサービスのログを表示できます:

  • 特定のサービスのログを表示:

    sudo journalctl -u my-app.service
    
  • リアルタイムでログを追跡:

    sudo journalctl -f -u my-app.service
    
  • 最後の起動以降のログを表示:

    sudo journalctl -b -u my-app.service
    

ベストプラクティスとヒント

  • 最新のアプリケーションにはType=notifyを使用: アプリケーションがサポートしている場合、Type=notifyはsystemdとのより良い統合を提供し、サービスの準備状態を正確に追跡できるようにします。
  • 非rootユーザーとしてサービスを実行: セキュリティリスクを最小限に抑えるために、[Service]セクションで常にUser=Group=を指定します。
  • 依存関係を慎重に定義: After=Requires=Wants=を使用して、サービスが正しい順序で起動し、重要な依存関係が満たされるようにします。
  • Restart=を活用: 適切な再起動ポリシーを設定して、サービスの可用性を確保します。
  • ユニットファイルをシンプルに保つ: 複雑な起動シーケンスの場合は、ユニットファイルに複雑なコマンドを直接記述するのではなく、ExecStart=で呼び出されるラッパースクリプトの使用を検討します。
  • systemctl cat <unit>を使用: オーバーライドを含む、systemdが見るユニットファイルの完全な内容を表示します。
  • systemctl edit <unit>を使用: このコマンドは、既存のユニットのオーバーライドファイルを作成するためのエディタを開きます。これは、デフォルトのユニットファイルを直接編集するよりもクリーンな方法です。

既存のユニットを安全に編集する

使い捨てのマシンをデバッグしている場合を除き、/usr/lib/systemd/system/または/lib/systemd/system/にあるパッケージ所有のユニットを編集しないでください。パッケージのアップグレードによってこれらのファイルが置き換えられる可能性があります。代わりにオーバーライドを使用します:

sudo systemctl edit nginx.service

これにより、/etc/systemd/system/nginx.service.d/の下にドロップインが作成されます。たとえば、再起動ポリシーを追加するには:

[Service]
Restart=on-failure
RestartSec=5s

一部のディレクティブは複数回指定できます。その他は、置き換える前にクリアする必要があります。ExecStart=は典型的な例です:

[Service]
ExecStart=
ExecStart=/usr/local/bin/my-nginx-wrapper

空白のExecStart=行は、以前の値をリセットします。これがないと、systemdがユニットを拒否したり、意図したよりも多くのコマンドを保持したりする可能性があります。

ユニットまたはドロップインを変更した後は、同じレビューループを使用します:

sudo systemctl daemon-reload
systemctl cat my-app.service
sudo systemctl restart my-app.service
journalctl -u my-app.service -n 50 --no-pager

ユニットファイルは、3つのジョブを分離すれば難しくありません。[Unit]は関係を記述し、[Service]はプロセスの動作を記述し、[Install]は有効化を記述します。実際のデバッグのほとんどは、これらのジョブのどれが間違った前提で設定されたかを見つけることです。

現実的なサービスファイルのウォークスルー

以下は、Python Webアプリケーションのための小規模ですが現実的なサービスです:

[Unit]
Description=Inventory API
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=inventory
Group=inventory
WorkingDirectory=/srv/inventory-api
EnvironmentFile=/etc/inventory-api/env
ExecStart=/srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

このファイルには、いくつかの静かな決定があります。サービスはrootではなくinventoryとして実行されます。コマンドは仮想環境のgunicornへの絶対パスを使用するため、インタラクティブシェルのPATHに依存しません。アプリはlocalhostにバインドします。これは、リバースプロキシが公開するためです。環境ファイルはユニットの外部にあるため、デプロイメントはパッケージ所有のサービスメタデータを書き換えることなく設定を更新できます。

依存関係の行は意図的に控えめです。After=postgresql.serviceは、PostgreSQLが同じ起動トランザクションの一部である場合に順序を制御します。データベースが接続の準備ができていることを証明するものではなく、アプリケーションのリトライロジックを置き換えるものでもありません。network-online.targetは、ネットワークの準備状態を正しく実装するシステムで役立ちますが、すべてのリモート依存関係に到達可能であるという普遍的な保証ではありません。

このサービスが失敗した場合、最初のチェックは予測可能です:

systemctl status inventory-api.service
journalctl -u inventory-api.service -b --no-pager
systemctl cat inventory-api.service
sudo -u inventory /srv/inventory-api/.venv/bin/gunicorn app:app --bind 127.0.0.1:9000

最後のコマンドは、本番環境で実行し続けるものではありません。これは、「設定されたユーザーはこのコマンドをまったく実行できますか?」と尋ねる診断チェックです。アプリをインポートできない、環境ファイルを読み取れない、ログディレクトリに書き込めない場合、systemdはそれを修正してくれません。

よく見かけるリソースとセキュリティディレクティブ

多くの本番ユニットには、ハードニングやリソース制御が含まれています。いくつかの一般的な例:

[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
MemoryMax=512M
CPUQuota=80%

これらのディレクティブは非常に便利ですが、前提を壊す可能性もあります。PrivateTmp=trueはサービスにプライベートな/tmpを提供するため、別のプロセスがそこに書き込まれたファイルを認識できない場合があります。ProtectHome=trueは、/home/root/run/userへのアクセスをブロックする可能性があります。ProtectSystem=fullは、サービスの観点からシステムの大部分を読み取り専用にします。アプリが以前書き込めていた場所に突然書き込めなくなった場合は、アプリを非難する前にハードニング設定を調べてください。

リソース制限にも同じトレードオフがあります。MemoryMax=は1つのサービスがマシン全体を消費するのを防ぐことができますが、値が低すぎると、通常の負荷でサービスが強制終了される可能性があります。制限を引き上げたり削除したりする前に、ジャーナルでメモリ不足メッセージを確認し、実際の使用量と比較してください。

最も便利なデバッグコマンド

サービスユニットを扱うときは、これらを手元に置いてください:

systemctl status my-app.service
systemctl cat my-app.service
systemctl show my-app.service
systemd-analyze verify /etc/systemd/system/my-app.service
journalctl -u my-app.service -b --no-pager

systemctl showは冗長ですが、ユニットを解析した後にsystemdが計算したプロパティを公開します。これにより、デフォルト、ドロップイン、またはリセットディレクティブから継承された驚くべき値が明らかになることがあります。systemd-analyze verifyは、サービスを再起動する前に、いくつかの構文エラーと依存関係エラーをキャッチします。アプリケーションのテストに代わるものではありませんが、実行する価値があるほど十分なミスをキャッチします。