再起動したら何も立ち上がらなかった
このラボは本物のVM上で動きます
Ubuntu 24.04のVM1台です。systemdがPID 1で、Kubernetesはありません。準備の段階で、小さな注文照会アプリ(/opt/orders-svc/app.py)を入れ、前の担当者がシェルからnohupで起動しておいた状態を再現しておきます。採点はVMの中のエージェントが行うので、再起動しないでください。採点が途切れます。ブート時に起動するかは、enableの状態とtargetの依存で判定します。
目標
顧客のVMに、アプリを、専用ユーザー・環境ファイル・再起動のポリシー・ブートのtargetを備えたsystemdのサービスとしてインストールし、enableの漏れ・kill -9・daemon-reloadの漏れ・環境ファイルの交換が、それぞれ何を変えるのかを自分で確認したあと、引き継ぎ用のチェックスクリプトで終えます。
なぜ重要なのか
「インストールして、問題なく動いています」は、いま起動しているという意味にすぎません。シェルから起動したプロセスは、サービス管理ツールが知らないプロセスなので、落ちても誰も復活させず、再起動しても誰も起動しません。systemctl startだけを行って、enableを抜かしても同じです。ブート時の起動は、[Install]セクションがenableのときに作るシンボリックリンク1つにかかっていて、ユニットファイルを直したあとにdaemon-reloadを忘れると、systemdは古い設定で動き続けます。この違いを、1回ずつ自分の手で作ってみてはじめて、顧客の現場で、「なぜ何も起動しないのですか」に、5分以内に答えられます。
想定所要時間は60分です。準備に数分かかります。セッションが終わるとVMが消えるので、残したいファイルは、終了の前に別に保管してください。
ステップ
- ポート8181を握っている前の担当者のプロセスを探し、
pid=・user=・cgroup=(/proc/<pid>/cgroupの経路)・unit=(その経路の最後の断片)・survives_reboot=(yesまたはno)の5行で記録します(ファイル:/root/svc/legacy.txt)。 - ログインできないシステムアカウント
ordersを作り、/var/lib/orders(所有者はorders、750)と、環境ファイルを作ります(root:orders、640。ファイル:/etc/orders/orders.env)。環境ファイルには、ORDERS_PORT=8181、16文字以上の新しいORDERS_TOKEN、ORDERS_DATA_DIR=/var/lib/ordersを入れます。 - ユニットを書きます(User=orders、EnvironmentFile、ExecStart=
/usr/bin/python3 /opt/orders-svc/app.py、Restart=on-failure、WantsとAfterにnetwork-online.target、WantedBy=multi-user.target。ファイル:/etc/systemd/system/orders.service)。daemon-reloadのあとで起動すると失敗します。journalで、アプリが残した原因の行(bind failed pid=…)を探して、そのまま書き写します(ファイル:/root/svc/why-failed.txt)。 - 前の担当者のプロセスを停止して、orders.serviceを起動します。
curl http://127.0.0.1:8181/healthzが、ordersユーザーと、サービスのメインプロセスのPIDで応答する必要があります。 - サービスをenableし、そのとき出た出力(作られたシンボリックリンクの経路を含む)を保存します(ファイル:
/root/svc/enable.txt)。 - メインプロセスを
kill -9で殺して、systemdが復活させるかを見ます。old_pid=・new_pid=を記録します(ファイル:/root/svc/kill.txt)。 - ユニットに、サンドボックス化(NoNewPrivileges=yes、ProtectSystem=strict、ProtectHome=yes、PrivateTmp=yes、ReadWritePaths=/var/lib/orders)を加えます。変更する前に、
systemd-analyze securityのスコアを測っておき、ファイルを直した直後にsystemctlが出すdaemon-reloadの警告を保存し(ファイル:/root/svc/reload.txt)、daemon-reloadと再起動を行って、もう一度測ります。before=・after=を記録します(ファイル:/root/svc/security.txt)。 - 環境ファイルの
ORDERS_TOKENを、新しい値に変えて、サービスに反映します。old_token_sha=・new_token_sha=(トークンのsha256の16進数)と、daemon_reload_needed=(yesまたはno)を記録します(ファイル:/root/svc/rotate.txt)。 - 引き継ぎ用のチェックスクリプトを書きます(ファイル:
/root/svc/verify.sh)。bash verify.sh <유닛>(プレースホルダーはユニット名です)が、そのユニットが、再起動のあとでも起動する条件(ロードされている、enabled、WantedByにmulti-user.target、Restartがon-failureまたはalways、WantsとAfterにnetwork-online.target、Userが空でなくrootでない、NeedDaemonReload=no)をすべて満たしていれば0、1つでも満たしていなければ、満たしていない項目を出力して、0以外の値で終わります。採点ツールは、用意された他のユニット(orders-audit・orders-report・orders-worker・orders-sync・orders-rootjob)にも動かしてみます。
参考
- ポートの持ち主:
ss -ltnp 'sport = :8181'。プロセスを抱えたユニット:systemctl status <pid>、または/proc/<pid>/cgroup。 - 属性は、ファイルではなく、systemdがロードした値で見ます:
systemctl show orders.service -p User,Restart,Wants,After,NeedDaemonReload。 - 失敗を何度も繰り返すと、
start-limit-hitで、それ以上起動しなくなります。systemctl reset-failed orders.serviceのあとで、もう一度起動します。 - よくある間違い1:
After=network-online.targetだけを書く。順序を決めるだけで、そのtargetを引き込みません。 - よくある間違い2:
systemctl startで起動しているのを見て終わる。ブート時の起動は、enableが作るシンボリックリンクが決めます。 - よくある間違い3: ProtectSystem=strictだけを有効にする。データディレクトリまで読み取り専用になって、アプリが
data dir errorで落ちます。
8181を握っている野良プロセス
8181をリッスンしているプロセスのpid・user・cgroup・unitと、再起動のあとの生存の有無を記録してください(ファイル: /root/svc/legacy.txt)。
ssの-pは、ソケットを持つプロセスを見せてくれます。/proc//cgroupの経路は、このプロセスが、どのユニットの中で生まれたかを教えてくれます。そのユニットが、このアプリを起動する責任を負ったユニットかどうかを、考えてみてください。
専用アカウントと環境ファイル
システムアカウントordersと、/var/lib/orders(orders、750)を作り、環境ファイルを作ってください(root:orders、640。ファイル: /etc/orders/orders.env)。
useraddの--systemは、システムのUIDの帯域を使い、--shellで、ログインできないシェルを与えます。環境ファイルには、トークンが入っているので、サービスのアカウントだけが読めるように、グループと権限を合わせてください。wikiの古いトークンは、すでに漏れた値です。
最初の起動が失敗した理由をjournalで
ユニットを書いて起動してみたあと(ファイル: /etc/systemd/system/orders.service)、journalの失敗の原因の行を書き写してください(ファイル: /root/svc/why-failed.txt)。
アプリの標準出力は、journalに行きます。journalctl -u 유닛 -o cat(プレースホルダーはユニット名です)は、先頭の情報なしで、メッセージだけを見せてくれます。systemctl startが成功で終わっても、Type=simpleは、プロセスを起動した瞬間に成功なので、実際の結果は、statusとjournalで見る必要があります。
野良プロセスを停止して、サービスとして立ち上げる
前の担当者のプロセスを停止して、orders.serviceをactiveにしてください。healthzが、ordersユーザーと、メインプロセスのPIDで応答する必要があります。
rootで動いているアプリだけを選んで停止してください(ordersアカウントのプロセスを一緒に殺してはいけません)。前のステップの失敗が繰り返されて、起動回数の制限に引っかかっているなら、reset-failedが必要です。
enableが作るシンボリックリンク1つ
orders.serviceをenableして、出力(シンボリックリンクの経路を含む)を保存してください(ファイル: /root/svc/enable.txt)。
enableは、サービスを起動しません。[Install]のWantedByを読んで、そのtargetの.wantsディレクトリに、シンボリックリンクを作るだけで、ブート時に、そのtargetがこのサービスを引き込みます。メッセージは、標準エラー出力に出ます。
kill -9しても復活するか
メインプロセスをkill -9で殺し、復活したPIDとあわせて、old_pid=・new_pid=を記録してください(ファイル: /root/svc/kill.txt)。
メインプロセスのPIDは、systemctl showのMainPIDです。Restart=on-failureが、どの終了を失敗と見なすかを、マニュアルの表で見てください。RestartSecだけ待ってから、新しいPIDができます。
サンドボックス化と、daemon-reloadの漏れ
スコアを測り、ユニットにサンドボックス化を加え、daemon-reloadの警告を残して(ファイル: /root/svc/reload.txt)から反映し、before=・after=を記録してください(ファイル: /root/svc/security.txt)。
systemd-analyze securityの最後の行が、全体の露出スコアです(低いほど、狭く閉じ込めています)。ProtectSystem=strictは、ファイルシステム全体を読み取り専用にするので、アプリが書き込む経路を、別に開く必要があります。ファイルを直した直後のsystemctl statusを、読んでみてください。
トークンの交換は、何で反映されるのか
ORDERS_TOKENを新しい値に変えて、サービスに反映し、old_token_sha=・new_token_sha=・daemon_reload_needed=を記録してください(ファイル: /root/svc/rotate.txt)。
EnvironmentFileは、ユニットの設定ではなく、プロセスを起動する直前に読むファイルです。ユニットファイルを直したのと何が違うのか、そして、すでに動いているプロセスの環境が、いつ変わるのかを、考えてみてください。ハッシュは、改行なしで、トークンだけを入れて計算します。
再起動なしでブートを証明するチェックスクリプト
ユニットが、再起動のあとでも起動する条件を、すべて満たしているかを判定するスクリプトを書いてください(ファイル: /root/svc/verify.sh)。
ファイルをgrepせずに、systemctl showとis-enabledで、systemdがロードした値を見てください。存在しないユニットも、showは成功しますが、LoadStateがnot-foundです。1つの項目ですぐに終わらせず、満たしていない項目をすべて出力すれば、引き継ぎを受ける人が、一度に直せます。