M1 MacからWindowsとUbuntu VMをSSHで従える — インストーラ無しのクロスベンチ環境構築 (OpenSSH / MinGit / node.exe単体)

AI

Rust製x86エミュレータ rustx86 のベンチマークを、M1 Mac・Windows実機・その上のUbuntu VMの3環境で横断的に取りたくなった。本編 (最適化戦記) で使った比較表の裏側 — M1 Macのターミナルから他の2環境を全部リモート操作する環境をどう作ったかの記録である。方針は一つ、インストーラを使わない。単体バイナリを置くだけなら、他人のマシン (未来の自分の別マシン含む) を汚さず、消すのも rm 一発で済む。

全体像

M1 Mac (指令塔)
 ├─ ssh → Ubuntu VM (VirtualBox on Windows機)   … Linux x86_64 の実測
 ├─ ssh → Windows実機 (OpenSSH Server)          … 素のWindows x86_64 の実測
 └─ git → private リポジトリ (検証イメージ配布)  … 3環境でテスト条件を統一

Ubuntu VM側 — sudo無しで揃える

VMへは元々 halogin (ssh + 鍵のalias) で入れるようにしてあった。足りないのはRustとNode。どちらもユーザー権限だけで入る:

# Rust: rustup (ユーザーのホーム下に入る)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
# Node: nvm (同上)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
nvm install --lts

罠が1つ: 非対話ssh (ssh host 'コマンド') では .bashrc が先頭のインタラクティブ判定で即returnするので、nvmのPATHが通らない。リモート実行では毎回 . ~/.nvm/nvm.sh && を前置する。

Windows実機側 — OpenSSH Serverは標準搭載

Windows 10/11にはOpenSSH Serverが最初から入っている (無効なだけ)。管理者PowerShellで1行:

Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0; Start-Service sshd; Set-Service -Name sshd -StartupType Automatic

鍵認証には管理者ユーザー特有の罠がある。~/.ssh/authorized_keys ではなく C:\ProgramData\ssh\administrators_authorized_keys を見る仕様で、しかもACLを絞らないとsshdが黙って無視する:

Add-Content -Force C:\ProgramData\ssh\administrators_authorized_keys "ssh-ed25519 AAAA..."
icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r /grant "SYSTEM:F" /grant "BUILTIN\Administrators:F"
Restart-Service sshd

これでMacから ssh user@windows機 が通る。デフォルトシェルはcmdなので、以降のリモートコマンドはcmdの作法で書く。

インストーラ無し主義 — node.exeとMinGitは「置くだけ」

Windows側にNodeとgitが要る。wingetはssh越しだと動かないことがあるし、そもそもマシンを汚したくない。実は両方とも単体配置で足りる:

# Node: node.exe 1ファイルで動く (npmが要らなければこれで十分)
curl -sL -o node.exe https://nodejs.org/dist/latest-v22.x/win-x64/node.exe
# git: MinGit (git-for-windowsのポータブルzip、インストーラ無し)
curl -sL -o mingit.zip https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.3/MinGit-2.55.0.3-64-bit.zip

scpで送って、Windows標準の tar.exe (Win10 1803から入っている) で展開するだけ。ソースコード本体もgitが無い段階では tar czf - . | ssh ... "tar xzf -" で送りつけた — bsdtarはgzipを読めるので、これで全部つながる。

踏んだ罠の記録 — デバッグの読みどころ

NodeのESMがWindowsのパスで死ぬ

ベンチスクリプト (headless.mjs) がWindowsでだけ落ちた:

Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: ... Received protocol 'c:'

動的 import()C:\... の絶対パスを渡すと、ドライブ文字の c: がURLスキームとして解釈される。macOS/Linuxの / 始まりパスは偶然通るので、Windowsで初めて発火する。修正は1行 — pathToFileURL() を通す:

const mod = await import(pathToFileURL(join(root, 'web/pkg/rustx86_wasm.js')).href);

cmdの set は引用符が命

ssh越しのcmdで環境変数付きコマンドが黙って効かない:

set GIT_SSH_COMMAND=ssh -i C:\Users\...\key& git clone ...   :: NG (静かに壊れる)
set "GIT_SSH_COMMAND=ssh -i C:/Users/.../key" && git clone ...  :: OK

set "VAR=値" と引用し、パスはスラッシュにする。cmdは失敗を教えてくれないので、ssh -T git@github.com で鍵単体の疎通を先に確かめて、原因を「鍵」と「渡し方」に切り分けたのが決め手だった。

KVMが無い謎 → 犯人はHyper-V

Ubuntu VMに /dev/kvm が無い。VirtualBoxのネスト仮想化を疑ったが、正体はWindowsでWSL2/Docker用にHyper-Vを有効化していたこと。Hyper-Vが居るとVirtualBoxはHyper-V上の互換モードで動き、VT-xをゲストに渡せない。ちなみにこの互換モードの性能ペナルティを実測したら5〜9%しかなかった (エミュレータのような純ユーザー空間演算はVM exitをほぼ起こさない)。「VMだから遅い」はこのワークロードでは迷信だった。

検証イメージはprivateリポジトリで配る

3環境で同じカーネル・同じinitramfsを使わないと比較にならない。毎回scpで撒くのは事故のもとなので、privateリポジトリに置いて全環境からgitで取る形にした。

ライセンスの整理もしておく。中身はLinuxカーネルとBusyBox (GPLv2) のバイナリだが、GPLの義務 (ソース提供) が発火するのは第三者への頒布の瞬間。自分のマシン間の同期とprivateリポジトリへの保管は私的複製の範囲で、義務はそもそも掛からない。publicにした瞬間・他人を招待した瞬間から頒布になるので、その注意書きをリポジトリのREADME先頭に書いた。公開側の再現性は、Alpineミラーから取得するスクリプトが既に担っている — この分業が一番きれいだと思う。

取得側の認証はread-onlyのデプロイキーにした。個人アクセストークンと違ってリポジトリ1個に閉じるので、ベンチ用マシンに置いても被害半径が最小で済む:

GIT_SSH_COMMAND="ssh -i ~/.ssh/images-deploy-key" git clone git@github.com:.../rustx86-images.git

まとめ

  • 3環境ともsudo/インストーラ無し (rustup・nvm・node.exe単体・MinGit zip) で揃った
  • Windowsの罠: administrators_authorized_keys + icacls、ESMのドライブ文字、cmdのset引用
  • 仮想化の税は実測5〜9% — 犯人扱いする前に測る
  • イメージ配布はprivateリポジトリ+read-onlyデプロイキー。GPLは私的複製に義務なし、公開する日は表記を整えてから

この環境が回り出した瞬間、「M1で効かなかった最適化がx86でも効かないこと」を10分で検証できた。環境構築は面倒だが、検証の単価を下げる投資としては本編の最適化と同じ話である。

コメント

タイトルとURLをコピーしました