はじめに (対象読者・この記事でわかること)

この記事は、Linuxサーバーを運用しているエンジニアや、systemdを使ってサービスを管理している開発者、そしてsystemdユニットファイルで「command not found」といったパスの問題に直面している方を対象としています。

この記事を読むことで、systemdのユニットファイル内でコマンドの実行パスを通す基本的な考え方と、具体的な設定方法がわかります。特に、PATH環境変数の設定方法、コマンドを絶対パスで指定する方法、そしてEnvironmentディレクティブやEnvironmentFileを活用した環境変数の管理方法を習得し、サービスが正しく起動しない問題を解決できるようになります。systemdサービスを安定して運用するための一助となれば幸いです。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 * systemdの基本的な概念(サービス、ユニットファイル) * Linuxコマンドラインの基本的な操作 * シェルスクリプトの基本的な知識

systemdユニットファイルにおけるパス問題の背景

systemdはLinuxのサービス管理において非常に強力なツールですが、そのサービス設定、特にパスの扱いに関して戸惑うことがあります。通常のシェル環境(例えばSSHでログインした際のBashやZsh)では、PATH環境変数によって多くのコマンドが実行可能になっています。しかし、systemdによって起動されるサービスは、ログインシェルとは異なる、よりクリーンで最小限の環境で実行されます。

この「クリーンな環境」が、サービスの起動時に「command not found」エラーを引き起こす主な原因です。サービスが実行しようとするプログラムやスクリプトが、システム標準のパス(/usr/bin, /bin, /usr/local/binなど)に存在しない場合、またはアプリケーション固有のパス(例えば/opt/my_app/bin)にインストールされている場合、systemdはそのコマンドを見つけることができません。結果として、サービスは起動に失敗し、journalctlでログを確認すると「No such file or directory」や「command not found」といったエラーメッセージが表示されることになります。

この問題の解決策は、systemdに対して明示的にコマンドの実行パスを教えることです。具体的な方法としては、以下の3つのアプローチがあります。

  1. 絶対パスでコマンドを指定する: 最も単純で確実な方法です。
  2. ユニットファイル内でPATH環境変数を設定する: Environmentディレクティブを使用します。
  3. 環境変数を外部ファイルで管理する: EnvironmentFileディレクティブを使用し、より柔軟にパスを設定します。

次のセクションでは、これらの具体的な設定方法について詳しく解説していきます。

systemdユニットファイルで実行パスを通す具体的な方法

systemdサービスがコマンドを見つけられない問題を解決するために、ここでは3つの主要なアプローチと、そのデバッグ方法を詳細に解説します。

ステップ1: 基本の理解と絶対パスでの指定

systemdサービスは、私たちが普段ログインして使うシェル環境とは異なり、非常に限られた環境で起動します。これはセキュリティやサービス間の依存関係を明確にするために設計されています。そのため、慣れ親しんだPATH環境変数がサービス起動時には設定されていないことが多いのです。

最も確実で単純な解決策は、ExecStartなどのコマンド実行ディレクティブで、常に絶対パスを使用することです。

例:

Ini
# /etc/systemd/system/my_app.service [Unit] Description=My Awesome Application [Service] Type=simple # /usr/local/bin/my_script.sh を絶対パスで指定 ExecStart=/usr/local/bin/my_script.sh --config /etc/my_app/config.yaml Restart=on-failure [Install] WantedBy=multi-user.target

メリット: * シンプルで分かりやすい。 * PATH設定に依存しないため、動作が確実。

デメリット: * プログラムのインストールパスが変わると、ユニットファイルを修正する必要がある。 * 複数のコマンドを絶対パスで指定すると、ユニットファイルが読みにくくなる。 * スクリプト内で他のコマンド(例えばjqcurlなど)を使用する場合、それらも絶対パスで指定しないと結局「command not found」になる可能性がある。

この方法で解決できる場合は、余計な設定を増やさずに済むため、まず最初に検討する価値があります。

ステップ2: EnvironmentディレクティブでPATHを設定する

サービス内で特定のディレクトリにあるコマンドを実行する必要があるが、絶対パスで書きたくない場合や、複数のコマンドが特定のディレクトリにまとまっている場合は、[Service]セクションにEnvironmentディレクティブを追加してPATH環境変数を設定するのが効果的です。

Environmentディレクティブは、指定された環境変数をサービス実行時に設定します。複数の環境変数を設定したい場合は、それぞれの変数を個別のEnvironment行で指定するか、スペース区切りで記述します(ただし、PATHのようにコロン区切りの値を設定する際は注意が必要です)。

例:

Ini
# /etc/systemd/system/my_app.service [Unit] Description=My Awesome Application After=network.target [Service] Type=simple # PATH環境変数を設定。既存のPATHを上書きするので注意。 # 複数のパスはコロンで区切る Environment="PATH=/opt/my_app/bin:/usr/local/bin:/usr/bin:/bin" ExecStart=my_script.sh --config /etc/my_app/config.yaml Restart=on-failure [Install] WantedBy=multi-user.target

解説: 上記の例では、my_script.sh/opt/my_app/binディレクトリに存在する場合、ExecStartでスクリプト名を直接指定しても、systemdはPATHに設定されたパスの中からmy_script.shを見つけて実行します。

重要な注意点: Environment="PATH=..."のように設定すると、サービスが元々持っていたデフォルトのPATH環境変数は上書きされます。システム標準のコマンド(ls, catなど)も利用したい場合は、上記例のようにそれらのコマンドが存在するディレクトリ(/usr/bin, /binなど)もPATHに含める必要があります。

サービスが実際にどのような環境変数で起動されているかを確認するには、以下のコマンドが役立ちます。

Bash
sudo systemctl show my_app.service | grep Environment

メリット: * ユニットファイル内で完結し、パス設定を明示的に行える。 * ExecStartでコマンド名を直接指定できるため、記述が簡潔になる。

デメリット: * PATHを上書きするため、必要なパスをすべて手動で指定する必要がある。 * 複数のサービスで同じパス設定を使いたい場合、各ユニットファイルに同じ記述を繰り返すことになり、変更時の手間が増える。

ステップ3: EnvironmentFileディレクティブで環境変数を外部ファイルから読み込む

より多くの環境変数を設定したい場合や、複数のサービス間で共通の環境変数を使い回したい場合は、EnvironmentFileディレクティブを使用するのが最も柔軟で推奨される方法です。

EnvironmentFileは、指定されたファイルから環境変数を読み込みます。このファイルは、シェルスクリプトのsourceコマンドのように、key=value形式の行を解釈します。

手順:

  1. 環境変数ファイルを作成する /etc/default/my_app (または /etc/sysconfig/my_app など、任意の場所) に環境変数を記述したファイルを作成します。

    ```ini

    /etc/default/my_app

    アプリケーション固有のbinディレクトリをPATHに追加

    既存のPATH ($PATH) を利用しつつ、新しいパスを追加

    PATH="/opt/my_app/bin:/usr/local/bin:$PATH"

    その他の環境変数も設定可能

    APP_CONFIG_DIR="/etc/my_app" DB_HOST="localhost" ```

    注意点: * ファイル内では key=value 形式で記述します。 * 行頭の # はコメントとして扱われます。 * 値にスペースを含む場合は、"value with spaces" のように引用符で囲む必要があります。 * $PATHのように既存の環境変数を参照して、パスを追記することが可能です。これにより、システム標準のPATHを壊さずに、必要なパスを追加できます。

  2. ユニットファイルにEnvironmentFileを追加する 作成した環境変数ファイルをユニットファイル内で参照するように設定します。

    ```ini

    /etc/systemd/system/my_app.service

    [Unit] Description=My Awesome Application After=network.target

    [Service] Type=simple

    環境変数ファイルを読み込む

    EnvironmentFile=/etc/default/my_app ExecStart=my_script.sh --config ${APP_CONFIG_DIR}/config.yaml Restart=on-failure

    [Install] WantedBy=multi-user.target ```

解説: EnvironmentFile=/etc/default/my_appが指定されているため、my_app.serviceは起動時に/etc/default/my_appの内容を読み込み、そこに定義されたPATHAPP_CONFIG_DIRなどの環境変数をサービスプロセスに引き継ぎます。ExecStartでは、$APP_CONFIG_DIRのように定義した変数を参照することも可能です。

メリット: * 環境変数をユニットファイルとは別のファイルで一元管理できる。 * 複数のサービスで共通の環境変数を共有できるため、管理が容易。 * ユニットファイル自体が簡潔に保たれる。 * 既存のPATHに追記する形で設定できるため、システム標準のコマンド利用に影響を与えにくい。

デメリット: * 環境変数ファイル自体の管理が必要になる。 * ファイルパスの指定を間違えると、環境変数が読み込まれない。

ハマった点やエラー解決

PATHや環境変数を設定しても、「command not found」が解消しない場合や、予期せぬエラーが発生する場合があります。

  1. PATH設定が正しく適用されていない:

    • 原因: systemctl daemon-reload の実行忘れ。または、ユニットファイルの編集後にサービスを再起動していない。
    • 確認方法: サービスをsystemctl restart your_serviceで再起動し、再度journalctl -u your_service.serviceでログを確認。
    • 確認方法: sudo systemctl show your_service.service | grep Environment で、実際に設定されている環境変数を確認する。
  2. 指定したパスにコマンドが存在しない:

    • 原因: PATHに指定したディレクトリは正しいが、目的のコマンドがそのディレクトリにない、またはファイル名が間違っている。
    • 確認方法: ls -l /path/to/my/bin/my_script.sh のように、指定したパスにファイルが実際に存在し、実行権限があるか確認する。
  3. EnvironmentFileのファイルフォーマットが間違っている:

    • 原因: key=value形式が守られていない、値にスペースが含まれるのに引用符で囲んでいない、ファイルに不正な文字が含まれるなど。
    • 確認方法: sudo systemd-analyze verify my_app.service を実行して、ユニットファイルの構文チェックを行う。EnvironmentFileのパスが間違っていないか、パーミッションが適切か(systemdが読み込めるか)も確認。
  4. スクリプト内でパスを指定していない他のコマンドがエラーになる:

    • 原因: サービスが起動するスクリプト(例: my_script.sh)の中で、jqcurlなどのコマンドを使っているが、それらのコマンドへのパスがPATH環境変数に含まれていない。
    • 確認方法: スクリプト内で使われているすべての外部コマンドが、設定されたPATH内で見つかるかを確認。必要であればPATHに追記する。

解決策

  • systemctl daemon-reloadとサービス再起動: ユニットファイルを変更したら、必ずsudo systemctl daemon-reloadを実行してsystemdに設定を再読み込みさせ、その後sudo systemctl restart your_serviceでサービスを再起動します。

  • デバッグ用ExecStartの活用: サービスが実際にどのようなPATH環境変数を認識しているかを確認するために、一時的にExecStartを以下のように変更してみるのが非常に有効です。

    ```ini [Service] Type=simple EnvironmentFile=/etc/default/my_app # または Environment="PATH=/..."

    サービス起動時に、まずPATHを表示し、その後本来のコマンドを実行

    ExecStart=/bin/bash -c "echo 'Current PATH: $PATH' && /usr/local/bin/my_script.sh --config /etc/my_app/config.yaml"

    または、より単純にPATHのみ確認

    ExecStart=/bin/bash -c "echo 'Current PATH: $PATH' && sleep 60"

    `` この設定でサービスを起動し、journalctl -u your_service.serviceのログを確認すると、サービスが起動時に認識しているPATHの値が表示されます。このPATHの中に、目的のコマンドがあるディレクトリが含まれているかを確認できます。デバッグが完了したら、元のExecStart`に戻すのを忘れないでください。

  • 詳細なログ出力: サービスを実行するスクリプト自体に、デバッグ用のログ出力を追加するのも有効です。例えば、スクリプトの先頭でecho "PATH in script: $PATH" >> /tmp/my_app_debug.logのように記述して、スクリプトが認識しているPATHをファイルに出力させます。

これらのデバッグ手法と解決策を組み合わせることで、systemdサービスにおけるパス関連の問題を効果的に特定し、解決することができます。

まとめ

本記事では、systemdユニットファイル内でのコマンド実行パスの通し方について解説しました。

  • systemdサービスの特殊性: ログインシェルとは異なる最小限の環境で起動するため、通常のPATHが引き継がれないことが多いです。
  • 絶対パス指定: 最も確実な方法ですが、柔軟性に欠けます。
  • Environmentディレクティブ: ユニットファイル内でPATH環境変数を直接設定する方法です。システム標準のパスも忘れずに含める必要があります。
  • EnvironmentFileディレクティブ: 環境変数を外部ファイルで管理する最も推奨される方法です。$PATHを利用して既存のパスに追記できるため、柔軟性が高く、複数のサービスでの再利用性にも優れています。

この記事を通して、systemdサービスが「command not found」エラーで起動しないといった問題を効果的に解決できるようになったことでしょう。適切なパス設定を行うことで、より堅牢で管理しやすいサービス運用が可能になります。

今後は、systemdのより高度な環境変数管理 (PassEnvironmentなど) や、セキュリティに関する設定 (CapabilityBoundingSet, PrivateTmpなど) についても記事にする予定です。

参考資料