Вывод сразу: при ~500 iOS CI-сборках в сутки большинству команд не нужны восемь Mac mini — хватит трёх Cloud Mac на Apple Silicon, если разделить job’ы в GitHub Actions по уровням и изолировать быструю и release-очереди через Self-hosted Mac Runner, а не гонять три полных Archive параллельно на каждой машине.
Три месяца назад мы переняли Xcode CI у iOS-команды. Исходные цифры:
- 18 разработчиков
- 2 приложения в общем monorepo
- GitHub Actions — единственная CI
- Около 500 macOS job’ов в день (PR, unit-тесты, ночные релизы)
В закупке числилось: восемь Mac mini в серверной под self-hosted runner. Две недели логов workflow по типам job показали разрыв с интуицией: ~70 % — PR и интеграционные сборки, 20 % — Simulator unit-тесты, ~10 % — archive + upload (upload в TestFlight можно учитывать отдельно, см. рис. 1).
Рис. 1 · Состав ~500 iOS CI-сборок в сутки (две недели GitHub Actions, замеры команды)
Расчёт ёмкости: с 91 до 63 машино-часов
500 job’ов/сутки за 24 часа — в среднем ~21 job/ч, но решают пики: утренние merge в EU/US, шторм PR перед релизом. Пики часто в 3–5 раз выше среднего. Ёмкость считайте под очередь в пик, а не под полночное среднее.
Вторая переменная — длительность job: Simulator-тесты 4–8 мин; PR-сборка с deps 12–20 мин; release archive + нотаризация + upload 25–45 мин. P50 за две недели: лёгкие ~6 мин, тяжёлые ~35 мин. Если ошибочно заложить 30 % archive, получится «нужно восемь Mac» — после разбивки тяжёлых job на 500 runs их заметно меньше.
Без кеша грубая оценка 325 лёгких + 175 тяжёлых ≈ 91 машино-ч/сутки — больше физического потолка трёх Mac (72 ч). С кешем DerivedData/SPM, сериализацией L2 и метками очередей лёгкие ~4 мин, тяжёлые ~22 мин на warm path: потребность ≈ 63 машино-ч. При коэффициенте пика ~1,3 это приемлемо — математика «хватит трёх».
Рис. 3 · Суточная потребность в машино-часах macOS (модель 500 job/сутки)
Слои Xcode CI: не смешивайте archive и PR в одной runner-очереди
Пайплайны на 500 runs/сутки почти всегда делят Xcode CI на три уровня:
- L0: SwiftLint, сборка модуля — метка
macos-fast. - L1: PR-сборка без archive — Fast Mac Runner, до 2 лёгких job параллельно на узел.
- L2: archive, нотаризация, TestFlight — метка
macos-archive, не более 1 job одновременно на машину.
GitLab CI, Buildkite, Jenkins — та же логика меток, не общая «свалка». Burst с Buildkite: Mac Agent Buildkite на Cloud Mac. Mac VPS vs пул runner: гид Mac VPS / macOS в облаке.
Три Cloud Mac: роли и топология
Развёрнутая статическая топология из трёх узлов (имена гибкие, роли лучше сохранить):
| Узел | Метка runner | Роль | Параллелизм |
|---|---|---|---|
| Mac-A | macos-fast |
PR Build, Unit Test | 2 × L0/L1 |
| Mac-B | macos-fast |
Симметричен Mac-A, fast-очередь | 2 × L0/L1 |
| Mac-C | macos-archive |
Archive, нотаризация, Upload | 1 × L2 |
Рис. 2 · Топология трёх узлов (GitHub Actions Self-hosted Runner)
Два fast-узла дают пропускную способность, один release — предсказуемую поставку. Если L2-очередь в день релиза выше SLO, Mac-A временно может получить macos-archive — но отключите параллель L0, иначе Keychain и блокировки DerivedData дают случайные сбои подписи. Minor Xcode на всех трёх должен совпадать с заметками к релизам Xcode.
Почему GitHub Actions Mac Runner быстро встают в очередь
Многие начинают с macOS runner от GitHub и видят очередь при потоке PR. Редко «GitHub медленный», чаще: (1) org-wide лимит concurrency macOS; (2) workflow с полным archive на каждый PR; (3) нет разделения по типу job на Self-hosted Mac Runner — быстрые job за медленными. До перехода на три выделенных Cloud Mac queue_wait P95 превышал 40 мин; с dual fast/archive — L1 P95 < 8 мин.
Типичная схема: три Cloud Mac как baseline self-hosted, hosted runner только на open-source-недели или экстремальные пики L0 — затраты под контролем, секреты без еженедельной миграции.
Self-hosted Runner vs Xcode Cloud
Xcode Cloud — для команд в экосистеме Apple с нулевым ops agent. Self-hosted Mac Runner — когда нужны свои очереди, ключи кеша и смешение с внутренним Jenkins/Buildkite. Сравнение:
| Измерение | Xcode Cloud | Cloud Mac + Self-hosted Runner |
|---|---|---|
| Биллинг | Пакет минут + cap concurrency | Подписка/день Cloud Mac, предсказуемые машино-часы |
| Очередь | Общая платформа | Свои метки fast/archive |
| Секреты | Удобная интеграция ASC | Match / Keychain — свой runbook |
| Кому подходит | Редкие релизы, мало кастома | 500+ runs/сутки iOS CI/CD |
Когда переключаться после исчерпания пакета минут: FAQ лимиты Xcode Cloud vs Cloud Mac для archive.
Почему команда отказалась от Xcode Cloud
Xcode Cloud оценивали; выбрали GitHub Actions + Self-hosted Runner, потому что: (1) backend и Android уже на GitHub — вторая CI не нужна; (2) нужны свои ключи кеша и matrix monorepo; (3) в релизные недели объём job выходит за «комфорт» пакета минут, очередь не управляется. Xcode Cloud не плох — он плохо совпадает с «500 runs/сутки, очередь под контролем».
Bitrise vs Self-hosted Mac Runner — интуиция по стоимости
Bitrise и аналоги экономят ops agent, но тарифицируют concurrency. Для 18 человек, двух app, ~500 job/сутки годовая сумма часто выше подписки на три Cloud Mac, а параллель archive всё равно упирается в tier. Bitrise — стартапам без желания трогать runner; две недели на Self-hosted Mac Runner — окупаемость узлов за 3–6 месяцев. Уже на Bitrise? переносите L2 на release-узел поэтапно.
Mac Agent Buildkite: плюсы и минусы
Buildkite держит очередь в облаке, agent на вашем железе — хороший burst, понятные artifact. Минус — лишний слой оркестрации; для трёх Mac иногда «из пушки по воробьям». У клиента PoC Buildkite отличный по burst, но остались на native GitHub Actions — YAML знают только там. С существующим Buildkite — та же стратегия меток fast/archive, см. статью Buildkite + Cloud Mac.
Cloud Mac vs локальный Mac mini CI
Восемь Mac mini в своей стойке: CapEx, амортизация, отключение питания, аудит ЦОД. Три Cloud Mac: предсказуемый OpEx, PoC на сутки, регион ближе к Git. Локальный CI оправдан при >14 ч compile/сутки на машину годами без изменений. Cloud Mac CI — для плавающих пиков, недель подрядчиков, сезона review и нотаризации. Команда оставила два офисных Mac под разработку; тяжёлый Xcode CI в облаке — вместо восьми mini простаивающими большую часть суток.
SLO очереди: четвёртая машина только по данным
Метрики: queue_wait_seconds (P95), run_duration по L0/L1/L2, cache_hit_ratio, l2_concurrent (редко > 3). Пример порогов: L1 P95 < 8 мин; L2 P95 < 25 мин. Четвёртый release-узел — если L2 три дня подряд выше порога и cache hit > 60 %.
Кеш: рычаг для 63 машино-часов
DerivedData: ключ branch + версия Xcode; смена lock SPM/CocoaPods bust. Fast-узлы делят read-only кеш, release хранит L2 DerivedData локально на NVMe. Материалы подписи через vault — не в bundle кеша. Ключи с minor macOS/Xcode, иначе «hit, но link fail» после апгрейда.
Минимальная настройка GitHub Actions Self-hosted Runner
Три регистрации: macos-fast ×2, macos-archive ×1. Для L2 нужен concurrency, чтобы release job не отменяли друг друга:
concurrency:
group: ios-archive-${{ github.ref }}
cancel-in-progress: false
jobs:
archive:
runs-on: [self-hosted, macos-archive]
steps:
- uses: actions/checkout@v4
- run: xcodebuild archive -scheme App -archivePath build/App.xcarchive
Release-узел: выделенный пользователь macOS + Match; после reboot — unlock-скрипт перед ночной очередью. См. документацию xcodebuild и Fastlane.
День релиза: 500 runs становятся 650
Порядок: (1) остановить некритичный L0; (2) hosted runner только для L1; (3) четвёртый Cloud Mac на 48 ч посуточно. Держать L2 на 2 parallel/машину постоянно — случайные сбои нотаризации.
Когда четвёртая машина обязательна
- L2 queue P95 > 40 мин неделю, кеш уже оптимизирован.
- Monorepo >5 app на одном release, ночное окно не хватает.
- Полный archive на каждый PR — сначала pipeline, не железо.
Анти-паттерны
Полный archive на PR; runner без меток; два archive на M4 16 ГБ; ключи кеша без ветки; смотреть только success rate без queue_wait — всё заставляет три машины казаться «мало» и толкает к заказу восьми mini.
FAQ: короткие ответы для поиска
Сколько Mac на 500 iOS-сборок в сутки?
При слоях job (PR / тесты / archive) и кеше DerivedData большинству команд хватит трёх Cloud Mac Apple Silicon: 2 Fast Mac Runner + 1 Release Mac Runner. Если >50 % runs — полный archive, сначала pipeline или четвёртый release-узел.
Сколько job параллельно на GitHub Actions Mac Runner?
На M4 16 ГБ: 2 лёгких job (PR, unit test) или 1 archive job. Не держите «2× archive» постоянно.
Почему archive нельзя сильно параллелить?
Давление на память → swap, очередь диска, блокировки Keychain и конфликты codesign — sporadic timeout, не воспроизводимые ошибки компиляции.
Cloud Mac дешевле Xcode Cloud?
При частом iOS CI/CD, постоянном Self-hosted Runner и resident secrets три Cloud Mac обычно предсказуемее поминутной оплаты. Редкие релизы, нулевой ops — сначала Xcode Cloud.
Bitrise или три Cloud Mac?
Bitrise экономит ops, быстрый старт. ~500 job/сутки с контролем очереди и кеша чаще выигрывает Self-hosted Mac Runner + Cloud Mac — ниже годовая сумма, L2 concurrency по вашим правилам.
VPSSpark: baseline Cloud Mac 2 Fast + 1 Release
Считаете «500 runs/сутки — сколько Mac?»: две недели логов как на рис. 1, затем проверьте, держится ли модель 63 машино-ч. VPSSpark Cloud Mac mini M4 / M4 Pro — PoC на сутки или подписка — для GitHub Actions Self-hosted Mac Runner, PR и archive в разных очередях.
См. тарифы Mac в облаке или главную VPSSpark — выберите регион, прогоните реальный workflow по bucket’ам, затем решите, хватит ли трёх — вместо предзаказа восьми mini.