アップグレードそのものは2時間、周辺整理は丸一日 — 母艦Mac StudioをSonomaからTahoeへ上げた記録

1. 「意外と大したことないのでは」

自宅の母艦であるMac Studio(M1 Max / 32GB)は、macOS Sonoma 14.8.5のまま動いていた。9月のApple発表でmacOS 27が出れば、2世代前のSonomaはセキュリティ更新の対象外になる。実際には調べてみると8月の14.8.9が最後の更新だったようで、「まだ大丈夫」の前提は既に崩れていた。

このMacは単なるデスクトップではない。家中のDHCPを配るdnsmasq、Dockerコンテナ5本(監視のUptime Kumaを含む)、Tailscaleで外部から到達できる公開経路5本、常駐のlaunchdジョブ30本。放置はできないが、上げる作業自体はApple Silicon同士なので淡々と終わるはず、というのが最初の読みだった。

その読みは半分正しかった。移行本体は2時間で終わった。ただし、その前後で丸一日使うことになった。

2. 事前調査で出た意外な結論

Claude Codeに読み取り専用の棚卸しを指示した。launchdジョブ30本(Daemon 13 / Agent 17)の依存バイナリ、Homebrewの96 formula、dnsmasqのリース41台、Dockerボリューム、Rosetta 2依存の有無。

結果は予想と違っていた。

最大のリスクはdnsmasqではなくFileVaultだった。 Tahoeは初回ログイン時にFileVaultの有効化を勧める画面を出す。FileVaultと自動ログインは設計上排他で、この機体はLaunchAgent 17本すべてが自動ログインにぶら下がっている。つまりFileVaultを有効にした瞬間、colimaが起動せず、Docker 5本が止まり、監視のKumaまで連鎖して落ちる。しかも停電復旧時はボリュームがロックされたままで、LaunchDaemonのdnsmasqすら人間のパスワード入力待ちになる。

ほかにも「Sonoma由来ではなくSequoia由来」の変更が2つ。/usr/bin/rsyncがTahoeではopenrsyncに切り替わり、NASへのrsync://デーモン接続を使っている唯一のバックアップが通らなくなる。/usr/bin/python3(Command Line Tools同梱の3.9.6)に依存するジョブが6本。

Rosetta 2依存はゼロだった。ここは心配不要だった。

3. アップグレード準備が、そのまま棚卸しになった

調査で見つかった穴は、macOSとは無関係のものが多かった。

Time Machineの宛先がなかった。 唯一動いていたバックアップがrsyncで、それがアップグレードで壊れる見込み。NASに機種別のユーザーと共有フォルダを作り、クォータ600GBで初回バックアップを回した。ついでに~/.colimaなど再生成可能なキャッシュ43GBを除外した。

通知先のない監視が4本あった。 Uptime KumaのmonitorをAPI経由で作ると、UIと違って既定の通知先が付かない。8月に「サイレント死対策」として追加したmonitor自身が、DOWNしても誰にも知らせない状態だった。

使用量監視が半月前から取得に失敗していた。 Claude Codeが認証情報の保存先を~/.claude/.credentials.jsonからmacOS Keychainに移した日に、監視スクリプトの読み取り先が古いままになっていた。認証失敗自体は24時間ごとにDiscordへ通知されていたが、誰も気に留めていなかった。

バージョン管理外の常駐が12本あった。 net-sentinel、使用量監視、Kumaのmonitor定義、release-watch……。「正がどこにあるか分からない」状態のまま動いていたものを、今回で全部リポジトリ化した。

旧Dockerボリュームを消す前に退避したら、現行DBに存在しないチャットが2件出てきた。 「旧スタックの残骸、退避の価値なし」というClaude Codeの当初の見立てを、退避を挟む判断で救えた。

事前対処として、rsyncはHomebrew版を絶対パスで指定、python3依存6本はHomebrew版へ移行。これでアップグレード当日の未知は、FileVault・TCC(ローカルネットワーク権限)・BTM(バックグラウンド項目の承認)・colimaのTahoe対応の4つに絞られた。

4. リハーサル — 14.8.9で再起動を2回

本番の前に、Sonoma内の点リリース14.8.9を当てた。目的はセキュリティ更新ではなく、「再起動で常駐30本が全部戻るか」をTahoe固有のリスク抜きで検証すること。この機体は最近再起動した記録がなく、「戻る」こと自体が未検証だった。

Claude Codeに差分比較スクリプトを作らせた。更新前にlaunchdの状態、brew services、colima、Docker、dnsmasq、待受ポート、Kumaの全monitor、ランタイム版などを採取し、更新後にcompareで突き合わせる。

設計上の判断が1つ。スクリプトはPASS/FAILを出さない。 DHCPリース数や使用率は動いて当然で、機械判定にすると嘘になる。差分は全部出して、「動いて当然のもの」「必ず差分なしであるべきもの」のリストを添え、判定は人間がする。

第1段(更新による再起動)、第2段(シャットダウン→物理清掃→コールドブート)、どちらも「必ず差分なし」は全項目クリーンだった。

副産物として地雷を1つ踏んだ。清掃後、GB10クラスタの2ノードを繋ぐ200G DACケーブルを右のQSFPケージに挿し直してしまい、リンクは上がるのに設定のある左ポートがNO-CARRIERのまま。vLLMのサービスが相方ノード待ちで止まり、Kumaの3本がDOWNした。管理LAN側ではpingもsshも通るので紛らわしい。

DAC挿入位置の誤り。設定のあるf0がNO-CARRIER

5. 本番 — 想定どおりと、想定外と

土曜10:28、colima stopを打って開始。

最初の想定外は、9/10に確保しておいたTahoeのフルインストーラが/Applicationsから消えていたこと。14.8.9の適用時にmacOSが掃除したらしい。再取得に5分。「点リリース適用後は本番直前にインストーラの存在を再確認」が教訓になった。

インストールは12:13に完了。直後の画面で「今後のアップデートを自動でインストール」を促されるが、ここは「自動ダウンロードのみ」を選ぶ。常時稼働の母艦が勝手に再起動したり、macOS 27に自動で上がる経路は塞いでおきたい。

アップデート完了画面。自動インストールは選ばない

そして事前調査で最大リスクと判定していた画面が、本当に出た。

FileVault勧奨画面。「今はしない」の1クリックが本日の最大リスク回避

「今はしない」。これで終わり。事前に機序を理解していたので迷いはなかったが、知らなければ「セキュリティ向上」に見える「オンにする」を押していたと思う。

compareの結果は、事前に潰した懸念がすべて想定どおりだった。/usr/bin/rsyncはopenrsyncになっていて、Homebrew版へ逃がしていなければバックアップが止まっていた。Command Line Toolsは無効化されず、xcode-selectも差分なし。

一方、Kumaの5本(NASとGB10クラスタ)がDOWNで出た。ローカルネットワーク権限を疑ったが、VM内からNASにもGB10にも到達できる。数分後に再実行すると全部消えた。起動直後、VMのネットワークが整う前にKumaがチェックを走らせて失敗しただけの一過性だった。

compare出力。Kuma 5本のDOWN(一過性)とrsync→openrsync

ここまでで当日の未知4つは全部白。移行完了、と思っていた。

あとから分かった77分

Claude Codeが後日ログを追って、1つ大きな発見をした。自動ログインは10:58に成立していたのに、colimaが動いたのは12:15。 その間の77分、LaunchAgent(colima・Docker・Kuma)は全部止まっていた。

原因はメジャーアップグレード後の初期設定画面(アップデート完了・FileVault・新機能紹介)だった。これらがユーザーセッションを占有している間、LaunchAgentは起動しない。私が画面を確認しながら1〜2分かけて進めていた時間が、そのまま停止時間に乗った。

14.8.9のリハーサルで検出できなかったのは、点リリースには初期設定画面が無いから。リハーサルでは検出できない構造だった。

救いは、dnsmasqをLaunchDaemonにしていたこと。LaunchDaemonは10:57に復帰していて、家中のDHCP停止は約30分で済んだ。LaunchAgentだったら105分だった。

6. 移行本体より手強かった brew upgrade

Tahoe移行が終わり、Homebrewの55 formulaを更新し、Command Line Toolsを27.0に入れ替えた。ここからが本番だった。

(d) 動いているプロセスが署名不一致で殺される。 brewがバイナリを差し替えると、その旧バイナリで動いていたLaunchAgentがOS_REASON_CODESIGNINGでSIGKILLされる。署名自体は有効で、手動実行なら動く。launchctl kickstart -kでは直らず、bootoutbootstrapが必要だった。

(e) ローカルネットワーク権限が全部失効した。 KumaがまたNASとGB10を見失った。今度は一過性ではない。VM内からNo route to host。ollama-proxy経由のGB10推論も、NASへのrsyncバックアップも通らない。

私は「brew upgradeでlimaが2.1.1→2.2.0に上がった退行」と診断し、切り戻しとpinを指示した。Claude Codeはその作業に入る前に互換性を確認する過程で、原因はlimaではなくTCC(Tahoeのローカルネットワーク権限)だと特定して止まった。 brew製バイナリは失敗し、システムバイナリは成功する、という対比が決め手だった。

ローカルネットワーク権限。アイコンが空白の項目は旧バイナリの残骸

移行直後に権限が「引き継がれた」ように見えたのは、古いバイナリが承認を保持していただけだった。TCCはパスではなくバイナリの同一性で承認を記録する。OS更新とパッケージ更新は別の試験である。 これが今日一番高くついた学びだった。

launchd配下のプロセスには許可ダイアログに答える相手がいないので、黙って拒否され続ける。対処は、対象のバイナリ(limactl・python3・rsync)をシェルから叩いてダイアログを誘発し、画面の前で「許可」を押すこと。

tailscaleも別の罠を踏んだ。 CLIとdaemonの版がずれていたのでsudo brew services restart tailscaleを打ったが効かず、代わりに--stateを渡さない別のLaunchDaemon定義が生成されていた。もしそちらが起動していたら別のstateを使い、ノードのidentityを失って再認証になっていた。正規のcom.tailscale.tailscaledを直接kickstart -kして復旧。旧daemonの終了理由は、やはりOS_REASON_CODESIGNINGだった。

監視の側から見た当日はこうだった。

Kuma→Discord通知。DOWN→UPの往復が実障害で機能した

7. 学び

事前対処は1勝1敗だった。rsyncは的中で、しかも想定より悪かった(/usr/libexec/rsync/の実体ごと消えていて、/var/select/rsyncで固定する案では救えなかった)。python3依存の移行は外れで、CLT 27.0も3.9.6を同梱していた。ただしbrew doctorが新CLTを要求している以上、入れ替えの局面は来る。無駄ではなかったが、予測としては外れ。

運用ルールとして残したもの:

  • (a) 点リリース適用後は、本番直前にフルインストーラの存在を再確認する
  • (b) 再起動後のcompareはログイン後5分以上待つ。起動直後の監視DOWNは一過性が多い
  • (c) メジャーアップグレードは初期設定画面がLaunchAgentを止める。人が画面の前にいて、最速で抜ける
  • (d) brew upgrade後は影響するLaunchAgentをbootoutbootstrap
  • (e) brew upgrade後は、LANへ出るbrew製バイナリのローカルネットワーク権限を再付与する(ダイアログの誘発が必要)
  • tailscaleはbrew servicesではなくcom.tailscale.tailscaledを直接再起動し、版一致を確認する

「意外と大したことない」は移行本体については当たっていた。手強かったのは周辺で、しかもその大半はmacOSとは無関係にこの機体が元々抱えていた弱点だった。アップグレードは、それを一度に表に出す機会になった。

次はmacOS 27。今回の記録と差分比較のベースラインがそのまま使える。ただし27.0を母艦に入れる気はないので、来年の春以降。