systemdの依存関係と条件を理解する
Requires、Wants、After、Before、ExecStartPre、Conditionの違いを整理します。起動順序と依存関係の落とし穴、起動しないunitのデバッグ方法を解説します。
Requires は After を意味しない
systemd の依存関係と条件には、ほとんどの unit ファイルで 1 つの仕組みのように扱われている 4 つの別個の機構があります。Requires= と Wants= は、どの unit を同時に起動対象へ組み込むかを決めます。After= と Before= は、unit の起動順序を決めます。ExecStartPre= はチェックを実行し、失敗した場合は unit を失敗状態にできます。Condition と Assert の各ファミリーは、unit を実行するかどうかを決めます。各機構は独立しているため、ある unit が別の unit を必要としていても、同じ時刻に起動できます。
この点を見落とすことが、「手動で起動すると動くのに、ブート時には失敗する」という報告のほぼすべての原因です。
[Unit]
Description=Inventory API
Requires=postgresql.service
[Service]
ExecStartPre=/usr/bin/pg_isready -h 127.0.0.1 -t 5
ExecStart=/usr/local/bin/inventory-apiRequires=postgresql.service は PostgreSQL を同じ起動トランザクションに組み込みます。PostgreSQL の起動を待つわけではありません。systemd は両方のジョブを並列に開始するため、PostgreSQL がまだデータディレクトリを開いている間に pg_isready が実行されます。まだ待ち受けているプロセスがないので終了コード 2 で終了し、ExecStart に到達する前に unit が失敗します。1 時間後に sudo systemctl start inventory-api を実行すると、その時点では PostgreSQL がすでに起動しているため成功します。unit ファイルの内容は変わっていないため、このファイルには問題がないように見えます。
修正は 1 行です。
[Unit]
Requires=postgresql.service
After=postgresql.service同じ箇所には、さらに重要な点があります。Requires= の依存先が失敗した場合でも、After= をその依存先に設定していなければ、unit の起動は停止しません。順序指定がなければ、依存先が失敗する時点で systemd はすでに unit を起動しているため、キャンセルする対象が残っていません。Requires= だけでは、期待されている保護は得られません。特別な理由がない限り、すべての Requires= と Wants= の隣に After= を記述してください。
Requires、Wants、Requisite、BindsTo の動作
これらはすべて依存関係の設定です。いずれも起動順序は指定しません。
Wants=: 相手の unit を取り込みます。相手が失敗しても、存在しなくても、この unit は起動します。systemctl enableによって、.wants/ディレクトリ内にシンボリックリンクとして作成されます。Requires=: 相手の unit を取り込みます。相手が失敗し、さらにAfter=によって順序も指定している場合、この unit は起動しません。相手の unit を後から明示的に停止すると、この unit も同時に停止します。Requisite=: 相手の unit を取り込みません。相手がすでに active でなければ、この unit は直ちに失敗します。BindsTo=:Requires=と同様ですが、ハードウェアが消失した場合を含め、相手の unit が何らかの理由で停止するたびに、この unit も停止します。PartOf=: 相手の unit に対する停止と再起動が、この unit にも伝播します。起動は伝播しません。Conflicts=: この unit を起動すると、相手の unit が停止します。
ある daemon が別の daemon と通信する場合、通常は Wants= と After= の組み合わせが適しています。Requires= は稼働期間を連動させます。保守のためにデータベースを停止すると、アプリケーションも同時に停止し、データベースが復旧しても自動的には戻りません。Wants= と After= を組み合わせると、この連動なしで起動順序だけを指定できます。依存先が後から消失する場合は、再起動ポリシーで対応します。
自分で記述していない依存関係も継承されます。デフォルトの DefaultDependencies=yes を使用すると、通常の service には Requires=sysinit.target、After=sysinit.target basic.target、Conflicts=shutdown.target が自動的に付与されます。そのため、[Unit] セクションがほぼ空の service でも、ブートの後半で起動し、シャットダウン時には正常に停止します。
After と Before はトランザクションの順序だけを決める
After= と Before= は純粋な順序指定です。要件は一切追加しません。Redis を他の何も pull しない unit に After=redis.service を指定しても、何も起こりません。redis.service がトランザクションに含まれていなければ、待機対象がないため、unit は直ちに起動します。
これは繰り返し説明する価値があります。後述する network-online.target の誤りとまったく同じ構造だからです。順序指定が待機するのは、同じトランザクションですでに起動対象になっている unit だけです。
この2つは対称です。a.service に After=b.service と記述することは、b.service に Before=a.service と記述することと同じ意味です。どちらか一方を使用し、自分が管理する unit に記述してください。停止時には順序が自動的に逆になるため、After=b.service は自分の unit が b.service より前に停止することも意味します。
After= は「起動済み」を待機し、Type= がその意味を定義する
After= は、もう一方の unit の起動処理が完了するまで待機します。「起動処理が完了した」と見なす条件は、その unit の Type= だけで決まります。
Type=simple: systemd がプロセスを fork した直後です。プログラムはまだ設定を解析していない可能性があり、ソケットを開いているとは限りません。Type=exec:execve()が成功した直後です。Type=simpleよりは強い条件ですが、準備完了を示すものではありません。Type=forking: 元の親プロセスが終了した時点です。Type=oneshot: プロセスが終了した時点です。この場合、「起動済み」は実際に処理が完了したことを意味します。Type=notify: サービスが通知用ソケットでREADY=1を送信した時点です。実際の準備完了を報告できるのはこのタイプだけです。
したがって、Type=simple デーモンに対する After= は、弱い保証にすぎません。これが、最初の例における競合状態のもう一つの原因です。依存先の unit が Type=simple として提供されている場合、その unit の後に起動するよう指定しても、接続を受け付けられる状態とは限りません。対処方法は明確に2つあります。依存先の socket unit の後に起動するよう指定し、デーモンの起動中もカーネルに着信接続をキューへ格納させます。あるいは、自分のサービスで再試行し、restart policy に処理を引き継がせます。unit が使用するタイプは systemctl cat で確認できます。起動順序を前提にする前に、Type= 設定と各値が systemd に伝える内容を確認してください。
ExecStartPre はユニットを失敗させるゲートです
ExecStartPre= は ExecStart= より前に実行されます。終了ステータスが 0 以外の場合、アクティベーションは中止され、ユニットは failed になります。ExecStart= は実行されません。実際のプログラムからメッセージが出ないまま失敗するユニットの多くは、この仕組みによるものです。プログラム自体が起動していないためです。
注意すべき点は次のとおりです。
- シェルではありません。パイプ、リダイレクト、glob、
&&は使用できません。最初のトークンは絶対パスでなければなりません。シェル構文が必要な場合は、行を/bin/sh -c '...'でラップします。 -プレフィックスを付けると、終了ステータスが 0 以外でも致命的なエラーになりません:ExecStartPre=-/usr/bin/optional-check。- 各
ExecStartPre=は、次のものが実行される前に終了しなければなりません。長時間実行するプロセスは起動できません。 - すべての
ExecStartPre=行は、ExecStart=とTimeoutStartSec=を共有します。データベースを待機する pre-check がループすると、起動タイムアウトを消費します。その後、ジャーナルにstart operation timed out. Terminating.が出力されると、ユニットはResult: timeoutで失敗します。
失敗行に示されるのは、メインプロセスではなく制御プロセスです。
inventory-api.service: Control process exited, code=exited, status=2/INVALIDARGUMENT
inventory-api.service: Failed with result 'exit-code'.このシンボリック名を注意して確認してください。systemd は小さい終了コードを固定テーブルで変換するため、2 はプログラムがそのコードにどのような意味を持たせたかにかかわらず、常に INVALIDARGUMENT と表示されます。実際の情報を伝えるのは status=203/EXEC です。パスが誤っているか、ファイルに実行権限がないため、systemd はバイナリをまったく実行できませんでした。
ディレクトリの作成に ExecStartPre= を使用しないでください。RuntimeDirectory=、StateDirectory=、LogsDirectory=、CacheDirectory= は適切な所有者とモードでディレクトリを作成し、RuntimeDirectory= はサービス停止時に削除されます。これらは DynamicUser= の下でも正しく動作しますが、手書きの mkdir は正しく動作しません。
Condition は静かに失敗します。Assert は明示的に失敗します。
Condition ファミリーと Assert ファミリーは、同じテストを実行します。違いは、テストに失敗したときの動作だけです。
Condition...= に失敗すると、その unit はスキップされます。start job は successful として報告されます。unit は inactive (dead) のままになり、失敗として記録されず、アラートも発生しません。journal には次の 1 行だけが記録されます。
Condition check resulted in Inventory API being skipped.systemd 250 以降では、systemctl status が理由を直接表示します。
Active: inactive (dead)
Condition: start condition unmet at Thu 2026-08-20 09:14:02 UTC; 2min agoその下のインデントされた行には、失敗したディレクティブが具体的に示されます。たとえば ConditionPathExists=/etc/inventory/api.conf was not met です。
Assert...= に失敗すると、unit は失敗します。journal には Assertion failed for Inventory API. と記録され、unit は failed (Result: assert) になります。監視システムが検知できる状態です。
どちらを使うかは、テストに失敗した場合の意味で判断します。Condition は「この unit はこのマシンには適用されない」という意味です。Assert は「これは必須条件であり、満たされなければ誰かに知らせる」という意味です。ほとんどの unit では Condition が適しています。何もせず静かに終わるより、unit を失敗させたほうがよい場合だけ Assert を使います。
Condition ファミリーには、2 つの注意点があります。
1 つ目は、condition の失敗によって、その unit に依存する unit まで失敗するわけではないことです。a.service に Requires=b.service があり、b.service が condition によってスキップされた場合でも、b.service の start job は完了として扱われます。そのため a.service は、b が実行されていない状態で通常どおり起動します。condition が保護するのは、それが記述された unit だけです。
2 つ目は、condition が unit の起動時、job が実行されるたびに評価されることです。VPS 上の systemd timer から起動される unit は、100 回連続でスキップされても、一度も failed にならないことがあります。これは 実行されるものの何もしない cron job と同じ種類の静かな no-op です。確認方法も同じで、終了状態を信頼するのではなく、その unit の journal を読みます。
サーバーで知っておくべき condition は次のとおりです。
ConditionPathExists=/etc/inventory/api.confと、その否定形であるConditionPathExists=!/etc/inventory/api.conf。ConditionFileNotEmpty=とConditionDirectoryNotEmpty=。パッケージが作成したものの空のまま残した設定ファイルやデータディレクトリに使います。ConditionVirtualization=。実際の kernel インターフェイスを必要とする unit にConditionVirtualization=!containerを指定できます。使用しているマシンの値はsystemd-detect-virtで確認します。ConditionHost=。hostname または machine ID に一致します。これにより、共有する 1 つの unit file の動作を 2 台のサーバーで変えられます。ConditionKernelCommandLine=とConditionKernelVersion=。boot parameter に依存する unit や、最低 kernel バージョンを必要とする unit に使います。
空の代入を指定するとリストが消去されます。これにより、パッケージが提供した condition を drop-in から削除できます。
[Unit]
ConditionPathExists=
ConditionPathExists=/srv/inventory/api.confnetwork.target がネットワークの起動完了を意味しない理由
network.target は同期ポイントであり、状態を表すものではありません。ブート時にこれより後へ順序付けると、ネットワーク管理ソフトウェアが起動済みであることだけを意味します。インターフェースにアドレスが設定されていることや、インターネットへの経路が存在することまでは意味しません。この target は主に逆方向のために存在します。After=network.target として順序付けられた unit は、シャットダウン時にネットワークが停止される前に停止します。
network-online.target が待機を行います。実際には、使用しているネットワーク管理ソフトウェアに対応する wait-online service が処理します。
- systemd-networkd がリンクを管理している場合の
systemd-networkd-wait-online.service。netplan で構成した Ubuntu server では通常この構成です。 - NetworkManager を使用する場合の
NetworkManager-wait-online.service。
古い ifupdown 構成では、networking.service によって同じ動作を実現できます。どれを使用する場合でも、target を正しく使うには 1 行ではなく 2 行が必要です。
[Unit]
Wants=network-online.target
After=network-online.targetnetwork-online.target はデフォルトのブートトランザクションには含まれず、自動的に組み込まれることもありません。After= だけを記述すると、キューに入っていない unit に対して順序を指定することになるため、その指定は何も行いません。これは前述した no-op の一例であり、特にコストの大きい形です。Wants= の行によって target がトランザクションに追加され、After= の行が待機できる対象になります。
次に理解すべき点は、"online" の定義を systemd ではなく wait-online の実装が決めることです。systemd-networkd-wait-online は、管理対象のリンクが設定された状態になると戻ります。DNS が名前解決できるかどうかや、リモートホストに到達できるかどうかは確認しません。
この定義が、VPS でよくある障害を引き起こします。netplan で 2 番目のインターフェースをプライベートネットワーク用として宣言しているものの、アドレスを設定していない場合、wait service はタイムアウトするまで待機し続けます。
systemd-networkd-wait-online[612]: Timeout occurred while waiting for network connectivity.
systemd-networkd-wait-online.service: Failed with result 'exit-code'.デフォルトのタイムアウトが 120 秒のため、ブートに 2 分余計にかかります。対処方法は 2 つあります。netplan ファイルで未使用のインターフェースを optional: true と指定し、networkd がそのインターフェースを待たないようにします。または、wait service に drop-in を追加し、--interface= で必要なリンクを指定するか、--any を渡していずれか 1 つのリンクが起動した時点で戻るようにします。
さらによい方法は、target 自体を必要としない構成にすることです。多くの service が network-online.target の後に順序付けられているのは、特定のアドレスに bind するためであり、ブート時に次のような行で失敗するからにすぎません。
nginx: [emerg] bind() to 203.0.113.10:443 failed (99: Cannot assign requested address)そのアドレスがまだ起動していないため、kernel は bind を拒否します。net.ipv4.ip_nonlocal_bind=1 を設定すると、ホストがまだ保持していないアドレスにもプロセスが bind でき、残りは restart policy で対応できます。ネットワークの準備完了を待つためにブート全体を遅延させるのは、通常は 1 つの socket だけが問題である場合には過剰な対策です。
稼働中のシステムで実際の systemd 依存関係を確認する方法
unit ファイルだけを見て判断してはいけません。drop-in、.wants/ シンボリックリンク、暗黙のデフォルト依存関係によって、ファイルに表示されないエッジも追加されます。
systemctl cat inventory-api.serviceこれは unit ファイルとすべての drop-in を、適用順に表示します。各ブロックの前には、そのソースパスも表示されます。最初に実行してください。/etc/systemd/system/inventory-api.service.d/ にある 5 行の override はパッケージが提供するファイルより優先され、それ以外では確認できません。
systemctl show inventory-api.service -p Requires -p Wants -p After -p Before -p ConditionResult -p AssertResultこれは drop-in の適用後、および systemd が暗黙の依存関係を追加した後の、解決済みの値を表示します。ConditionResult=no は、「unit は成功を報告したのに何もしなかった」という問いへの直接的な答えです。
systemctl list-dependencies inventory-api.service
systemctl list-dependencies --reverse inventory-api.service
systemctl list-dependencies --after inventory-api.service
systemctl list-dependencies --before inventory-api.service簡易形式では Requires= と Wants= を下方向にたどります。--reverse では、どの unit が対象 unit を pull in しているかを確認できます。これにより、ブート時に対象 unit を起動する target を特定できます。--after と --before には順序関係が表示されます。実際に何かが待機したかを確認するときは、この 2 つを読みます。
journalctl -b -u inventory-api.service --no-pager
journalctl -b -o short-precise -u inventory-api.service -u postgresql.service2 つ目のコマンドは、2 つの unit のイベントをミリ秒単位のタイムスタンプ付きで交互に表示します。これにより、推測ではなく順序競合を証明できます。pg_isready の失敗は PostgreSQL が database system is ready to accept connections をログに記録する前に発生しており、その間隔も出力から直接確認できます。
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemd-analyze critical-chain inventory-api.serviceverify は systemd が実際に読み込む方法で unit を読み込み、未知のディレクティブ、存在しない unit への依存関係、順序サイクル、解析できない構文を報告します。システム上の変更は行いません。critical-chain は unit の起動を遅延させた順序チェーンを、各ステップが active になった時刻とともに表示します。ただし、現在のブート中に起動した unit に対してのみ機能します。
unit ファイルを編集した後は、sudo systemctl daemon-reload を実行してください。パッケージが提供する unit を変更する場合は sudo systemctl edit inventory-api.service を使用します。これにより drop-in が作成されます。/usr/lib/systemd/system/ 配下の vendor ファイルを直接編集すると、その変更は次回のパッケージアップグレードでファイルが置き換えられるまでしか有効ではありません。同じ drop-in の仕組みを使えば、パッケージが所有するファイルに触れずに サービスにメモリと CPU の制限を設定することもできます。
順序関係の循環と、journal に残る記録
両方向に順序関係を追加すると、systemd はジョブの一方を削除してループを解消します。
systemd[1]: Found ordering cycle on inventory-api.service/start
systemd[1]: Job postgresql.service/start deleted to break ordering cycle starting with inventory-api.service/startsystemd は削除するジョブを選択しますが、必ずしも意図したジョブが選ばれるとは限りません。その結果、再起動によってサービスが存在したり存在しなかったりするように見え、外部からの切り分けが非常に困難になります。循環の多くは、DefaultDependencies=no を設定した unit が、それでも basic.target に対して順序付けを行うことによって発生します。また、すでに After= が自 unit を参照するよう設定されている unit に Before= を追加した場合にも発生します。systemd-analyze verify を使えば、再起動せずに循環を検出できます。
固定した unit
[Unit]
Description=Inventory API
Wants=postgresql.service network-online.target
After=postgresql.service network-online.target
ConditionPathExists=/etc/inventory/api.conf
[Service]
Type=notify
StateDirectory=inventory
ExecStart=/usr/local/bin/inventory-api
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=multi-user.target各行は1つの役割だけを担います。Wants= は両方の依存関係をトランザクションに含めますが、この unit のライフタイムを依存先に結び付けません。After= が待機処理を行います。依存関係と起動順序は別々の設定であるため、両方の名前を繰り返して指定する必要があります。ConditionPathExists= により、パッケージはあるものの設定がないマシンでは、この unit を警告せず静かにスキップします。設定駆動のサービスでは、これが適切な動作です。Type=notify により、この unit の後に起動順序が設定された対象は、fork の完了ではなく実際の準備完了を待ちます。Restart=on-failure は、起動からかなり後にデータベースが停止した場合にも対応します。起動順序の指定が適用されるのは、最初の起動時だけだからです。再試行をどの程度積極的に行うかは、Restart= と RestartSec= の設定で制御します。
信頼する前に確認します。
sudo systemctl daemon-reload
systemd-analyze verify /etc/systemd/system/inventory-api.service
systemctl list-dependencies --after inventory-api.service
sudo systemctl start inventory-api.service
systemctl show inventory-api.service -p ConditionResult -p ActiveState -p Result正常な unit では、ConditionResult=yes を ActiveState=active で読み込み、Result=success により直近の実行で失敗していないことを確認できます。ConditionResult=no と ActiveState=inactive が併記されている場合、その unit はスキップされています。条件名を示す journal の行から、どのテストに失敗したかを確認できます。
FAQ
Requires= は他の unit の起動を待機しますか?
いいえ。Requires= と After= は別の設定です。Requires= は他の unit を同じトランザクションに追加し、systemd は両方のジョブを並列に起動します。待機するには、同じ unit を指定する After= を追加します。これを追加する理由はもう 1 つあります。Requires= の依存関係が失敗しても、After= も設定されている場合にだけ unit の起動が阻止されます。順序指定がなければ、他の unit が失敗する前に自分の unit の起動が完了しているためです。
network.target と network-online.target のどちらの後に順序指定すべきですか?
ブート時の network.target は、ネットワーク管理ソフトウェアが起動したことだけを意味します。アドレスやルートが利用可能であることは保証しません。サービスの起動時に使用可能なアドレスが必要な場合は network-online.target を使用し、Wants=network-online.target と After=network-online.target の両方を記述します。この target はデフォルトのブートトランザクションに含まれないため、After= だけでは、キューに追加されていない unit を待機することになるからです。サービスが特定の IP への bind に失敗する場合だけは、Restart=on-failure と組み合わせた net.ipv4.ip_nonlocal_bind=1 の方が、ブートを遅延させずに済みます。
unit は成功を報告するのに、なぜ実行されないのですか?
Condition...= のテストに失敗すると、その unit はスキップされ、起動ジョブは成功として報告されます。そのため、失敗として記録されるものはありません。systemctl show <unit> -p ConditionResult を実行し、ConditionResult=no で確認します。次に journalctl -b -u <unit> で Condition check resulted in <description> being skipped という行を確認します。systemd 250 以降では、systemctl status <unit> により、満たされなかったディレクティブも正確に確認できます。
Condition と Assert の違いは何ですか?
両者は同じテストを実行します。Condition に失敗すると unit は何も通知せずにスキップされ、ジョブは成功します。Assert に失敗すると unit は失敗し、Assertion failed for <description>. がログに記録され、failed (Result: assert) の状態になります。「この unit はこのマシンには適用されない」という場合は Condition を使用します。実際のケースのほとんどがこれに該当します。失敗した unit を監視する担当者に、前提条件の欠落を明示する必要がある場合だけ Assert を使用します。
ExecStartPre が status=203/EXEC で失敗するのはなぜですか?
203/EXEC は、systemd がコマンドをまったく実行できなかったことを意味します。よくある原因は、パスが絶対パスではない、そのマシンにバイナリが存在しない、ファイルに実行権限がない、またはスクリプトの #! 行が存在しないインタープリターを指定していることです。systemd のその他の小さなコードは固定された一覧に基づくため、status=2/INVALIDARGUMENT はコマンドが 2 で終了したことだけを意味し、引数については何も示しません。ExecStartPre= は shell 経由で実行されないため、パイプや glob には /bin/sh -c '...' が必要です。