最初に手に入るもの
KVMFLUXから専有のクラウドMac mini M4をレンタルすると、実機のApple Silicon上でroot相当の管理者権限を得られます:16GBユニファイドメモリ、256GB SSD、macOSプリインストール済み、SSHとVNCで接続可能。何も共有せず何も仮想化しないため、xcodebuildからはM4のフル性能が見え、Secure Enclaveもデスクの実機と同様に動作します。
リージョンはチームの場所ではなく、成果物ストレージに近い場所を選んでください。シンガポール、日本、韓国、香港、米国東部、米国西部にあるランナーは、人間が操作する頻度よりもGitホストやCDNとやり取りする頻度のほうがはるかに高いからです。
ステップ1 — まずSSHを固める
マシンはログインできるようパスワード認証が有効な状態で届きます。最初のセッションでこれを終わらせましょう。鍵を登録してから、パスワードを無効化します。
ssh-keygen -t ed25519 -f ~/.ssh/kvmflux_ci -C "ci-runner"
ssh-copy-id -i ~/.ssh/kvmflux_ci.pub admin@<YOUR_MAC_HOST>
sudo tee -a /etc/ssh/sshd_config <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
EOF
sudo launchctl kickstart -k system/com.openssh.sshd
CIノードとして重要な設定がもう2つあります。マシンがスリープしないようにし、ディスプレイが接続されていないときにバックグラウンドデーモンが一時停止しないようにします。
sudo pmset -a sleep 0 displaysleep 0 disksleep 0
sudo systemsetup -setrestartfreeze on
VNCは非常時の脱出経路として残しておきましょう。ライセンス確認やシミュレータの初回起動許可など、一部のXcodeダイアログはGUIセッションでしか表示されません。SSHの設定が崩れたときのために、入る手段を確保しておく価値があります。
ステップ2 — Xcodeツールチェーンを導入する
App Storeは使いません。xcodesは特定のXcodeバージョンを非対話的にインストールできるため、ヘッドレスノードにぴったりです。まずHomebrewを入れ、続けてツールチェーンを導入します。
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install xcodesorg/made/xcodes aria2
xcodes install 16.2 --experimental-unxip
sudo xcodes select 16.2
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
実際にビルドするデスティネーションを確認しましょう。シミュレータテストを実行するパイプラインなら、初回ジョブ実行時ではなく今ランタイムをダウンロードしておきます。
xcodebuild -downloadPlatform iOS
xcrun simctl list runtimes
xcodebuild -version
モバイルパイプラインで定番のツールも追加しておきます:レーンと署名用のfastlane、バイナリアセットを含むリポジトリならgit-lfs。
brew install fastlane git-lfs jq
git lfs install --system
ステップ3 — マシンをランナーとして登録する
GitHub Actions、GitLab、Buildkiteのいずれも同じパターンです:エージェントをダウンロードし、スコープ付きトークンを渡し、サービスとして実行します。ここではApple SiliconのためのGitHub Actions版を示します。
mkdir ~/actions-runner && cd ~/actions-runner
curl -o runner.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gz
tar xzf runner.tar.gz
./config.sh --url https://github.com/your-org/your-app \
--token <REGISTRATION_TOKEN> \
--labels macos,arm64,m4 --unattended
./svc.sh install && ./svc.sh start
svc.shでのインストールはランナーをlaunchdに組み込むため、ログイン中のデスクトップセッションがなくても再起動を生き延びます。ワークフローからはラベルで指定します。
jobs:
build:
runs-on: [self-hosted, macos, m4]
steps:
- uses: actions/checkout@v4
- run: xcodebuild -scheme App -destination \
'platform=iOS Simulator,name=iPhone 16' test
登録トークンは1時間で失効し、単一目的専用です——そこは問題ありません。ランナーの長期的な認証情報はランナーディレクトリ内の.credentialsに保存されるため、そのユーザーアカウントは何も持たない状態を保ちましょう:個人のSSH鍵もブラウザセッションも置かないことです。
ステップ4 — CIがロック解除できるキーチェーンを用意する
署名は、多くのセルフホストMacセットアップが詰まる場所です。解決策は、レーンが作成・解除・破棄する専用のキーチェーンです。これによりログインキーチェーンがプロンプトを出すこともなく、ジョブ間で漏れることもありません。
KEYCHAIN=ci.keychain-db
security create-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN
security set-keychain-settings -lut 3600 $KEYCHAIN
security unlock-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN
security import dist.p12 -k $KEYCHAIN -P "$P12_PASS" \
-T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASS" $KEYCHAIN
security list-keychains -d user -s $KEYCHAIN login.keychain-db
set-key-partition-listの行は誰もが忘れがちな部分です——これがないと、codesignは誰も目にすることのないGUIパスワードプロンプトを待って止まってしまいます。fastlaneを使うなら、matchとプライベートな証明書リポジトリでこのブロック全体を自動化できます。
キャッシュ:ディスク上に成果を残す
専有マシンがエフェメラルなランナーより優れているのは、状態が持続する点にあります。ジョブごとに世界を再ダウンロードするのではなく、それを活用しましょう。
- DerivedData —
-derivedDataPath ~/ci-cache/ddで安定したパスに固定すれば、インクリメンタルビルドは分単位から秒単位に短縮されることが多いです。 - Swiftパッケージ —
-clonedSourcePackagesDirPath ~/ci-cache/spmを設定すれば依存関係の解決がジョブ間でチェックアウトを再利用します。 - CocoaPodsとHomebrew — どちらもデフォルトでランナーユーザーのホーム下にキャッシュされます。ビルド間でワークスペースを消さないようにするだけです。
ディスクは現実的に見積もりましょう。256GBのSSDでは、Xcodeとランタイムでおおよそ40GBを見込み、稼働中のアプリではキャッシュが60〜80GBに膨らむことを想定してください。危機的な状況になる前に、スケジュールで整理します。
find ~/ci-cache/dd -maxdepth 1 -mtime +14 -exec rm -rf {} +
xcrun simctl delete unavailable
df -h /
プロジェクトが大容量アセットを含む場合や複数のXcodeバージョンを併存させている場合は、月額$11.7の追加SSD +1TBのほうが、リリース直前にディスク満杯のデバッグをするよりずっと安く済みます。
並列実行数:M4 1台あたり何ジョブまで動かせるか
16GBのM4 1台は重いジョブ1つを問題なく処理します:完全なクリーンビルドとシミュレータテストスイートは、すべてのコアを気持ちよく使い切ります。シミュレータベースのジョブを2つ並列で回すのは小規模アプリでは動作しますが、メモリ圧迫が始まると両方が劣化します。
数か月間パイプライン運用を続けて得た経験則は次のとおりです。
- ランナープロセス1つにつきジョブ1つ、というのがビルド+テストの作業における安定したデフォルトです。
- lint、単体テスト、UIテストをそれぞれ別ジョブに分けるのは、2台目のマシンを追加するときだけにしましょう。1台に詰め込むと余計なオーバーヘッドで直列化するだけです。
- 夜間ジョブは空き時間として活用できます:依存関係の監査やスクリーンショット取得はリージョンの時間帯で業務時間外に回しましょう。
キュー待ち時間がボトルネックになったら、レンタルノードを1台追加すると線形にスケールします——同じラベルで登録すればスケジューラが無料で負荷分散してくれます。日額と月額どちらが得かという計算は別の話題として、レンタル期間のコスト分析で数字を出していますし、周辺のワークフローは活用例の概要にまとめています。
セットアップ全体を圧縮すると
- 鍵認証のみのSSH、スリープ無効、VNCはフォールバックとして保持。
xcodes経由のXcode、ライセンス承諾済み、ランタイムは事前ダウンロード。- ランナーは意味のあるラベル付きの
launchdサービスとして登録。 - パーティションリストを設定した専用CIキーチェーン。
- スケジュール整理付きの永続キャッシュ。
手を動かす時間は合計で1時間未満、そして結果としてチームが意識しなくなるビルドノードが手に入ります——それがこの取り組みの本質です。
実機で試してみる
上記のコマンドはすべて、当社がレンタルしているまさにそのマシンで書き、テストしたものです。1台プロビジョニングして手順をなぞり、合わなければ解約してください——どちらの結果でもハードウェアの請求は発生しません。
専有ハードウェア、USD課金、いつでも解約可能。