サービス起動順序の設計 — 依存関係の初期化制御

サービス起動順序の設計ミスは、依存制御の方式選定の誤りに起因することがほとんどです。systemdのAfterとRequiresの責務を混同したまま本番に持ち込んだり、KubernetesでinitコンテナとwaitForを場当たりに組み合わせたりすると、起動シーケンスが予測不能になります。依存先が起動済みでも「ready」でない状態でトラフィックを受けるリスクも別途存在します。本記事では、systemd・Kubernetesそれぞれで「何を選び、どこで線を引くか」という依存制御の設計判断軸を体系的に解説します。

systemdのAfter/Requires/Wantsをどう書き分けるか

AfterとRequiresの責務と組み合わせ判断

Afterディレクティブが制御するのは起動の「順序」であり、依存先の失敗が自サービスに伝播するかどうかは制御しません。Requiresと組み合わせて初めて「依存が失敗したら自分も停止する」という強依存が成立します。After単独で依存を表現したつもりが依存先が落ちても自サービスが生き続けるという状態は、一般的なつまずきのひとつです。BindsToはRequiresよりさらに強い結合で、依存先がactiveでなくなると即座に自サービスも停止します。DBとそのプロキシのように「片方だけ動いていても意味がない」ペアには有効ですが、本番環境での多用は1コンポーネントの障害が連鎖停止を引き起こすリスクを高めます。設計の出発点は「依存先が落ちたとき、自サービスはどう振る舞うべきか」という問いへの回答です。

Wantsが有効な判断場面とアンチパターン

WantsはRequiresの弱いバージョンで、依存先の起動失敗がシステム全体の失敗にならない場合に適しています。コアの処理とは独立したキャッシュサービスや監視エージェントが起動できなくてもコア処理を続けたい場合には、Wantsが向いています。判断の三択を整理すると「一緒に停止すべき」ならRequires、「起動を試みるが失敗しても続行する」ならWants、「順序だけ制御する」ならAfter単独です。よくあるアンチパターンは、Unit Bがactive状態になった時点でAfterが満たされたとみなされる挙動への誤解です。activeはプロセスが起動した状態ですが、サービスがリクエストを受け付けられるready状態ではありません。DBプロセスが起動してもTCP待ち受けが確立されるまで数秒かかる構成では、After単独では競合状態が発生します。このギャップを埋めるにはExecStartPostで待機処理を入れるか、Type=notifyやsd_notify連携を検討する判断が必要です。

起動順序をハードコードするか動的解決に任せるかの判断

ハードコードが設計上有効なケースと落とし穴

起動順序をUnit定義に明示する判断は、依存関係が静的で変化しないシステムに適しています。オンプレミスの3層構成(DB→AP→Web)のように依存先が固定で、スケールアウトも動的再構成もない環境では、Unit定義への明記が「誰が見ても依存関係が分かる」状態を保ちます。ハードコードの落とし穴は、依存が実態と乖離したまま維持される「ゾンビ依存」です。サービスを増減するたびにUnit定義を更新する運用が発生し、開発・ステージング・本番の3環境を抱えるチームでは環境ごとの定義ファイルの差分管理がボトルネックになりやすいです。

動的解決に任せる判断基準と移行期リスク

サービスメッシュやKubernetesのようにランタイムが依存関係をエンドポイント解決してくれる構成では、起動順序のハードコードを減らし、アプリケーション側でリトライ・フォールバックを持つ設計が有効です。この設計思想の前提は「サービスはいつ起動してもよく、依存先が準備できるまで待てる」という考え方です。移行期に問題になるのは「ハードコードの依存制御」と「動的解決」が同一サービスに二重にかかるケースです。想定例として、systemdの依存待機とinitコンテナの待機が直列化し起動に想定の2倍以上の時間がかかる状況が考えられます。移行は一度に全サービスを切り替えるより、サービス単位で責任範囲を明確にしながら進めることが安全です。

KubernetesのinitコンテナとwaitForを使い分ける設計

initコンテナを選ぶ判断基準

initコンテナはメインコンテナが起動する前に完了する処理を分離する仕組みで、設定ファイルの生成・DBマイグレーション・依存サービスへの疎通確認など「一度だけ実行して成功したら次へ進む」処理に向いています。責務を分離することでメインコンテナのイメージをシンプルに保てます。判断基準は「初期化処理が冪等かつ先行して完結できるか」です。initコンテナが失敗するとPodは再起動されinitコンテナから再試行されるため、冪等でない処理(同一IDのレコード挿入など)を置くと副作用が重複します。また、initコンテナが複数ある場合は直列実行が保証されるため、5つを超えると起動時間の累積が問題になります。並列初期化が必要なら、メインコンテナ起動後にアプリケーション側で処理するか、別Podへの責務分離を検討することになります。

waitFor系ツールの使いどころと設計上の注意

wait-for-it.shやdockerizeのようなwaitForツールは、依存サービスのTCPポート疎通またはHTTPレスポンスを確認するまで待機するアプローチです。initコンテナに組み込んで使うケースが多いですが、設計上重要なのは「何を確認するか」の粒度です。TCPポートが開いていてもアプリケーションが起動途中の場合があるため、HTTPの `/health` エンドポイントを確認する方が実態に近くなります。waitForツールをinitコンテナに組み合わせる際の落とし穴は、タイムアウト設定の欠落です。タイムアウトなしで待機するとPodが永遠にPending状態になるリスクがあります。サービスディスカバリで実行時に依存先が変わる構成では、initコンテナでの一度限りの確認では不十分で、メインコンテナ側でのリトライ設計が別途必要です。

readiness probeで依存サービスの準備を確認する設計

readiness probeとliveness probeの使い分け

readiness probeはKubernetesがPodをServiceのエンドポイントに登録するかどうかを制御し、失敗するとトラフィックが流れなくなります。依存サービスとの接続確立を確認するロジックをreadiness probeに組み込むことで「起動はしたが依存先との接続が未確立」の状態でトラフィックを受けるリスクを防げます。よくあるつまずきは、依存サービスへの疎通確認をliveness probeに組み込むことです。liveness probeが失敗するとコンテナが再起動されるため、依存先が一時的に落ちるたびに連鎖的な再起動ループが発生します。使い分けの原則は「自コンテナの問題」→liveness probe、「依存先や外部状態の問題」→readiness probeです。デッドロックや無限ループのような自コンテナ固有の問題にはliveness probeが有効ですが、外部依存の状態変化はreadiness probeで受けることが設計の基本です。

probeパラメータの設計判断

readiness probeのinitialDelaySeconds・periodSeconds・failureThresholdの組み合わせは、環境の特性に合わせた設計が必要です。初回起動に平均30秒かかるアプリケーションにinitialDelaySecondsを5秒に設定すると、起動中に何度もfailが記録されます。failureThresholdを大きくすることで補えますが、それは「何回失敗したら問題とみなすか」の設計判断を迂回しているだけです。アプリケーションの起動特性を計測し(目安として10回以上の起動ログを収集する)、実測値からパラメータを設定することが望ましいです。readiness probeに複数の依存確認ロジックを詰め込みすぎるとprobeが重くなり常にfailする状態が続くリスクがあるため、確認内容は必要最小限に絞ることが原則です。

外部サービス可用性を起動時に確認するスクリプト設計

確認処理の設計粒度とタイムアウト戦略

外部サービスの可用性を起動時に確認するスクリプトを設計する際、確認する項目と確認方法の粒度が設計判断の核心です。TCPポートの到達性確認は最低限のチェックですが、アプリケーションが実際に利用するエンドポイントの疎通を確認する方が信頼性は高くなります。ただし確認処理が複雑になるほど、スクリプト自体の信頼性を担保するコストも増えます。タイムアウト設計は必須で、最大待機時間(想定例として300秒)とリトライ間隔(想定例として5秒)を組み合わせ、上限に達したら起動失敗として終了する構成が一般的です。無限ループで待ち続けると、外部サービスが恒久的に停止している場合に起動が永遠に終わりません。タイムアウト後に「起動を続けるか」「失敗させてsystemd/Kubernetesに再起動を任せるか」の判断は、サービスが外部サービスなしでも部分的に機能するかどうかで変わります。

冪等性と副作用排除の設計制約

起動確認スクリプトが副作用を持つ設計は避けるべきです。確認の過程でDBにレコードを作成したり外部APIにPOSTリクエストを送ったりする設計は、リトライのたびに副作用が重複します。起動確認スクリプトはGET/HEADリクエストや読み取り専用クエリに限定し、副作用ゼロを設計上の制約にすることが原則です。スクリプトを変更する際もこの制約を守るため、コードレビューのチェックリストに「副作用ゼロか」の確認項目を追加することが有効な管理方法です。特にマイクロサービス構成で複数チームがスクリプトを管理する場合、この制約の明文化が後の問題を防ぎます。

起動失敗のリトライ回数と間隔をどこに設定するか

リトライ設定の責任一元化

systemdのリトライ設定はStartLimitIntervalSec/StartLimitBurstとRestartSecの組み合わせで制御します。設計判断として重要なのは「どこがリトライを管理するか」の一元化です。アプリケーション内部でもリトライを持ち、systemdでもリトライを持つ二重構造になると、障害時のログ解析が複雑になります。想定例として、アプリケーションが内部で最大10回リトライを行い、systemdがさらに3回再起動する場合、最大30回の試行ログが出力されます。責任範囲を分けるなら「アプリケーション内リトライは同一起動サイクル内の一時エラー対応」「systemdリトライは起動プロセス全体の失敗への対応」と設計で明確にすることが重要です。Kubernetesでは同様の概念がCrashLoopBackOffの指数バックオフとbackoffLimitに対応します。

指数バックオフとジッターの設計判断

複数のサービスが同時に起動して同一の外部サービスに接続確認を行う構成では、全サービスが同じ間隔でリトライすると外部サービスへのリクエストが集中します(サンダリングハード問題)。想定例として10サービスが同時に5秒間隔でリトライすると、外部サービスは5秒ごとにスパイクを受け続けます。これを避けるためリトライ間隔にジッター(ランダムな揺らぎ)を加える設計が有効で、基底間隔の10〜20%程度のジッターを加えることが一般的な設計指針です。ジッターの実装をアプリケーション側に任せるか、waitForスクリプト内で乱数を加算するかを設計フェーズで決めておくことが複数サービス間での一貫性につながります。KubernetesのJobリソースはbackoffLimitのデフォルト値(6回)が適切かどうかを外部APIの負荷特性に合わせて判断し、必要なら小さく設定することが安全です。

依存制御方式比較表(systemd / initコンテナ / スクリプト制御)

3方式の設計特性比較

制御方式 向いている場面 落とし穴・注意点 リトライ管理の主体
systemd(After/Requires/Wants) 静的な依存関係を持つオンプレミス・VM環境。Unit定義で依存を明示したいケース activeとreadyの混同。After単独では依存失敗が伝播しない。二重リトライによるログ肥大化 StartLimitIntervalSec/RestartSecによるsystemd管理
Kubernetes initコンテナ Pod起動前に完結する初期化処理。冪等なマイグレーション・疎通確認の分離 冪等でない処理の配置によるリトライ時の副作用重複。並列初期化が不可。5つ超で直列時間が累積 PodのrestartPolicyとCrashLoopBackOffの指数バックオフ
waitForスクリプト(シェルスクリプト制御) TCPポート/HTTPエンドポイントへの疎通確認。Kubernetes・Docker双方で使える汎用チェック タイムアウト設定の欠落によるPending永続化。副作用を持つ処理をスクリプトに混入するリスク スクリプト内のリトライロジックとタイムアウト設定

方式選定チェックリスト

  • 依存関係は静的か動的か。静的ならsystemdのUnit定義への明示が第一候補、動的ならアプリケーション側リトライか動的解決を検討する
  • 依存先が落ちたとき、自サービスはどう振る舞うべきか。「停止すべき」ならRequires/BindsTo、「続行すべき」ならWants、「順序のみ制御」ならAfterを選ぶ
  • 初期化処理はPod起動前に完結するか、かつ冪等か。両方YESならinitコンテナに置く。NOなら起動後の処理として設計する
  • readiness probeに何を確認させるか。外部依存の確認はreadiness probe、自コンテナの状態確認はliveness probeに分けて設計する
  • 起動確認スクリプトの処理は副作用ゼロか。GETリクエストや読み取り専用クエリのみに制限されているかをコードレビューで確認する
  • リトライ管理の責任はどこか。アプリケーション内リトライとsystemd/Kubernetesのリトライが二重にかかっていないかを設計段階で確認する
  • 複数サービスが同一外部サービスに同時接続確認を行う場合、ジッターを加えてサンダリングハードを防ぐ設計になっているか

▶ 手を動かして本番まで到達する

脱属人化の第一歩は「読める・書けるスクリプト」から始まります

ここまでの設計判断は、読めば腹に落ちます。ですが「自分の手で本番まで通しきる自信」は、それとは別の話になります。

自動化の設計を、手を動かして身につける2日間です。手順書に閉じ込めた構築を、他人が読めて再現できる形へ変えていきます。

サービス起動依存の制御設計をスクリプトで実践するシェルスクリプト実践コースへ →

シェルスクリプトマスターセミナー|自動化の設計を手で身につける2日間