- 1 台の Mac にある DerivedData キャッシュは、2 台目の Runner にはデフォルトでは存在しない——多ノード iOS CI の隠れコストは、各マシンが独自にコールドスタートすること
- 共有できるのはビルド成果物レイヤー(DerivedData、SPM 解決結果、Pods/)であり、「複数台が同じディレクトリに同時書き込み」ではない。pull → build → push かリージョン別キャッシュハブが 2026 年の主流
- Cloud Mac fleet では lockfile ハッシュ + Xcode バージョンをキャッシュキーに、rsync/オブジェクトストレージと組み合わせる——warm ビルドでさらに 5〜15 分短縮できることが多い(単機キャッシュ最適化の上に)
GitHub Actions の actions/cache を設定し、warm ビルドが 18 分から 9 分に下がった——その後、セルフホスト M4 Runner を 3 台に増やしたら P95 が再び 16 分へ。どのマシンも「この commit を初めて見る」状態だ。
これは原点回帰ではなく、多ノードビルドの第二段階:Xcode キャッシュ共有だ。本稿は「なぜキャッシュか」(CocoaPods / SPM / DerivedData 単機ガイド参照)ではなく、複数 Mac 間でビルドデータを同期する方法を扱う——どの Runner でも、チームメイトがコンパイル済みのモジュールを再利用できるようにする。
1. 多ノード CI が単機より遅い理由
ロードバランサがジョブを Runner A/B/C にランダム割り当てすると、各マシンのディスク状態は独立している。Runner A がコンパイルした MyAppKit.swiftmodule は Runner B には無意味——成果物を運ばない限り。
GitHub ホスト runner は actions/cache で tarball をクラウドに保存し、ジョブ開始時に restore する。セルフホストや Cloud Mac fleet には同等のマネージド層がないことが多く、チームは次のいずれかを選ぶ:
- マシンごとの warming を受け入れる(計算リソースの浪費)
- キャッシュディレクトリを NFS にマウントし、複数台が同時書き込み(破損リスク)
- キャッシュハブを構築:ビルド前に pull、成功後に push
3 番目が iOS チームで最も一般的で、多リージョンノード展開にも合う——米東の M4 3 台が 1 つのキャッシュバケットを共有し、APAC の 2 台が別バケットを共有し、リージョン間では SPM/Pods 層だけ同期する。
2. Xcode ビルドデータのレイヤー
同期する前に、「転送する価値があるもの」と「混ぜてはいけないもの」を分ける。
2.1 DerivedData
xcodebuild -derivedDataPath が指すディレクトリ。コンパイル済み .o、.swiftmodule、リンク中間成果物、部分インデックスを含む。容量は数 GB に達することもあり、warm ビルド高速化の最大レバーであり、Xcode バージョンに最も敏感な層だ。
CI ではパスを固定する。例:/Volumes/CI/DerivedData/<scheme>。同期スクリプトの REMOTE 変数と一致させる——単機キャッシュ記事で強調した通り、パス不一致はキャッシュなしと同義だ。
2.2 SPM と CocoaPods
SPM:~/Library/Caches/org.swift.swiftpm + リポジトリ内 .build(解決結果をコミットしている場合)。CocoaPods:Pods/ と ~/Library/Caches/CocoaPods。
これらの層は DerivedData より変更頻度が低い(lockfile に追従)ため、マシン間、場合によってはリージョン間同期に向く。多くのチームは SPM/Pods だけ S3 に push し、DerivedData はリージョン内 rsync に留める。
2.3 ModuleCache と Xcode グローバルキャッシュ
~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex と各種 SDKStatCaches。通常は DerivedData と一緒に扱う。別途同期する場合は 同一 Xcode マイナーバージョンを強制する。
3. 四つの多ノード同期アーキテクチャ
| パターン | 方式 | 利点 | リスク |
|---|---|---|---|
| ローカルのみ | 各 Runner が独立ディスク、共有なし | 運用ゼロ | 水平スケールしても時間は節約されない |
| NFS 共有マウント | DerivedData をネットワークボリュームに、読み取り専用または読み書き | 設定が簡単 | 並行書き込みで破損しやすい;ネットワーク遅延がリンクを遅くする |
| rsync ハブ | 中央ディレクトリまたは leader マシン;ビルド前 pull、成功後 push | 制御可能、Xcode 互換性が良い | スクリプトとロックが必要 |
| オブジェクトストレージ tarball | キャッシュキーで S3/R2 にパックしてアップロード;ジョブ開始時にダウンロードして展開 | クロスリージョン、GHA cache モデルと一致 | 大きな DerivedData のアップロードは遅い;圧縮が必要 |
2026 年の実践では、同一データセンター / 同一リージョンの Cloud Mac fleet は rsync ハブを優先し、グローバルチームはオブジェクトストレージで SPM/Pods を同期し、DerivedData はリージョン内に留める。NFS を「複数ライター共有 DerivedData」の銀の弾と見なさないこと。
2 台の Runner が同時に同一 DerivedData ツリーに対して xcodebuild を実行してはならない。ボリューム共有が必要なら ファイルロック か 単一ライター複数リーダー(指定 warmer がビルド後に rsync で配布)を使う。
4. キャッシュキーと無効化
多ノード共有キャッシュキーは単機より保守的に——誤ヒットはコールドスタートより悪い(謎のリンクエラー、署名失敗)。
キーに含めるべきフィールド:
xcodebuild -versionまたはXCODE_VERSION環境変数hashFiles('**/Podfile.lock')またはPackage.resolved- Scheme 名またはターゲット集合(多 scheme リポジトリはキャッシュを分離)
arm64(Apple Silicon 専用プレフィックスでレガシー x86 汚染を回避)
完全な github.sha を唯一のキーにしない——そうすると常に miss になる。階層的 restore-keys を使う:dd-arm64-main-<lockhash> → dd-arm64-main- → dd-arm64-。
無効化のタイミング:Xcode アップグレード、lockfile 変更、SWIFT_VERSION や主要 Build Settings の変更、手動 clean build 後——キー接尾辞を bump するかリモートディレクトリを削除する。
5. 実践:rsync pull/push スクリプト
以下は M4 Cloud Mac fleet で検証済みの最小スクリプト。キャッシュハブは cache-leader.internal:/cache/ios/(fleet 内の大容量ディスクマシンまたは NAS)。
#!/usr/bin/env bash
# cache-sync.sh — 在 xcodebuild 前后调用
set -euo pipefail
ACTION="${1:?pull|push}"
CACHE_ROOT="${CACHE_ROOT:-cache-leader.internal:/cache/ios}"
SCHEME="${SCHEME:-MyApp}"
KEY="${CACHE_KEY:-arm64-$(md5 -q Podfile.lock 2>/dev/null || echo nolock)-$(xcodebuild -version | head -1 | tr ' ' '-')}"
LOCAL_DD="${DERIVED_DATA_PATH:-/Volumes/CI/DerivedData/${SCHEME}}"
REMOTE="${CACHE_ROOT}/${KEY}/${SCHEME}/DerivedData"
LOCAL_SPM="${HOME}/Library/Caches/org.swift.swiftpm"
REMOTE_SPM="${CACHE_ROOT}/${KEY}/swiftpm"
rsync_opts=(-az --delete-delay --contimeout=10)
case "$ACTION" in
pull)
rsync "${rsync_opts[@]}" "${REMOTE}/" "${LOCAL_DD}/" || true
rsync "${rsync_opts[@]}" "${REMOTE_SPM}/" "${LOCAL_SPM}/" || true
;;
push)
# 仅成功构建后调用
rsync "${rsync_opts[@]}" "${LOCAL_DD}/" "${REMOTE}/"
rsync "${rsync_opts[@]}" "${LOCAL_SPM}/" "${REMOTE_SPM}/"
;;
*) echo "usage: $0 pull|push" >&2; exit 1 ;;
esac
CI フロー:
- ジョブ開始 →
cache-sync.sh pull xcodebuild -scheme "$SCHEME" -derivedDataPath "$LOCAL_DD" build- ビルド成功時のみ →
cache-sync.sh push
複数マシン並行時、push は競合しうる——flock を使うか last-write-wins を受け入れる。同一 lockfile + 同一 Xcode 下の DerivedData は互換のはず。push が非常に頻繁なら 非同期アップロード(ビルド後にバックグラウンド rsync、パイプラインをブロックしない)に切り替える。
6. クロスリージョン Cloud Mac キャッシュハブ
Runner が 米東、米西、APAC に分散しているとき、全量 DerivedData の越境 rsync は往々にして割に合わない(遅延 + egress)。推奨アプローチ:
- リージョンごとに 1 つのキャッシュバケット(S3 / R2 / プロバイダーオブジェクトストレージ)
- 同期内容:
Podfile.lockハッシュキーのPods/tarball +Package.resolvedキーの SPM キャッシュ - DerivedData はリージョン内のみ rsync;最初のジョブはコールドスタート、以降は warm
- イメージバージョンを ゴールデンイメージタグ と揃え、Xcode ドリフトでライブラリ全体が無効化されるのを防ぐ
アップロード前に SPM キャッシュを tar czf で圧縮——40%〜60% 小さくなることが多い。DerivedData は既に大量の小ファイルなので、裸のクロスリージョン rsync より tar + 並列アップロードの方が安定する。
7. GitHub Actions / セルフホスト Runner への配線
セルフホスト runner では workflow を次のように包む:
- name: Restore shared Xcode cache
run: ./scripts/cache-sync.sh pull
env:
CACHE_ROOT: ${{ secrets.IOS_CACHE_SSH }}
SCHEME: MyApp
DERIVED_DATA_PATH: /Volumes/CI/DerivedData/MyApp
- name: Build
run: xcodebuild -scheme MyApp -derivedDataPath /Volumes/CI/DerivedData/MyApp build
- name: Save shared Xcode cache
if: success()
run: ./scripts/cache-sync.sh push
GitHub ホスト actions/cache を併用する場合、セルフホスト fleet は二重トラックが可能:GHA cache をフォールバック、rsync ハブを同一リージョン低遅延層に。2 つのキー名前空間は分離し、相互上書きを避ける。
モニタリング:pull/push 所要時間、転送バイト数、ビルドがインクリメンタルか(xcodebuild ログの CompileSwift 行数)を記録。P95 が遅くなったらまずキャッシュ miss、次にマシン負荷を確認——ビルド時間の不安定さのトラブルシュートと同じ手順だ。
8. 落とし穴チェックリスト
- DerivedData 並行書き込み:ランダムな
stat cache file corrupted→ pull/push に切り替え - Xcode マイナーバージョン不一致:モジュール ABI 不一致 → キーにバージョンを含め、イメージは統一タグ
- パス不一致:キャッシュはパス A、コンパイルはパス B → フルリビルド
- clean 後の push:空ディレクトリが良いキャッシュを上書き → 成功時のみ push、clean 後は push しない
- 署名済み成果物をキャッシュに含める:Provisioning 変更後に古い署名 →
Build/Productsをキャッシュから除外するか設定別にキー分割 - ディスク満杯:DerivedData 膨張 → リモート
${KEY}ディレクトリを定期的に LRU 刈り込み、直近 N 個の lockhash を保持
9. FAQ
複数 Mac が同じ DerivedData ディレクトリを直接共有できるか?
並行書き込みでは不可。読み取り専用マウントや単一ライター/複数リーダーは可能だが、リンク段階でロックに当たることがある。本番ではローカル DerivedData + rsync を優先する。
Xcode キャッシュ共有には同一 Xcode バージョンが必要か?
はい。Xcode アップグレード後は新しいキャッシュキーを使い、古いディレクトリは非同期で削除できる。
Bazel / Tuist リモートキャッシュとの比較は?
Bazel/Tuist リモートキャッシュは粒度が細かい(アクションレベル)で巨大 monorepo に向く。標準 Xcode プロジェクトは DerivedData 同期の移行コストが低く、既存 xcodebuild フローと互換性がある。
複数 Cloud Mac にどれくらいのディスクが必要か?
各マシンに DerivedData + SPM 用に少なくとも 50〜100GB をローカル確保。ハブまたはオブジェクトストレージは「並行 scheme 数 × scheme あたりキャッシュサイズ × 保持バージョン数」で見積もる。M4 + 2TB 拡張は中規模チームには通常十分。
結論
Xcode キャッシュ共有が解くのは多 Runner 時代の「二度目のコールドスタート」——単機キャッシュ最適化の後、水平スケールしても全額のコンパイル税を再び払うべきではない。三層の分離(Pods / SPM / DerivedData)、一つの同期規律(pull-build-push)、一つの鉄則(DerivedData 並行書き込み禁止)を覚えておく。
キャッシュは分散ビルドで最も安価な計算倍率——M4 を 2 台追加するより ROI が早い。
1 台の cache leader と cache-sync.sh から始める。リモートディレクトリは lockfile ハッシュで命名。ビルドがグリーンのときだけ push。来週の P95 曲線が価値を教えてくれる。
多ノード CI 向けディスク余裕のある Cloud Mac
Vuncloud Mac mini M4:米東・米西・APAC で SSH 即利用——大容量 DerivedData、rsync キャッシュハブ、セルフホスト Runner fleet に最適。
関連記事
- CocoaPods / SPM / DerivedData:単機キャッシュガイド
- ゴールデンイメージと Cloud Mac 自動化
- 越境 iOS チームの統一ビルド環境
- iOS CI/CD が Mac mini M4 で動く理由
Apple ツールチェーンの挙動は公式リリースに従う。SSH とオブジェクトストレージの認証情報はチームのセキュリティポリシーに沿って監査すること。最終更新:2026 年 7 月 27 日。