カスタムSystemdユニットファイルを効果的に作成・管理する方法

この包括的なガイドで、Linuxサービスの管理技術を習得しましょう。カスタムsystemdユニットファイルの作成、設定、トラブルシューティングを学び、`ExecStart`、`WantedBy`、`Type`などの重要なディレクティブを活用します。ステップバイステップの手順と実践例を提供し、アプリケーションの起動を標準化し、信頼性の高い運用を確保し、カスタムプロセスをLinuxシステム環境にシームレスに統合する方法を解説します。堅牢なサービス管理を目指す開発者や管理者に必須の内容です。

カスタムSystemdユニットファイルを効果的に作成・管理する方法

カスタムsystemdユニットファイルは、ターミナルで動作するコマンドを、オペレーティングシステムが起動、停止、再起動、ログ記録、監視できるサービスに変換します。その違いは重要です。シェル内のコマンドは環境を継承し、セッションが終了すると終了します。一方、サービスには明示的なユーザー、作業ディレクトリ、再起動ポリシー、依存関係、リソース制限、ログがあります。

これは、小規模な内部API、ワーカー、サイドカースクリプト、単発のデーモンに対して私が使用する実践的な方法です。最もシンプルで正しいユニットを作成し、専用ユーザーとして実行し、ログをジャーナルに送り、実際の要件が現れた場合にのみ高度なディレクティブを追加します。

Systemdユニットファイルの理解

Systemdは、ユニットと呼ばれるさまざまなシステムリソースを管理し、これらは設定ファイルによって定義されます。これらのユニットには、サービス(.service)、マウントポイント(.mount)、デバイス(.device)、ソケット(.socket)などが含まれます。アプリケーションやバックグラウンドプロセスを管理するには、.serviceユニットタイプが最も一般的で関連性があります。

Systemdユニットファイルはプレーンテキストファイルで、通常は特定のディレクトリに保存されます。優先順位の高い順に、主な場所は次のとおりです。

  • /etc/systemd/system/:カスタムユニットファイルとオーバーライドに推奨される場所です。システムデフォルトよりも優先され、システム更新後も保持されます。
  • /run/systemd/system/:ランタイムで生成されたユニットファイルに使用されます。
  • /usr/lib/systemd/system/:インストールされたパッケージによって提供されるユニットファイルが含まれます。このディレクトリ内のファイルを直接変更しないでください。

カスタムユニットファイルを/etc/systemd/system/に配置することで、systemdによって適切に認識および管理されることが保証されます。

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

systemdの.serviceユニットファイルは、いくつかのセクションに構造化されており、各セクションは[SectionName]で示され、さまざまなディレクティブ(キーと値のペア)が含まれています。サービスユニットの3つの主要なセクションは、[Unit][Service][Install]です。

使用する最も重要なディレクティブを詳しく見ていきましょう。

[Unit] セクション

このセクションには、ユニット、その説明、依存関係に関する一般的なオプションが含まれます。

  • Description:サービスを説明する人間が読める文字列。systemctl statusの出力に表示されます。
    Description=My Custom Python Web Application
    
  • Documentation:サービスのドキュメントを指すURL(オプション)。
    Documentation=https://example.com/docs/my-app
    
  • After:このユニットがリストされたユニットのに起動することを指定します。起動順序の管理に役立ちます。Webアプリケーションの場合、ネットワークが起動していることを確認したい場合があります。
    After=network.target
    
  • Requires:強い依存関係。必要なユニットの起動に失敗した場合、このユニットは起動しません。必要なユニットが停止された場合、このユニットも停止される可能性があります。
    Requires=docker.service
    
  • Wants:弱い依存関係。対象のユニットが失敗した場合や見つからない場合でも、このユニットは起動を試みます。通常、Requiresよりも良いデフォルトです。
    Wants=syslog.target
    

[Service] セクション

このセクションでは、サービスの実行パラメータを定義します。これには、起動方法、停止方法、動作方法が含まれます。

  • Type:プロセスの起動タイプを定義します。systemdがサービスをどのように監視するかに重要です。

    • simple(デフォルト)ExecStartコマンドがサービスのメインプロセスです。Systemdは、ExecStartが呼び出された直後にサービスが開始されたと見なします。プロセスがフォアグラウンドで無期限に実行されることを期待します。
    • forkingExecStartコマンドが子プロセスをフォークし、親プロセスが終了します。Systemdは、親プロセスが終了するとサービスが開始されたと見なします。アプリケーションが自身をデーモン化する場合に使用します。
    • oneshotExecStartコマンドは、処理が完了すると終了する1回限りのプロセスです。タスクを実行して終了するスクリプト(バックアップスクリプトなど)に便利です。
    • notifysimpleと似ていますが、サービスが準備できたことをsystemdに通知します。これには、systemd通知のアプリケーションサポートが必要です。
    • idleExecStartコマンドは、すべてのジョブが完了した場合にのみ実行され、システムがほぼアイドル状態になるまで実行が遅延されます。
    Type=simple
    
  • ExecStart:サービス開始時に実行するコマンド。これはこのセクションで最も重要なディレクティブです。実行可能ファイルまたはスクリプトへの絶対パスを常に使用してください。

    ExecStart=/usr/bin/python3 /opt/my_app/app.py
    
  • ExecStop:サービス停止時に実行するコマンド(オプション)。指定しない場合、systemdはプロセスにSIGTERMを送信します。

    ExecStop=/usr/bin/pkill -f 'my_app/app.py'
    
  • ExecReload:サービスの設定をリロードするために実行するコマンド(オプション)。

    ExecReload=/bin/kill -HUP $MAINPID
    
  • User:サービスのプロセスが実行されるユーザーアカウント。セキュリティ上必須です。絶対に必要な場合を除き、rootは避けてください。

    User=myappuser
    
  • Group:サービスのプロセスが実行されるグループアカウント。

    Group=myappgroup
    
  • WorkingDirectory:実行されるコマンドの作業ディレクトリ。

    WorkingDirectory=/opt/my_app
    
  • Restart:サービスを自動的に再起動するタイミングを定義します。

    • no(デフォルト):再起動しません。
    • on-success:サービスが正常に終了した場合のみ再起動します。
    • on-failure:サービスがゼロ以外のステータスコードで終了した場合、またはシグナルによって強制終了された場合のみ再起動します。
    • always:終了ステータスに関係なく、常にサービスを再起動します。
    Restart=on-failure
    
  • RestartSec:サービスを再起動する前に待機する時間(例:5sは5秒)。

    RestartSec=5s
    
  • Environment:実行されるコマンドの環境変数を設定します。

    Environment="APP_ENV=production" "DEBUG=false"
    
  • EnvironmentFile:ファイルから環境変数を読み取ります。各行はKEY=VALUEである必要があります。

    EnvironmentFile=/etc/default/my_app
    
  • LimitNOFILE:サービスに許可されるオープンファイル記述子の最大数を設定します(例:100000)。高並行アプリケーションにとって重要です。

    LimitNOFILE=65536
    

[Install] セクション

このセクションでは、サービスが起動時に自動的に開始できるようにする方法を定義します。

  • WantedBy:このサービスを「必要とする」ターゲットユニットを指定します。ターゲットユニットが有効になると、このサービスはその.wantsディレクトリにシンボリックリンクされ、事実上ターゲットとともに起動するようになります。
    • multi-user.target:ほとんどのサーバーサービスに標準的なターゲットで、非グラフィカルなマルチユーザーログインを備えたシステムを示します。
    • graphical.target:グラフィカル環境を必要とするサービス向け。
    WantedBy=multi-user.target
    
  • RequiredByWantedByと似ていますが、より強い依存関係です。ターゲットが有効になっている場合、このユニットも有効になり、このユニットが失敗した場合、ターゲットも失敗します。

サーバー上でバックグラウンドで実行されることを意図したほとんどのカスタムサービスでは、Type=simpleWantedBy=multi-user.targetが適切な出発点です。アプリケーションがすでに自身をデーモン化している場合は、その動作を無効にするか、注意してType=forkingを使用してください。フォアグラウンドプロセスの方がsystemdが監視しやすいです。

ステップバイステップ:カスタムSystemdサービスの作成と管理

実用的な例を作成しましょう。指定されたディレクトリからファイルを提供するシンプルなPython HTTPサーバーです。これをsystemdサービスとしてセットアップします。

ステップ1:アプリケーション/スクリプトの準備

まず、アプリケーションスクリプトを作成します。この例では、シンプルなPython HTTPサーバーを使用します。アプリケーション用のディレクトリ(例:/opt/my_app)を作成し、その中にapp.pyを配置します。

# /opt/my_app/app.py

import http.server
import socketserver
import os

PORT = int(os.environ.get("PORT", 8000))
DIRECTORY = os.environ.get("DIRECTORY", os.getcwd())

class Handler(http.server.SimpleHTTPRequestHandler):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, directory=DIRECTORY, **kwargs)

print(f"Serving directory {DIRECTORY} on port {PORT}")

with socketserver.TCPServer(("", PORT), Handler) as httpd:
    print("Server started.")
    httpd.serve_forever()

ディレクトリとファイルを作成します。

sudo mkdir -p /opt/my_app
sudo nano /opt/my_app/app.py

(Pythonコードを貼り付けます)

スクリプトを実行可能にします(python3コマンドではオプションですが、良い習慣です)。

sudo chmod +x /opt/my_app/app.py

セキュリティ上の理由から、サービス専用のユーザーを作成することを検討してください。

sudo useradd --system --no-create-home myappuser

アプリケーションディレクトリに適切な所有権を設定します。

sudo chown -R myappuser:myappuser /opt/my_app

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

次に、Pythonアプリケーション用のsystemdユニットファイルを作成します。名前はmy_app.serviceとします。

sudo nano /etc/systemd/system/my_app.service

次の内容を貼り付けます。

# /etc/systemd/system/my_app.service

[Unit]
Description=My Custom Python HTTP Server
Documentation=https://github.com/example/my_app
After=network.target

[Service]
Type=simple
User=myappuser
Group=myappuser
WorkingDirectory=/opt/my_app
Environment="PORT=8080" "DIRECTORY=/var/www/html"
ExecStart=/usr/bin/python3 /opt/my_app/app.py
Restart=on-failure
RestartSec=10s
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

StandardOutput=journalStandardError=journalを設定して、サービスの出力をsystemdジャーナルに送り、journalctlでログを簡単に表示できるようにしています。

アプリがシークレットを必要とする場合、ユニットファイルに直接配置しないでください。制限付きの権限を持つ環境ファイル、シークレットマネージャー、またはディストリビューション固有の資格情報サポートを使用してください。ユニットファイルは、予想以上に多くの人に読まれることがよくあります。

ステップ3:ユニットファイルの配置

指示に従い、ユニットファイルを/etc/systemd/system/に配置しました。これはカスタムユニットファイルを置くべき場所です。

ステップ4:Systemdデーモンのリロード

ユニットファイルを作成または変更した後、systemdに変更を通知する必要があります。これは、systemdデーモンをリロードすることで行われます。

sudo systemctl daemon-reload

ステップ5:サービスの開始

これでサービスを開始できます。

sudo systemctl start my_app.service

ステップ6:サービスのステータスとログの確認

サービスが正しく実行されていることを確認します。

systemctl status my_app.service

出力例(一部省略):

● my_app.service - My Custom Python HTTP Server
     Loaded: loaded (/etc/systemd/system/my_app.service; disabled; vendor preset: enabled)
     Active: active (running) since Tue 2023-10-26 10:30:00 UTC; 5s ago
       Docs: https://github.com/example/my_app
   Main PID: 12345 (python3)
      Tasks: 1 (limit: 1100)
     Memory: 6.5M
        CPU: 45ms
     CGroup: /system.slice/my_app.service
             └─12345 /usr/bin/python3 /opt/my_app/app.py

Oct 26 10:30:00 yourhostname python3[12345]: Serving directory /var/www/html on port 8080
Oct 26 10:30:00 yourhostname python3[12345]: Server started.

サービスのログを表示するには、journalctlを使用します。

journalctl -u my_app.service -f

このコマンドはmy_app.serviceのログを表示し、-f(フォロー)は新しいログをリアルタイムで表示します。

また、ブラウザまたはcurlhttp://localhost:8080にアクセスしてサーバーをテストすることもできます(/var/www/htmlが存在し、いくつかのファイルが含まれていることを前提とします)。

ステップ7:自動起動のためのサービスの有効化

システム起動時にサービスが自動的に開始されるようにするには、サービスを有効にする必要があります。

sudo systemctl enable my_app.service

このコマンドは、/etc/systemd/system/multi-user.target.wants/my_app.serviceから/etc/systemd/system/my_app.serviceへのシンボリックリンクを作成します。

ステップ8:サービスの停止と無効化

実行中のサービスを停止するには:

sudo systemctl stop my_app.service

サービスが起動時に自動的に開始されないようにするには(手動で開始できるように有効のままにしておく場合):

sudo systemctl disable my_app.service

サービスを完全に削除する場合は、最初にdisableし、次にstopし、最後に/etc/systemd/system/から.serviceファイルを削除し、sudo systemctl daemon-reloadを実行します。

ステップ9:サービスの更新

app.pyスクリプトまたはmy_app.serviceユニットファイルを変更した場合は、systemdを更新してサービスを再起動する必要があります。

  1. /opt/my_app/app.pyまたは/etc/systemd/system/my_app.serviceを編集します。
  2. ユニットファイルを変更した場合は、sudo systemctl daemon-reloadを実行します。
  3. サービスを再起動します:sudo systemctl restart my_app.service

実際のサービスにおけるより安全なパターン

動作するユニットは、必ずしも長期間維持したいユニットであるとは限りません。これらのパターンは、一般的な間違いを防ぎます。

  • フォアグラウンドで実行する。 systemdにメインプロセスを監視させます。ExecStart内でnohupscreentmux、バックグラウンドの&、またはアプリケーションのデーモンモードを避けてください。
  • ExecStartを直接的に保つ。 パイプや変数展開などのシェル機能が必要な場合は、意図的に/bin/sh -c '...'を呼び出します。それ以外の場合は、実行可能ファイルを直接実行します。
  • 専用ユーザーを使用する。 /opt/my_appの読み取りと非特権ポートへのバインドのみが必要なサービスは、rootとして実行すべきではありません。
  • ユニット編集後にリロードする。 ユニットファイルが変更された場合は、sudo systemctl daemon-reloadが必要です。
  • コードのデプロイとユニットの変更を分離する。 Pythonコードのみが変更された場合は、サービスを再起動します。ユニットが変更された場合は、最初にsystemdをリロードします。

新しいユニットのトラブルシューティング

サービスが失敗した場合は、以下から始めます。

systemctl status my_app.service
journalctl -u my_app.service -n 100 --no-pager
systemctl cat my_app.service

一般的な失敗は、通常は明白です。

  • status=203/EXECは、多くの場合、実行可能ファイルのパスが間違っている、ファイルがない、またはファイルが実行可能でないことを意味します。
  • Permission deniedは、通常、サービスユーザーがファイルを読み取れない、ディレクトリに入れない、ログを書き込めない、または要求されたポートにバインドできないことを意味します。
  • address already in useは、別のプロセスがポートを所有していることを意味します。sudo ss -tulpen | grep ':8080'で確認してください。
  • 手動では起動するが、systemdでは失敗するサービスは、多くの場合、環境変数、異なる作業ディレクトリ、またはホームディレクトリ内のファイルに依存しています。

サービスユーザーとしてコマンドをテストできます。

sudo -u myappuser /usr/bin/python3 /opt/my_app/app.py

これはsystemdの環境を完全に再現するわけではありませんが、ユニットファイルの詳細を追う前に、明らかなアプリケーションエラーをキャッチします。

よりプロダクション向けのバリアント

長期実行される内部サービスの場合、通常はいくつかのガードレールを追加します。

[Unit]
Description=My Custom Python HTTP Server
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=myappuser
Group=myappuser
WorkingDirectory=/opt/my_app
EnvironmentFile=-/etc/my_app/my_app.env
ExecStart=/usr/bin/python3 /opt/my_app/app.py
Restart=on-failure
RestartSec=10s
TimeoutStopSec=30s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/opt/my_app /var/log/my_app
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

ProtectSystem=fullは、システムの大部分をサービスから読み取り専用にするため、アプリが実際に書き込む必要があるディレクトリに対してのみReadWritePaths=を追加してください。ハードニングは一度に1つのディレクティブずつテストしてください。セキュリティオプションは便利ですが、設定を読み取ったりデータを書き込んだりできないサービスは、起動時に失敗します。

ベストプラクティスとトラブルシューティング

  • 絶対パスExecStartWorkingDirectory、およびユニットファイル内のその他のファイルパスには、常に絶対パスを使用してください。相対パスは予期しない動作を引き起こす可能性があります。
  • 専用ユーザー:セキュリティを強化し、侵害が発生した場合の潜在的な損害を制限するために、非特権の専用ユーザーアカウント(例:myappuser)でサービスを実行します。
  • 明確なログ記録StandardOutput=journalStandardError=journalを利用して、サービスの出力をsystemdジャーナルに送ります。journalctl -u <サービス名>を使用してログを表示します。
  • 依存関係AfterWantsRequiresを慎重に検討して、サービスが依存関係(ネットワーク、データベースなど)に対して正しい順序で起動するようにします。
  • 変更のテスト:サービスを起動時に開始できるようにする前に、手動で開始および停止して徹底的にテストします。ステータスとログを確認します。
  • リソース制限:サービスに既知の制限や障害モードがある場合は、LimitNOFILELimitNPROCMemoryMaxなどのディレクティブを使用します。
  • 環境変数:ユニットファイルやスクリプトにハードコーディングするのではなく、変更される可能性がある、または環境によって異なる可能性がある設定値には、Environment=またはEnvironmentFile=を使用します。
  • スクリプトのエラー処理:アプリケーションスクリプトがエラーを適切に処理することを確認します。ゼロ以外の終了コードは、Restart=on-failureをトリガーします。

警告/usr/lib/systemd/system/内のユニットファイルを直接変更しないでください。変更はパッケージの更新によって上書きされる可能性があります。カスタムユニットまたはオーバーライドには/etc/systemd/system/を使用してください。

優れたカスタムユニットは、最も良い意味で退屈です。コマンドは明示的で、ユーザーは非特権で、再起動動作は意図的で、ログは見つけやすいです。それが確固たるものになれば、systemdタイマー、ソケットアクティベーション、より深いcgroup制御が自然な次のステップになります。