0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

nvmのデフォルトを替えても常駐サービスは旧Nodeのままだった:ランタイム緊急更新で固定パスが残る問題と「更新完了」の測り方

0
Posted at

デフォルトを切り替えても固定パスで旧ランタイムへ繋がったままの常駐プロセスと、実プロセスを測る確認を表すアイキャッチ

2026年10月22日、個人のMacでNode.jsランタイムを緊急更新したときの記録です。対象はエンジニア・テックリードで、常駐プロセスを持つランタイムをどう更新し、何をもって「更新完了」と言うかを考えます。

実績区分(最初に明記します): 本稿は個人のMac運用での検証(2026-10-22、Node.js v24.11.1 → v24.22.1、launchd常駐4サービス)であり、業務・本番サーバーでの運用実績ではありません。systemd / cron / Docker / CIへの一般化部分は設計案です。PHP側は公式発表の確認のみで、未検証です。数値・版数は、実測したものと公式発表で確認したものを分けて書きます。

結論:「更新完了」は設定の書き換えではなく、新プロセスが新版を掴んだ実測時点

ランタイムの「更新完了」は、設定ファイルを書き換えた時点ではなく、新しく起動したプロセスが実バイナリとして新版を掴んでいることを実測した時点、と定義することを推奨します。

今回、対話シェル側の既定は新版に切り替わっても、常駐サービスは旧版のまま動き続けました。理由は単純で、サービス定義側に旧版への固定パスが残っていたからです。ファイルの更新とプロセスの更新は別の段階であり、後者は実測しないと分かりません。

少量の手動起動スクリプトしかない環境なら、固定箇所を都度直す運用でも足ります。常駐サービスが複数あり、セキュリティ更新を確実に届けたい場合は、固定箇所の棚卸しと実測の手順を先に決めておく価値があります。

きっかけ:Node.jsとPHPで同日に重大な更新が出た

2026年10月21日、Node.jsプロジェクトは26.x / 24.x / 22.x向けのセキュリティリリースを公開し、修正版としてv26.12.1 / v24.22.1 / v22.23.4を示しました。最も深刻なものはHighで、Brotliデコーダの境界外書込みによるリモートコード実行(CVE-2026-111497)などが含まれます。Node.js公式(October 21, 2026 Security Releases)

同じ頃、PHP側でもクリティカルなリモートコード実行に関するセキュリティ勧告が出ており(いわゆるPHP2Shellとして報じられた一連の話題です)、「手元のランタイムをまとめて点検する」動機になりました。PHP公式ニュースアーカイブ(2026-10-21のsecurity advisory) ただし本稿で実際に更新・実測したのはNode.jsだけです。PHPについては公式発表の確認のみで、自分の環境での検証はしていません。

技術的な問いと前提・制約

問いはこれです。

バージョン管理ツールの「デフォルト」を新版に替えたのに、なぜ常駐プロセスは旧版のままなのか。どこを直せば切り替わり、何を確認すれば「完了」と言えるのか。

前提と制約は次のとおりです。

  • 個人のMac(macOS)で、バージョン管理にnvmを使っている。
  • launchdで常駐する自作サービスが4つあり、いずれもNode.jsで動く。
  • 更新前はv24.11.1、更新先は公式修正版のv24.22.1。旧版はアンインストールせず残してある(切り戻し用)。
  • 目的は「常駐4サービスすべてが新版で動いている状態」を作り、それを証拠付きで確認すること。サービスの機能変更はしない。

対話シェルでは、新しいターミナルを開けばnvmのデフォルトが効きます。しかしlaunchdのジョブはログインシェルを経由しないため、シェルの初期化で決まる「現在のデフォルト」を自動では引き継ぎません。ここが今回の落とし穴でした。

何が起きたか:デフォルトを替えても、常駐は旧版のまま

手順どおり新版を入れ、nvmのデフォルトを新版に替えました。新しいターミナルでは新版が返ります。

次に常駐サービスを再起動しました。ところが、再起動後のプロセスが掴んでいる実行ファイルを確認すると、4サービスとも旧版のままでした。

サービス定義を調べると、固定箇所は2種類・合計7か所ありました。

固定のされ方 か所数 中身
サービス定義ファイル(plist)の環境変数PATH先頭 4 旧版のバージョンディレクトリを先頭に固定
起動スクリプト(run.sh)の実行ファイル指定 3 exec '<旧版の絶対パス>/node' ... と直書き

残り1サービスは起動スクリプト側がPATH上のnodeを引く形だったため、定義ファイル側のPATH固定だけが実質の固定点でした。つまり「nvmのデフォルト」は、対話シェルの入口を替えただけで、常駐サービスの入口は別々に固定されていた、という構造です。

選択肢の比較:固定するか、追随させるか

常駐サービスがランタイムを指す方法は、大きく3つあります。

方法 再現性 更新時の作業 リスク・向く条件
絶対パス固定(バージョン直書き) 高い。起動する版が定義から読める 版ごとに全固定箇所の書換え+再読込みが必須 書き換え漏れがそのまま旧版稼働になる。版を厳密に揃えたい常駐向き
既定参照(PATH / shim / デフォルトリンク経由) 中程度。参照先を替えた時点で全体が動く 参照先の切替1か所 意図せず版が変わる事故が起きうる。個人の常駐や開発機向き
起動時に版を検証して落ちる 高い(不一致を検知できる) 検証の実装と、落ちたときの復旧手順が要る 「旧版で動き続ける」より「止まって気づく」ほうが安全なサービス向き

どれが正解ではなく、サービスごとに「再現性(固定)」と「鮮度(追随)」のどちらを優先するかで選びます。今回の4サービスは、普段は勝手に版が変わらない固定の安心感を取りつつ、緊急更新のときだけ確実に切り替える、という運用に寄せました。その代償として、書換え漏れを防ぐ棚卸しと実測が必須になります。

採用した設計とその理由

採用したのは、固定は維持し、更新手順を「棚卸し → 書換え → 定義の再読込み → 新プロセスの実測」の4段階にする方法です。

理由は3つです。

  1. 固定自体は悪くない。 常駐サービスでは「どの版で動いているか」が定義から読めること自体に価値があります。問題は固定したことではなく、固定箇所を数え切れていなかったことです。
  2. セキュリティ更新では「旧版のまま稼働」が一番見えにくい。 エラーが出ないため、設定を直したつもりでも静かに旧版が残ります。完了の定義を実測に置くと、この取りこぼしを検出できます。
  3. 個人環境では自動追随より、手順の確実さが効く。 常駐が4つなら棚卸しは数分で終わります。自動で追随させる仕組みを作るより、更新のたびに同じ確認をするほうが、今の規模では単純で安全です。

逆に、サービス数が多い、複数台ある、といった規模ではこの手作業は破綻します。その場合は既定参照への寄せ方や、版不一致を検知して通知する仕組みを先に検討すべきで、本稿の手順はそのまま使えません(設計案としての一般化は後述します)。

実装の要点:固定箇所の棚卸しと切替手順

実際に使った手順を、サービス名・ローカルパスを一般化して示します。

1. 固定箇所を数える

バージョンディレクトリへの参照を、サービス定義と起動スクリプトの両方から洗い出します。

# サービス定義(plist)と起動スクリプトから、版固定の参照を洗い出す
grep -l 'versions/node' ~/Library/LaunchAgents/*.plist
grep -rn 'versions/node' /path/to/services/*/run.sh

ポイントは、定義ファイルのPATHと、スクリプト内の実行ファイル指定は別の固定だということです。片方だけ直すと、スクリプトがPATHに頼らず旧版を直接実行する、といった食い違いが残ります。

2. 書き換える(バックアップを残す)

旧版の文字列を新版へ置き換えます。置換前に、対象ファイルのバックアップを日時付きで残しました。置換後は、旧版文字列の残存がゼロであることを再検索で確認します。

# 置換後: 旧版の残存ゼロと、新版の件数を確認する
grep -rn 'v24.11.1' ~/Library/LaunchAgents/*.plist /path/to/services/*/run.sh || echo '旧版の残存なし'

3. 定義を再読込みする(ここが今回の核心)

launchdでは、サービス定義ファイルを変更しても、読み込み済みのジョブ定義は自動では差し替わりません。再起動だけでは古い定義のままプロセスが立ち上がります。定義を読み直させるには、いったん解除して登録し直します。

# 定義ファイルを読み直させる: 解除 → 登録 → 起動
launchctl bootout gui/$(id -u)/<サービス名>
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/<サービス定義>.plist
launchctl kickstart -k gui/$(id -u)/<サービス名>

<サービス名>とファイル名は自分の環境の値に読み替えてください(本稿では実名は出しません)。

4. 新プロセスの実バイナリを実測する

最後に、起動したプロセスが実際に掴んでいる実行ファイルを確認します。これが「更新完了」の証拠です。

# 起動中ジョブのPIDを取る
launchctl print gui/$(id -u)/<サービス名> | grep -E 'pid =|state ='

# そのプロセスが掴んでいる実行ファイル(txt)を見る
lsof -p <PID> | grep ' txt '

今回は4サービスすべてで、新版ディレクトリ配下のnodeが実バイナリとして表示されることを確認して、初めて完了としました。node -vを対話シェルで打つ確認だけでは、常駐プロセスの版は分かりません。

失敗・ハマりどころ

ハマり1: 再起動しても切り替わらない

当初は再起動コマンドでプロセスを立て直せば切り替わると思っていました。しかし実際は旧版のままでした。原因は、再起動が「読み込み済みの定義でプロセスを再実行する」動作であり、書き換えた定義ファイル自体は再読込されていなかったことです。定義を解除→登録し直して初めて、新しいPATHと起動スクリプトでプロセスが立ち上がりました。

教訓は、「プロセスを再起動した」と「新しい定義で起動した」は別だということです。設定ファイルを変更するタイプの常駐では、再読込の要否を手順に含めます。

ハマり2: 再起動が遅れて見える

一部のサービスには、短時間に何度も再起動しないための間隔設定(ThrottleInterval)が入っており、サービスによっては300秒に設定されていました。この場合、再起動を指示してもすぐには新しいプロセスが立ち上がらず、「切り替わっていないのでは」と見えます。

ここで焦って設定をいじり直すと、状態が分からなくなります。まずジョブの状態とPIDがどうなっているかを確認し、間隔の経過を待ってから実バイナリを測る、という順序にしました。「まだ起動していない」と「旧版で起動した」は、実測すれば区別できます。

ハマり3: 対話シェルの確認で安心してしまった

新しいターミナルで新版が返った時点で、更新できた気になっていました。しかしそれは対話シェルの入口の確認にすぎません。常駐プロセスは別の入口(定義ファイルと起動スクリプト)から起動します。確認した経路と、実際に動いている経路は一致させる必要があります。

本番運用への一般化(設計案)

ここからは、今回の個人Macでの実測をもとにした設計案です。本番サーバーでの運用実績ではありません。

同じ構造は、macOS以外でもよく見ます。

  • systemd: ユニットのExecStartに絶対パスを書けば固定、環境ファイルやPATHに頼れば追随寄りになります。ユニット変更後に定義の再読込が必要な点も、今回の話と同じです。
  • cron: 対話シェルと違い、cronのPATHは最小限です。「手で実行すれば動くのにcronでは旧版(または見つからない)」は、入口の違いとして定番の失敗です。
  • Docker / CI: イメージタグやランタイム版のピン留めは固定の代表例です。ピン留め自体は再現性のために正しく、問題は「ピンを更新したつもりで、実際に動く成果物が旧版のまま」になることです。ビルド済み成果物や実行中コンテナの版を測るところまでを更新手順に入れます。

どの環境でも共通する完了条件は、次の3点です。

  1. 固定箇所を数え切れている(定義・スクリプト・環境変数・成果物)。
  2. 新しく起動したプロセス/成果物が新版であることを、版表示ではなく実体で確認する。
  3. 旧版で動き続けた場合に気づける(版の監視、起動時検証、棚卸しの定期実行など)。

特に3は、今回のような「静かに旧版で動き続ける」障害への対策です。エラーが出ない失敗ほど、完了の定義を実測に置くか、不一致の検知を入れるかしないと残ります。

次に改善すること

今回で手順はできましたが、まだ手作業です。次に手を入れるなら次の順です。

  1. 棚卸しの一発確認: 固定箇所の検索と、各常駐プロセスの実バイナリ版表示を1つのスクリプトにまとめ、更新のたびに同じ出力で比べられるようにする。
  2. 起動時の版記録: サービス起動時に、自分が掴んだランタイムの版をログへ1行残す。小さな仕掛けですが、「いつの再起動から新版か」を後から追えます。今回は未実装です。
  3. 旧版の扱いを決める: 切り戻し用に残した旧版を、常駐がすべて新版で安定した後にどうするか(残す期間と削除の判断)を決める。残し続けると、将来の棚卸しで混乱の元になります。
  4. PHP側の点検: 今回は公式発表の確認のみで未検証です。PHPを使う環境がある場合は、そちらの版と常駐の有無を別途確認します。

まとめ

  • バージョン管理ツールのデフォルト切替は、対話シェルの入口にしか効かないことがあります。常駐サービスは定義ファイルや起動スクリプト側に固定パスを持ちがちです。
  • 固定(再現性)と追随(鮮度)はトレードオフです。どちらを選んでも、「固定箇所を数える」「定義の再読込が要るか」「新プロセスの実体を測る」を手順に含めます。
  • 「更新完了」は、設定を書き換えた時点ではなく、新しく起動したプロセスが実バイナリとして新版を掴んでいることを実測した時点と定義すると、静かに旧版で動き続ける取りこぼしを防げます。

改めて実績区分を示します。本稿の実測は個人のMac運用(2026-10-22、Node.js v24.11.1 → v24.22.1、launchd常駐4サービス、固定箇所7か所の書換えと実バイナリ確認)であり、業務・本番サーバーでの運用実績ではありません。PHP側は公式発表の確認のみ・未検証、systemd / cron / Docker / CIへの展開は設計案です。

参考資料

いずれも2026-10-22に確認。公式発表は仕様・脆弱性情報の根拠であり、本稿の環境で再現した証拠ではありません。

この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表。AI × Web × 業務自動化の開発・検証に取り組んでいます。
技術選定・品質・運用の判断材料を、検証範囲と制約を示して発信しています。
AI導入や業務自動化の技術選定・設計整理については、対応できる範囲を確認のうえご相談を承ります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?