よくあるSystemd設定ミスとその修正方法

よくあるsystemdユニットファイルのミスを修正:パスの誤り、サービスタイプの間違い、環境変数の欠落、パーミッション、依存関係の順序。

よくあるSystemd設定ミスとその修正方法

Systemdの設定ミスは、実際よりも劇的に見えることがよくあります。サービスが起動しなかったり、デプロイがロールバックされたり、ほとんど覚えていないユニット名で起動が止まったりします。そして、実際の原因は、ExecStart=のスラッシュの欠落、間違ったユーザーで実行されているプロセス、またはdaemon-reloadを実行しなかったためにsystemdマネージャーに反映されなかったユニットファイルの変更であることが判明します。

これらの問題を最速で解決する方法は、ユニットファイルを契約書として扱うことです。ユニットファイルは、systemdに対して、どのプロセスを実行するか、どのユーザーで実行するか、最初に何が存在する必要があるか、準備完了をどのように報告するか、クラッシュ後に何をすべきかを指示します。これらの詳細のいずれかが間違っている場合、systemdは通常、指示されたとおりに動作しています。作業は、アプリケーションが必要としているものと、ユニットファイルが実際に言っていることとの間の不一致を見つけることです。

1. ユニットファイルの構文エラーとパスエラー

サービス障害の最も頻繁な原因の1つは、ユニットファイル内の単純なタイプミスや誤って定義されたパスです。

Execコマンドのパスが絶対パスでない、または正しくない

Systemdは、テストに使用したシェルセッションと同じ環境からサービスを実行しません。制御された環境でプロセスを開始するため、エイリアス、シェル関数、virtualenvのアクティベーション、カスタムPATHに関する前提は、しばしば失敗します。ExecStart=では実行ファイルに絶対パスを使用し、サービスが必要とするすべてのディレクトリやファイルを明示的に指定してください。

エラー:

場所を指定せずにコマンド名を使用する。

[Service]
ExecStart=my-app-server --config /etc/config.yaml

my-app-server/usr/local/binにある場合、systemdはそれを見つけられない可能性があります。

修正:

実行ファイルへの完全な絶対パスを常に使用してください。

[Service]
ExecStart=/usr/local/bin/my-app-server --config /etc/config.yaml

ExecStart=を設定する前に、command -v my-app-serverまたはwhich my-app-serverでパスを確認してください。アプリケーションが言語固有の場所(例:/opt/myapp/venv/bin/gunicornの下のPython仮想環境)にある場合は、アクティベーションスクリプトに依存する代わりに、そのバイナリを直接指定してください。

タイプミスと大文字小文字の区別

Systemdの設定ディレクティブは大文字小文字を区別し、正しいセクション([Unit][Service][Install])に配置する必要があります。スペルミスや誤った大文字小文字は、サービスの読み込みに失敗したり、予期しない動作を引き起こしたりします。

エラー例:

[Service]
ExecStart=/usr/bin/python3 app.py
RestartAlways=true  ; Should be Restart=always

修正:

デーモンをリロードする前に、systemd-analyze verify <unit_file>を使用してください。すべてのランタイムエラーをキャッチできるわけではありませんが、多くのスペルミスのあるディレクティブ、無効なセクション配置、パースエラーを、アプリケーションログを追跡する時間を無駄にする前にキャッチできます。

$ systemd-analyze verify /etc/systemd/system/my-service.service

2. サービス依存関係と順序の誤管理

依存関係はサービスが必要とするリソースを定義し、順序はそれらのリソースが利用可能になるタイミングを定義します。

RequiresWantsの混同

これらのディレクティブは依存関係を定義するために使用されますが、障害の処理方法が異なります。

  • Wants=:弱い依存関係。対象のユニットが失敗したり起動しなかった場合でも、現在のユニットは起動を試みます。重要でない依存関係に使用します。
  • Requires=:強い依存関係。必要なユニットを起動できない場合、現在のユニットも失敗します。必要なユニットが明示的に停止された場合、依存するユニットも停止されます。

適切な順序付けなしでのRequiresへの依存

例えばRequires=network.targetのような依存関係を定義すると、その依存関係がトランザクションに取り込まれます。それ自体では起動順序は作成されず、network.targetは「ネットワークがアウトバウンド接続に使用可能である」ことを意味しません。サービスが設定されたネットワークを必要とする場合は、network-online.targetを使用し、その動作が必要な場合にディストリビューションのwait-onlineサービスが有効になっていることを確認してください。

エラー:

Webサーバーは起動するが、ネットワーキングスタックがまだ初期化中のため、データベース接続が失敗する。

修正: After=Before=の使用

順序を強制するには、After=(またはBefore=)を使用する必要があります。一般的な要件は、続行する前にネットワークが完全に起動し設定されていることを確認することです。

[Unit]
Description=My Web Application Service
Wants=network-online.target
After=network-online.target

[Service]
...

ほとんどのアプリケーションサービスでは、依存関係の意図と順序の意図をペアにします。Wants=postgresql.serviceは「PostgreSQLも起動してください」を意味します。After=postgresql.serviceは「PostgreSQLの起動ジョブが完了した後に私を起動してください」を意味します。これらは異なる問題を解決します。

誤ったサービスタイプの管理

Systemdサービスには、Type=ディレクティブで管理されるいくつかの実行タイプがあります。これを誤って設定することは、サービスが一瞬起動してすぐに失敗する一般的な原因です。

エラー: Type=forkingの誤用

アプリケーションがフォアグラウンドで実行され、単一のメインプロセスを維持するように設計されている場合、Type=forkingを設定すると、systemdは古いスタイルのデーモン動作を期待するようになります。これにより、混乱を招く結果になる可能性があります。systemdは親プロセスの終了を待機したり、実際のメインプロセスを識別できなかったり、アプリケーションが実際には準備できていないのにサービスをアクティブとしてマークしたりする可能性があります。

修正:

  1. 最新のアプリケーションの場合: Type=simpleを使用します。これがデフォルトであり、ExecStartプロセスがメインプロセスであることを期待します。
  2. デーモン化(フォーク)するレガシーアプリケーションの場合: Type=forkingを設定し、重要なのは、フォークを生き残った子プロセスをsystemdが追跡できるようにPIDFile=ディレクティブを定義することです。
[Service]
Type=forking
PIDFile=/var/run/legacy-app.pid
ExecStart=/usr/sbin/legacy-app

もう1つの一般的な準備完了の落とし穴があります。使用可能になるまでに時間がかかるアプリケーションにType=simpleを使用することです。Type=simpleの場合、systemdはプロセスが生成されるとすぐにサービスが開始されたと見なします。別のサービスがすぐに起動してそれに接続すると、断続的な障害が発生する可能性があります。準備ができたときにsystemdに通知できるアプリケーションの場合は、Type=notifyの方がクリーンです。通知できないアプリケーションの場合は、プロセスが存在するという理由だけでユニットが完全に準備できているふりをしないでください。依存するアプリケーションで実際のヘルスチェック、ソケットアクティベーション、または前提条件が十分に単純な場合はExecStartPre=チェックを使用してください。

oneshotにも注意してください。Type=oneshotサービスは、ディレクトリの作成、ファイアウォールルールのロード、マイグレーションの実行など、タスクを実行して終了するコマンド用です。長時間実行されるデーモンに使用すると、systemdは期待どおりに監視しません。コマンドが正常に終了し、依存関係のためにユニットを「アクティブ」に保ちたい場合は、RemainAfterExit=yesを追加してください。そうしないと、依存ユニットが意図した状態を認識できない可能性があります。

3. 環境とユーザーコンテキストの問題

サービス障害は、サービスがアプリケーションの期待とは異なるコンテキストで実行されていることに起因することが多く、通常はパーミッションまたは環境変数に関連しています。

パーミッション拒否またはファイルの欠落

アプリケーションを手動でテストする場合、通常は適切なパーミッションを持つユーザーアカウントで実行されます。systemdによって実行される場合、多くの場合、rootユーザーまたはユニットファイルで指定されたユーザーがデフォルトになります。

エラー:

典型的な症状は単純です。Permission deniedNo such file or directoryFailed to open log file、またはソケットを作成できない、PIDファイルを書き込めない、設定ファイルを読み取れないというアプリケーション固有のエラーです。rootとして手動でコマンドを実行するとユニットが動作しても、User=appの下では失敗する場合があります。

修正:

  1. 非rootユーザーを定義する: サービスには常に専用の低権限ユーザーとグループを指定してください。

    [Service]
    User=www-data
    Group=www-data
    ...
    
  2. 所有権を確認する: サービスの作業ディレクトリ、ログファイル、設定ファイルが指定されたUser=Group=によって所有されていることを確認してください。

    sudo chown -R www-data:www-data /var/www/my-app
    
  3. サービスが触れるすべてのパスを確認する: アプリケーションディレクトリだけで止めないでください。WorkingDirectory=、ログディレクトリ、アップロードディレクトリ、キャッシュディレクトリ、TLSキーファイル、Unixソケット、環境ファイルで参照されているすべてのパスを確認してください。

    sudo -u www-data test -r /etc/my-app/config.yml
    sudo -u www-data test -w /var/lib/my-app
    sudo -u www-data /usr/local/bin/my-app --check-config
    

サービスがポート80または443にバインドする必要がある場合、自動的にrootとして実行しないでください。多くのシステムでは、その前にリバースプロキシを配置したり、ソケットアクティベーションを使用したり、バイナリに特定のケイパビリティを付与したりできます。適切な選択はサービスによって異なりますが、広範なrootプロセスをデフォルトの回答にすべきではありません。

もう1つ、人々を悩ませるパーミッションの詳細があります。親ディレクトリには実行パーミッションが必要です。ファイルは読み取り可能に見えても、/opt/opt/myapp、または別の親ディレクトリがサービスユーザーのトラバーサルをブロックしているため、サービスはそのファイルに到達できません。namei -l /opt/myapp/config.ymlは、最終ファイルだけでなく、各パスコンポーネントのパーミッションを表示するため便利です。

環境変数の欠落

Systemdサービスは最小限の環境で実行されます。重要な環境変数(APIキー、データベース接続文字列、カスタムライブラリパスなど)は、明示的に渡す必要があります。

修正: Environment=またはEnvironmentFile=の使用

単純な変数の場合は、Environment=を使用します。

[Service]
Environment="APP_PORT=8080"
Environment="API_KEY=ABCDEFG"

複雑または多数の変数の場合は、標準の.envファイルを指すEnvironmentFile=を使用します。

[Service]
EnvironmentFile=/etc/default/my-app.conf

機密情報は、誰でも読み取り可能なユニットファイルに保存しないでください。/etc/systemd/systemの下のユニットファイルは、通常、ローカルユーザーが読み取ることができます。APIキーを直接Environment=に配置した場合、ユニットファイルを読み取ったりプロセスメタデータを検査したりできる人に公開されると想定してください。ルートが所有する制限されたパーミッションの環境ファイル、シークレットマネージャー、またはそれらをサポートするディストリビューションのsystemdクレデンシャルを優先してください。

また、EnvironmentFile=はシェルスクリプトではないことに注意してください。export APP_PORT=8080のような行や、TOKEN=$(cat /run/token)のようなコマンド置換は、Bashのように解釈されません。プレーンな代入を使用してください。

APP_PORT=8080
APP_ENV=production

アプリケーションがログインシェルのセットアップを必要とする場合、それは通常、問題の兆候です。.bashrcに依存する代わりに、実際の環境をユニットファイル、環境ファイル、またはアプリケーション自身の設定に配置してください。

言語ランタイムの場合、実際にテストしたランタイムをsystemdに指定してください。Pythonサービスは通常、仮想環境のバイナリを直接呼び出す必要があります(例:/opt/myapp/venv/bin/pythonまたは/opt/myapp/venv/bin/gunicorn)。バージョンマネージャーを介してインストールされたNodeサービスは、ターミナルでは動作しても、nvmasdfがインタラクティブシェルのみを変更したため、systemdの下では失敗する可能性があります。本番ユニットでは、明示的なパスがシェル起動の魔法よりも優れています。

4. 重要なデバッグワークフロー

最も一般的な設定ミスは、ユニットファイルの編集とサービスの再起動の試行との間の重要なステップを忘れることです。

デーモンのリロードを忘れる

Systemdはユニットファイルの変更を自動的に監視しません。/etc/systemd/system/内のファイルを変更した後は、systemdマネージャーに設定キャッシュをリロードするように指示する必要があります。

エラー:

ファイルを編集し、systemctl restart my-serviceを実行しても、古い設定が使用されます。

修正: daemon-reloadを実行する

ユニットファイルの変更を保存した直後に、常にこのコマンドを実行してください。

sudo systemctl daemon-reload
sudo systemctl restart my-service

systemctl edit my-serviceでドロップインオーバーライドを編集した場合も、同じルールが適用されます。生成されたオーバーライドは/etc/systemd/system/my-service.service.d/の下に保存され、systemdは新しい設定が有効になる前にユニットキャッシュをリロードする必要があります。

再起動が奇妙に動作する場合は、systemdが認識している正確なマージ済みユニットを検査してください。

systemctl cat my-service.service
systemctl show my-service.service -p FragmentPath -p DropInPaths -p User -p ExecStart

これにより、一般的な間違いをキャッチできます。/usr/lib/systemd/systemのベンダーユニットを編集している間に、/etc/systemd/systemのオーバーライドがまだ設定を変更している場合や、systemdがロードしたものとは異なるユニットのコピーを編集している場合です。

ロギングツールの効果的な使用

サービスが失敗した場合は、公式ツールに依存して正確な診断を行ってください。

  1. サービスステータスを確認する: これにより、即時の状態、終了コード、および最後の数行のログが得られます。

    systemctl status my-service.service
    
  2. ジャーナルを検査する: ジャーナルには、サービスの包括的な出力(stdout/stderr)が保持されています。*"Permission denied""No such file or directory"*などの手がかりを探してください。

    # ユニット固有の最近のログを表示
    journalctl -u my-service.service --since '1 hour ago' 
    
    # ログを表示し、リアルタイムで出力を追跡
    journalctl -f -u my-service.service
    

実践的なトラブルシューティングパス

壊れたユニットをレビューするとき、私は通常、次の順序で1回パスします。

systemctl status my-service.service
journalctl -u my-service.service --since "15 minutes ago"
systemctl cat my-service.service
systemd-analyze verify /etc/systemd/system/my-service.service

次に、単純な質問をします。ExecStart=は実際の実行可能ファイルを指していますか?設定されたUser=はそれを実行できますか?WorkingDirectory=は存在しますか?環境変数はシェルに依存せずに存在していますか?Type=はプロセスの動作について正直ですか?別のユニットを起動し、このユニットの前に順序付ける必要がある場合、Wants=After=の両方が存在しますか?

編集後は、リロードして1つのことをテストします。

sudo systemctl daemon-reload
sudo systemctl restart my-service.service
systemctl status my-service.service --no-pager

それでもサービスが失敗する場合は、ユニットファイルを盲目的に変更し続けたい衝動を抑えてください。ジャーナルは通常、次の問題がsystemd設定、アプリケーション設定、パーミッション、またはそれ自体で失敗している依存関係のいずれであるかを教えてくれます。