Vuncloud ブログ
← フィールドノートに戻る

Xcode キャッシュ共有の実践:複数ノードでビルドデータを同期するコツ

DerivedData · SPM · CocoaPods · マルチ Runner 同期約 13 分

データ分析ダッシュボード——マルチノード Xcode ビルドキャッシュの監視と同期効率を象徴
TL;DR · 三行で
  • 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 でも、チームメイトがコンパイル済みのモジュールを再利用できるようにする。

3
キャッシュ層:依存関係 · ビルド成果物 · インデックス
5〜15 分
多ノード warm ビルドで典型的に節約できる時間
1
鉄則:同一 DerivedData への並行書き込み禁止

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 マイナーバージョンを強制する。

MacBook 上のコードエディタ——複数 Runner が同じ Xcode コンパイルキャッシュを共有するイメージ
共有するのは「コンパイル済みモジュール」であり「全マシンで開いたプロジェクト」ではない——各 Runner はローカル DerivedData コピーでビルドする

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 フロー:

  1. ジョブ開始 → cache-sync.sh pull
  2. xcodebuild -scheme "$SCHEME" -derivedDataPath "$LOCAL_DD" build
  3. ビルド成功時のみ → 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 に最適。

Cloud Mac プランを見る · M4 CI Runner コストモデル

Apple ツールチェーンの挙動は公式リリースに従う。SSH とオブジェクトストレージの認証情報はチームのセキュリティポリシーに沿って監査すること。最終更新:2026 年 7 月 27 日。

フィールドノート · ビルド高速化

DerivedData 同期 · マルチ Runner · Cloud Mac

rsync ハブ · cache key · リージョンキャッシュバケット

Cloud Mac プランを見る
期間限定 プランを見る