TT Lab
はじめる
学ぶ 学習パス コース

FDE総合演習:倉庫に同じ注文が3回届いた

今動いていることと起動時に立ち上がることは違う

TT Labで続きを見る

一言でいうと

「起動している」と「ブート時に起動する」は、別の事実です。前者は、いまプロセスがあるという意味で、後者は、systemdがブートのトランザクションにそのサービスを入れるように、enableがシンボリックリンクを作っておいたという意味です。顧客VMの引き継ぎは、後者を証明してはじめて終わります。

なぜ必要なのか

現場で最もよくあるインストールは、こうです。アプリをコピーして、シェルでnohup python3 app.py &を打ち、ブラウザーで開いてみて、「問題なく動いています」と報告します。数週間後、顧客がセキュリティパッチのためにVMを再起動すると、何も起動しません。そのプロセスは、サービス管理ツールが知らないプロセスだったからです。落ちても誰も復活させず、ブートのときに誰も起動しません。ログはnohup.outのどこかにあり、そのファイルも、誰かが消したかもしれません。

systemdのサービスとして上げれば、こうした問いに答えが生まれます。誰が起動するのか(systemd)、どのユーザーで(User=)、設定はどこから来るのか(EnvironmentFile=)、落ちたらどうするのか(Restart=)、ブートのときにいつ立ち上がるのか(WantedBy=・After=)、ログはどこにあるのか(journal)。FDEが顧客のVMに何かをインストールするなら、この6行が、引き継ぎ文書の骨格です。

どう動くのか

ユニットファイルと、ロードされた設定は違います。管理者が作ったユニットは、/etc/systemd/system/に置き、このパスが、ディストリビューションのパッケージの/usr/lib/systemd/system/より優先されます。ところが、systemdは、ファイルを毎回読むわけではありません。ロードしておいた設定で動くので、すでにロードされたユニットのファイルを直したら、systemctl daemon-reloadで読み直させる必要があります。直したあとにreloadを忘れると、systemctl showのNeedDaemonReload=yesで表に出て、systemctlが警告を出します。

WantsとAfterは、互いの代わりになりません。systemd.unitのマニュアルは、要求の依存(Wants=・Requires=)は、起動の順序に影響を与えず、順序は、After=・Before=で別に決めると、はっきり書いています。そのため、ネットワークが設定されたあとに起動すべきサービスなら、systemd.specialのマニュアルの言うとおり、network-online.targetをWants=で引き込み、同時にAfter=で、その後ろに立ちます。片方だけを書くと、引き込むだけで順序がないか、順序だけがあって、誰もそのtargetを引き込みません。

enableは、起動ではなくシンボリックリンクです。[Install]セクションは、普段、実行中には解釈されません。systemctl enableが、WantedBy=を読んで、multi-user.target.wants/にシンボリックリンクを作り、そのシンボリックリンクが、ブートのときに、multi-user.targetからこのサービスへ向かうWants=の依存の役割を果たします。逆に、systemctl startは、いま一度起動するだけで、シンボリックリンクを作りません。そのため、「起動しているのに、再起動すると起動しない」は、ほとんどいつも、enableの漏れです。

Restart=のデフォルトは、noです。on-failureは、0以外の終了コード、シグナルによる終了、タイムアウト、ウォッチドッグで再起動します。ただし、マニュアルは、SIGHUP・SIGINT・SIGTERM・SIGPIPEを、きれいな終了と見なします。そのため、kill -9(SIGKILL)で殺せば復活しますが、systemctl stopはもちろん、普通のkill(SIGTERM)も、on-failureを呼びません。再起動の間隔RestartSec=のデフォルトは100msで、あまり頻繁に起動すると、StartLimitIntervalSec=・StartLimitBurst=の制限に引っかかって、それ以上起動しなくなります(reset-failedで解きます)。

EnvironmentFileは、プロセスを起動するときに読みます。マニュアルは、このファイルが、プロセスの実行の直前に読まれると書いています。行ごとにKEY=VALUEで、#で始まる行は無視され、ファイルがなければ、サービスが起動に失敗します(パスの前に-を付ければ、なくても通ります)。したがって、トークンを変えたなら、ユニットファイルはそのままなので、daemon-reloadは必要なく、再起動してはじめて、新しいプロセスが新しい値を受け取ります。

サンドボックス化は、スコアで見ます。systemd.execのオプションで、サービスを閉じ込めます。NoNewPrivileges=yesは、execveで新しい権限を得られないようにし、ProtectSystem=strictは、/dev・/proc・/sysを除いたファイルシステム全体を、読み取り専用でマウントし(書き込む経路は、ReadWritePaths=で開きます)、ProtectHome=yesは、/home・/root・/run/userを空に見せ、PrivateTmp=yesは、専用の/tmpを与えます。systemd-analyze security <유닛>(プレースホルダーはユニット名です)は、こうした設定を調べて、0.0から10.0までの露出レベルを出します。高いほど、閉じ込めが緩いということです。マニュアルが強調するとおり、このスコアは、systemdが掛けた仕組みだけを見るので、「脆弱だ」という判定ではなく、比較のものさしです。

[Unit]
Wants=network-online.target
After=network-online.target

[Service]
User=orders
EnvironmentFile=/etc/orders/orders.env
ExecStart=/usr/bin/python3 /opt/orders-svc/app.py
Restart=on-failure
ProtectSystem=strict
ReadWritePaths=/var/lib/orders

[Install]
WantedBy=multi-user.target

現場での姿

ラボのVMで、前の担当者のプロセスをたどってみると(実測)、/proc/<pid>/cgroupは、経路(/system.slice/cloud-final.service)を示します。準備のスクリプトを動かしたユニットの中に乗っているだけで、そのユニットには、このアプリを再び起動する責任がありません。新しいサービスを上げても、最初の起動が失敗しますが、野良プロセスが、まだポートを握っているからです。systemctl startは、Type=simpleなので、プロセスを起動した瞬間に成功として返るため、原因は、journalのbind failed … Address already in useでしか見えません。Restart=on-failureでRestartSec=1なら、1秒ごとに同じ失敗を繰り返し、journalには、Failed with result 'exit-code'が、延々と積み重なります(実測)。この状態で5回を超えると、デフォルトの起動制限(10秒に5回)に引っかかります。逆に、kill -9は、status=9/KILLのあと、1秒で新しいPIDで復活し、普通のkill(SIGTERM)は、Deactivated successfullyで終わって、inactiveのまま残りました(実測)。

サンドボックス化を有効にするときにも、典型的な順序があります。ファイルを直すと、systemctlが、「Warning: The unit file, source configuration file or drop-ins of orders.service changed on disk. Run 'systemctl daemon-reload' to reload units.」と警告します(実測)。reloadして再起動すると、ReadWritePathsを抜かした場合、アプリがデータディレクトリに書き込めず、すぐに落ちます。このラボのサービスは、User=ordersだけを与えた状態で、露出スコアが9.2で、上の5つのオプションを足すと、8.3に下がりました(実測)。

実務で本当に大切なこと

次のラボですること

UbuntuのVMで、前の担当者がnohupで起動したアプリを見つけて、正体を記録し、専用のアカウントと環境ファイルを作ってから、ユニットを上げます。最初の起動の失敗の原因をjournalで探し、野良プロセスを停止してサービスとして立ち上げ、enableが作ったシンボリックリンクを確認します。kill -9で復活するかを見て、サンドボックス化を加えて、daemon-reloadの警告と露出スコアの変化を記録し、トークンを交換して、再起動で反映します。最後に、再起動なしで、ブートの条件を判定するチェックのスクリプトを、用意されたユニットに動かします。

参考ドキュメント: systemd.service(5)、systemd.exec(5)、systemd.unit(5)、systemd.special(7)、systemd-analyze(1)