VPSSpark ブログ
← 開発日記に戻る

iOS チームが 20 人になると、CI/CD はどこから崩れるか

サーバー手記 · 2026.06.04 · 約 16 分

iOS CI ビルドキューとスケーリング障害を検討する開発チーム
中規模 iOS チームの CI レビュー:queue time とコンパイル時間

本稿は iOS CI/CD の Scaling Failure Model(拡員時の崩壊モデル)であり、容量計算機ではありません。引き継ぎ前、チームは 14 か月で 8 名から 20 名へ拡大した一方、Runner トポロジはほぼ据え置きのままでした。結果として、リリース週は PR が全面赤、ビルドが長時間 Queued のまま、TestFlight が 2 日遅れました。以下では「人数が増えると CI が遅くなる理由」と、ボトルネックが pipeline のどこに乗るかを整理します。

「1 日 500 回のビルドに Mac は何台必要か」が知りたい場合は、姉妹記事 3 台の Cloud Mac で 1 日 500 回の iOS ビルドを支える を参照してください。あちらは引き継ぎ後の実装論です。本稿は引き継ぎ前に何が崩れたかに焦点を当てます。

結論から:20 名規模で CI が崩れる原因を「Mac が足りない」一言で片づけることはほぼありません。本事例では五つのボトルネックが順に重なった結果です。並列上限到達 → Runner と開発環境の奪い合い → PR で Archive 実行 → マトリクス Job の急増 → 署名の競合。12~16 名の段階で兆候は出ていました。20 名になってもキューを分離しなければ Failure Zone に入ります。

俯瞰:CI が「人数増」から Failure Zone へ滑り落ちる仕組み

図 1 は本稿全体のメカニズム俯瞰図です。pipeline の細部より、postmortem の冒頭やチーム onboarding 向きです。崩壊は単一点ではなく、下記チェーン上の重なり合いとして起きます。

図 1 · CI 崩壊メカニズム俯瞰(Scaling Failure Cascade)

PR マージ頻度の上昇team scaling · 非線形の起点
Matrix / Job 数の爆発monorepo · path filter 不備
Queue 滞留queue_wait_seconds ↑
Runner プール飽和hosted 並列 · mini 占有
Archive 過負荷L2 が PR キューへ流入
Signing / Keychain 競合codesign contention
Failure Zonequeue P95 > 30min · Archive > 30%

当該顧客では 12 名で Queue が詰まり、16 名で Runner/Archive の分岐が同時に点灯し、20 名で底部の赤域に到達しました。healthy baseline との対照は後述します。

iOS CI/CD アーキテクチャ図:PR から TestFlight までのボトルネック地図

図 2 は macOS Job 1 回分の実行経路(L0/L1/L2)で、本事例で最初に打ち抜かれた 2 箇所を示します。「コンパイルが遅い」のか「Job がそもそも走っていない」のかを切り分けるのに使えます。

図 2 · iOS CI/CD Pipeline:L0 / L1 / L2 層と典型ボトルネック(当該顧客 2024–2025)

PR Pushpath filter · matrix
Queuequeue_wait_seconds
Runner Poolhosted + self-hosted
L0/L1 Build統合 · 単体テスト
L2 Archivexcodebuild archive
SigningKeychain · Match
UploadTestFlight
↑ Bottleneck · Queue(組織 macOS 並列の上限到達) ↑ Contention · Runner Pool + Signing(オフィス Mac と Keychain の奪い合い)

L0:lint / 軽量チェック。L1:PR 統合ビルドと Simulator テスト。L2:Archive、公証、アップロード。当該顧客は L1 コンパイルではなく Queue と Runner 混在プールで崩れました。マトリクス膨張はしばしば「Xcode が遅くなった」と誤読されます。

起点:8 名のとき「足りていた」CI が、なぜ伸びなかったか

2024 年中期の構成は典型的でした。

CTO の言葉は「CI はボトルネックではない。ここに時間をかけるな」でした。8 名なら成立します。人員が倍になっても CI は見えない税となり、容量計画はなく feature team だけが workflow を積み上げていました。

Healthy baseline:何を「正常」と対照するか

20 名の崩壊週だけを見ると、「チームが大きくなれば Mac を大量購入しなければならない」と誤判断しがちです。ここでは健康基線 vs 崩壊週の対照表を補います。数値は当該顧客の 8 名帯の実測と、当社が手がけた中規模 iOS チームの経験レンジに基づきます(業界標準ではありませんが、社内 review には十分です)。

指標 Healthy baseline(8~12 名 · トポロジ妥当) 当該顧客 · 20 名崩壊週
L1 queue_wait P95 < 8 分 47 分
L1 run duration P95 12~18 分 14 分(コンパイルは遅くなっていない)
Archive 系 Job の比率 < 15% 38%
1 回の push · macOS Job P99 < 6 19
macOS Job / 日 80~180 500+(リリース週)
Runner トポロジ L1/L2 ラベルでプール分離 hosted + オフィス mini 混在

重要な対照:崩壊週の run duration はほぼ不変で、queue time だけが約 10 倍です。問題はコンパイル能力ではなく、キューと workflow にあります。これが「CI が遅い」という言い方でチームを誤導しやすいポイントです。

タイムライン:3 回の警報、3 回とも「偶発」と見なされた

第 1 回(12 名、iOS +4 名):PR マージ頻度が 1 日平均 6 回から 14 回へ。ホスト型 Runner の build queue が午後に 25~40 分の待ちを記録。チームの反応は「今日は GitHub が不調か」。queue_wait_seconds の曲線は誰も引いていませんでした。

第 2 回(16 名、同一 repo を共有するバックエンド +2 名):オフィスに2 台目の Mac miniを追加。両方とも self-hosted 登録で、ラベルによるキュー分離なし。Archive と PR Build が同一プールを奪い合い、軽 Job が 8 分から 35 分に化け、重 Job の公証がランダム失敗。反応は「もう 1 台 HDD を買ってみよう」。

第 3 回(20 名、2 App の大型バージョンが衝突):リリース週に macOS Job が 1 日平均 120 回から280+へ。Slack #ios-ci が 24 時間体制のメンションに。反応は「Mac mini 8 台の調達が進行中」——これが 1 日 500 回の事例で見た 8 台案です。当時は Job の内訳計算は未実施でした。

図 3 · チーム規模 vs macOS Job/日 vs リスク帯(当該顧客 2024–2025 実測)

チーム人数 macOS Job / 日 GitHub Actions queue time(L1 P95) リスク帯 段階メモ
8 名 ~100 < 5 分 Baseline オフィス mini 1 台で足りる
12 名 ~180 25~40 分 Warning Zone キュー待ちが初めて持続的に超過
16 名 ~260 ~35 分 Structural Debt Archive 混在、署名が偶発失敗
20 名 500+ 47 分 Failure Zone リリース週ピーク、CI 実質崩壊
Job / 日 0 200 400 600 100 180 260 500+ 8名 12名 16名 20名

データソース:GitHub Actions workflow 実行ログ(14 日スライディングウィンドウ)。20 名は 2 App リリース週のピーク。Job 量は人数より速く増え、主因は matrix 膨張と PR Archive であり、単純な「人数増」ではありません。

表の 2 点:16→20 名で Job 量が急増し、Archive 比率は約 18% から 38% へ。queue time と Job 量は非線形——人数は倍になっていないのに、キューは先に倍になります。拡張予算を誤算しやすい箇所です。

閾値モデル:12 名 Warning / 16 名 Structural Debt / 20 名 Failure Zone

本事例を weekly review で使える枠組みに整理しました。精密な式ではありませんが、Runner トポロジを動かすべきかの判断には足ります。

CI 崩壊リスク ∝ (PR 頻度 × Matrix 幅 × Archive 比率) ÷ 有効 Runner プール容量

分子 3 項は GitHub Actions ログから算出可能です。分母の「有効 Runner プール」は登録台数ではなく、ラベルで分流できる並列 Mac スロット数です。オフィス mini が開発に占有されると、分母は瞬時に割り引かれます。以下任意 2 つが同時に成立したら、「もう 1 台 Mac mini」ではなく pipeline 再設計を推奨します。

  • queue_wait_seconds P95 > 30 分 かつ Archive 系 Job 比率 > 30% → L1/L2 分離必須、PR 全量 Archive を停止
  • 1 回の push で起動する macOS Job 数 P99 > 15 → matrix/path filter が制御不能、機械追加より Job 削減を先に
  • L2 が 3 日連続 queue P95 > 40 分 かつキャッシュヒット > 60% → 構造的拡張、専用 release プールを検討

当該顧客の 20 名崩壊週は 3 条件すべて該当——Failure Zone であり、「分数パック追加」では解決しない Warning ではありません。

GitHub Actions queue time:12 名以降、最初に失控する指標

多くのチームは「CI 遅延」を compile 遅延と解釈し、DerivedData を調整します。本顧客のログでは失控したのは queue time——Job が長時間 Queued のままです。GitHub は queue_wait_seconds と run duration を分離統計します。後者だけを見ると、コンパイルを直しつつボトルネックは組織並列と Runner 分池のまま残ります。

組織 macOS 並列は GitHub Actions usage limits を参照。12 名以降の午後 PR ラッシュではRunner が遅いのではなく、空きスロット待ちです。UI の Queued スピナーはネットワークや Xcode 問題と混同されがちです。調査時は Job 実行時間とキュー待ちで wait と run を分けてください。

12 名帯で queue P95 はすでに 25~40 分。当時 fast/archive をラベル分池していれば(2 台 mini の混在ではなく)、後続の署名競合は先送りできた可能性があります。Runner トポロジ選定は エラスティックプール vs 常駐ノードの意思決定マトリクス を参照。

拡員時の 5 ボトルネック(Scaling Taxonomy)

以下 5 類は当社の mobile CI 障害ラベルです。図 2 の pipeline と対応し、postmortem 記述に使えます。

① Capacity bottleneck · 組織 macOS 並列の上限

ホスト型 Runner 並列が打ち抜かれると、軽 Job と Archive が同一キューに並び、速いキューが遅い Job に塞がれます。表現は前節の GitHub Actions queue time 曲線と一致します。分数パック以外に、L2 をホスト型プールから切り出す方が安定します。組織並列は常に最初の天井です。

② Resource contention · オフィス Mac mini が「予備開発機」化

エンジニアが SSH できるマシンに Runner を載せると、いずれ誰かがローカルで xcodebuild debug を実行します。Runner と日常開発が CPU/ディスクを奪い合い、タイムアウトや偶発 compile error になります。16 名時、2 台 mini は「金曜全滅・月曜復活」があり、根因は Remote Desktop でのブランチ変更でした。

③ Workflow design debt · 全 PR でフル Archive

8 名期の「PR 緑 = リリース可能」の省略実装:各 pull request workflow の末尾に Archive + アップロード。人数が少ないうちは 1 日 6 回なら耐えられます。20 名ではArchive 系 Job 比率が 10% から workflow 設計で 35%+へ押し上げられ、遅い Job がすでに混雑したキューへ流入します。正しくは L1 統合ビルドと L2 Archive を分離——引き継ぎ後の改造で初めて実装され、姉妹記事の Job 層別章を参照。

④ Scaling explosion · Monorepo マトリクス Job の指数増

20 名時、1 回の push で最大 22 個の macOS Job——人数は 2.5 倍、Job は 4 倍。プロダクトペース下で最も見落とされやすい崩壊点です。

⑤ Crypto / signing contention · リリース日の Keychain と署名競合

2 台の Self-hosted Mac が同時 Archive なら 16GB でも偶発は耐えます。Match 解除、公証、TestFlight アップロードが重なると、Keychain ロックと codesign 競合で同一エラーコードがランダムに出ます。リリース週は「証明書が見つからない」が 4 連続、ローカル Mac 再実行は成功——典型的な複数 Job による署名環境の奪い合いであり、証明書期限切れではありません。設定は Apple Xcode Signing and Capabilities を参照。

試した「修正」が、なぜ状況を悪化させたか

GitHub 分数パックの追加購入:ホスト型キューだけ緩和、Archive 比率は不変。費用はかかり P95 は 60 分超のまま。

オフィス 3 台目 Mac mini:依然 fast/archive ラベルなし、3 台混在で署名失敗率が上昇。

金曜マージ禁止:PR 頻度を人為抑制、リリース週に集中爆発、ピークがより尖る。

各 Job に timeout-minutes: 180Queued 時間は timeout に含まれず、より多くの Job が長時間スロット占有。

唯一有効だが未完だった試み:Release を別マシンへ——しかし依然オフィス mini、夜間 Keychain 未解除、月曜 L2 キュー滞留。

監視で見るべき、誰も見ていなかった 3 指標

崩壊前の Grafana は「成功率」と「平均時間」のみ。引き継ぎ 1 週目に追加した 3 指標で 80% を特定(queue_wait_seconds の切り口は前節 GitHub 公式モニタリング参照):

  • queue_wait_seconds P95(L0/L1/L2 別バケット)——「コンパイル遅延」と「キュー待ち」の分離
  • Archive 系 Job 比率(週次)——workflow 設計の失控検知
  • 1 回の push で起動する macOS Job 数 P99——matrix の指数膨張

20 名崩壊週:L1 queue_wait P95 47 分、Archive 比率 38%、push トリガー Job P99 19。3 つのうち 2 つ超過なら、調達よりトポロジ変更を先に。

止血の順序:トポロジを直してから、Mac 台数を議論

引き継ぎ後 2 週間は新規購入なし、4 点のみ実施。

  • PR workflow から Archive 削除、L2 は nightly + release ブランチのみ
  • path filter 強化、README/バックエンド変更で iOS matrix を起動しない
  • オフィス mini から Runner 撤去、開発との争用を回避
  • ホスト型は L1 ピークのみ、L2 はすべて専用ノードへ

この 4 点だけで、リリース週 queue_wait P95 は 47 分から22 分へ——まだ不十分ですが、崩壊の主因はトポロジと workflow であり「Mac 台数の神秘公式」ではないことを示しました。その後 Cloud Mac の fast/release 分池と容量検証を実施。エラスティック vs 常駐は GitHub Actions 自ホスト macOS Runner:エラスティックプールと常駐ノードの意思決定マトリクス を参照。

拡大中チーム向け:3 つの早期シグナル

12 名から 20 名へ伸ばしている最中、以下のいずれかが出たら、リリース衝突を待たず当週 30 分の CI 専項レビューを推奨します。

  • 開発者が「CI をスキップして先に merge できないか」と聞き始める
  • オフィス Mac に「再起動禁止、Runner 実行中」の付箋が出る
  • TestFlight ビルドは成功するが「アップロード枠待ち」が常態化

これら 3 シグナルの背後では、通常すでに上記 5 崩壊点のうち 2 つ以上に踏んでいます。

Postmortem Summary(引用可)

Root cause

拡員時にCI トポロジを同期拡張しなかった:PR 頻度と matrix 幅は上昇したが、Runner は「hosted + オフィス mini 混在」のまま、PR workflow は L2 Archive を継続し、queue と signing が二重飽和。

Contributing factors

  • queue_wait_seconds 監視なし、Queued を compile 遅延と誤認
  • 2 台目 Mac mini に fast/archive ラベルなし、Archive と PR が同一プール
  • Monorepo path filter が広すぎ、README 変更で iOS matrix 起動
  • リリース週 Job ピークと調達/トポロジ変更が未連動

Fixes tried(無効または悪化)

  • GitHub 分数パック追加——hosted キューのみ緩和、Archive 比率不変
  • オフィス 3 台目 mini 混在——署名失敗率上昇
  • 金曜マージ禁止——ピークがリリース週へ移動
  • timeout-minutes: 180——Queued は timeout 対象外

What worked(引き継ぎ後 2 週間)

  • PR から Archive 削除;L2 は nightly + release のみ
  • path filter 強化;オフィス mini から Runner 撤去
  • L2 を専用 release ノードへ——queue P95 47min → 22min

FAQ

GitHub Actions がずっと Queued、コンパイル遅延ではない場合は?

まず queue_wait_seconds と run duration を確認。Queued 比率が高ければボトルネックはキューまたは Runner プールです。図 2 の Queue ノードと図 3 の健康基線を対照してください。

iOS チームは何人規模で CI が崩れやすい?

本事例は 12 Warning / 16 Structural Debt / 20 Failure Zone の 3 段。より信頼できるのは指標:queue P95 > 30 分かつ Archive > 30% なら pipeline 再設計を優先。

拡張時、先に機械追加か pipeline 変更か?

Job 層別、PR Archive 停止、matrix 縮小を先に。機械だけ増やすと崩壊は先送りされ、次のリリース週に queue ピークが戻ります。

「1 日 500 回に Mac 何台」と同一記事か?

いいえ。本稿はなぜ崩れたか(Failure Model)、姉妹記事は崩壊後の容量と台数(Sizing)。診断のため本稿を先に、容量は後から読むことを推奨します。

Failure Zone 処方:先に L2 分離、次に Runner プール検証

以下 3 条件を同時に満たすなら Failure Zone(図 1 底部と一致)

  • queue_wait_seconds P95 > 30 分
  • Archive 系 Job 比率 > 30%
  • 1 回の push で macOS Job > 15 個

エンジニアリング優先アクション(調達より先)

1. L2 isolation——PR から Archive 削除;release/nightly は macos-archive ラベル専用。

2. Dedicated Mac pool——L2 をオフィス開発と hosted 混在から切り出し;L1 は hosted またはエラスティック self-hosted でピーク対応可。

3. 検証——分離した release ノードで 1 回リリース workflow を実行し、図 3 健康基線で queue P95 が < 8 分帯に戻るか確認。

台数と fast/release 分池アルゴリズムは 3 台の Cloud Mac で 1 日 500 回の iOS ビルド;Runner エラスティック vs 常駐は GitHub Actions 自ホスト macOS Runner 意思決定マトリクス

日単位 PoC で release 専用ノード 1 台による L2 isolation 検証が必要なら、Mac クラウドホストプラン または VPSSpark トップ で Archive キューを分離した Cloud Mac を利用できます。これはトポロジ検証であり、最初からフルクラスタ購入する手順ではありません。

期間限定

20 人 iOS CI が崩れた? まずキューを分割

build queue · Archive 分離 · Cloud Mac Runner

ホームへ
期間限定 プランを見る