Если вам близок подход «как к VPS» к вычислительной мощности, а в React Native / Expo вы упираетесь в «очередь EAS, нативные модули и форму учётных данных», эта статья сводит управляемую сборку и выделенную плоскость macOS в одной таблице: сначала разбор болевых точек, затем матрица решений и шестишаговый чек-лист, в конце — ритм самостоятельно размещаемой сборки с опорой на аудит. После прочтения будет ясно, когда рационально остаться на EAS, а когда перенести iOS-сборки на арендованный удалённый Mac как отдельный узел.
Expo Application Services (EAS) хорошо упаковывают сертификаты, загрузку и облачные сборки и подходят для стандартного пути. Когда в репозитории много нативных патчей, приватных Pod и нужно вписать логи сборок и audit trail команд в пакет соответствия, непрозрачная очередь и границы образа управляемого пула начинают мешать. Ниже шесть кластеров, обобщённых по реальным сессиям разбора, чтобы оценить, не нужен ли вам второй, «с доступом по SSH» контур сборки.
Семантика очереди неочевидна: в пик воркфлоу может ожидать в очереди десятки минут; если релизное окно жёстко зафиксировано с бизнесом, эта неопределённость перерастает в риск инцидента. У пула нельзя зафиксировать «максимальную конкуренцию» в духе внутреннего SLA, в отличие от выделенного узла.
Длинная нативная цепочка: react-native-config, Firebase, карты и Bluetooth при кастомных Podspec и прекомпиляционных сценариях снижают долю попаданий в кэш образа, время одной сборки сильно плавает; при отладке нет «шелла на хосте» с полным выводом xcodebuild -showBuildSettings.
Расхождение моделей подписи и учётных данных: при одновременном EAS-управляемом подписании и внутреннем match / собственном CA «локально Archive проходит, в облаке изредка падает»; часто виноваты контекст связки ключей и сочетание CODE_SIGN_IDENTITY, не полностью воспроизводимое в чужом образе.
Неконтролируемый уровень диска и DerivedData: в крупном monorepo на общем воркере чаще срабатывают агрессивные чистки, сбивая «тёплый» кэш; командам с частыми UI-тестами и Archive это даёт длинные хвосты по времени.
Рассинхрон с вашей схемой меток в CI: для бэкенда и Web в GitHub Actions уже заданы labels, лимиты конкуренции и self-hosted runner, а iOS живёт в другом оркестраторе — операционная модель и дежурства распадаются на две ветки.
Недостаточно полей для аудита: в финансах, медицине или регулируемом экспорте нередко требуют «кто, с какого IP, на каком билд-хосте какие команды запускал»; если с управляемой стороны нельзя выгрузить сырые SSH/сеансы в формат, согласованный с внутренним SOC2, выделенный узел закрывает цепочку доказательств.
Если срабатывают два и более пункта, разумно читать дальше сравнительные таблицы и шаги; если сборки редкие, а нативный слой тонкий, на первом этапе стоит выжать EAS, не раздувая собственный контур. Подробности «как вешать runner на выделенный узел» вынесены в материал о self-hosted runner в GitHub Actions, здесь — разделение зон в терминах RN/Expo.
Сводить варианты в одной таблице — не ради критики EAS, а чтобы в ревью говорить на одном языке об очереди, образе, подписи и аудите. Таблица исходит из того, что команда уже пользуется profile в eas.json и обладает базовыми навыками сопровождения macOS.
| Измерение | EAS Build (управляемый) | Self-hosted на выделенном удалённом Mac |
|---|---|---|
| Скорость выдачи (холодный старт) | Высокая, без закупки железа и установки ОС | Средняя, нужна первичная настройка SSH, «отпечатка» Ruby/Node и регистрация runner |
| Контроль очереди и конкуренции | Зависит от общего пула, пиковая задержка слабо предсказуема | Эксклюзивные CPU/диск, фиксированные верхние границы конкуренции и окна уборки |
| Сложность нативного стека | Хорошо для типового сценария Expo | Уместно при глубоком нативе, кастомных Pod, пре-скриптах и цепочке локальных патчей |
| Политика подписи и сертификатов | Тесно с учётными данными EAS | Можно выровнять с match, внутренним KMS и разграничением связок ключей по средам |
| Логи и аудит | Платформенные логи, детализация ограничена продуктом | Полный вывод xcodebuild и shell-сессий для внутреннего контроля |
| Операционная нагрузка | Низкая, ближе к «CI как сервису» | Средняя и выше, ближе к «как к кластеру VPS» |
«Управляемая сборка решает задачу от нуля к единице; выделенный узел — от единицы к масштабу: предсказуемость и проверяемость.»
Типичный компромисс: ежедневные пакетные и PR-контуры — на EAS, а Release Archive, матрицу UI-тестов и итерации с тяжёлым нативом — на выделенный удалённый Mac. Так «дорогие» человеко-часы удерживаются в среде с контролируемыми параметрами, оставляя силам интеграции удобный управляемый путь.
Матрица ниже по сигналам «масштаб команды × глубина нативного слоя × сила требований к комплаенсу» даёт практичные ориентиры. Она не заменяет вашу ёмкостную смету, но ускоряет согласование приоритетов в архитектурной сессии.
| Сигнал | Основной упор на EAS | Подключать выделенный удалённый Mac |
|---|---|---|
| Нативные зависимости | Немного community-модулей, без кастомных Pod | Приватные Pod, бинарные проприетарные SDK или нестандартные флаги компиляции |
| Релизный ритм | Еженедельные / двухнедельные циклы, допустимы пики очереди | Ежедневные релизы или жёсткие временные боксы, нужны фиксированные конкуренция и окна |
| Комплаенс | Нет требований к полевому аудиту | Нужен аудит на уровне SSH/команды, фиксированный egress или сегментация VPC |
| Текущий CI | iOS-конвейер обособлен от бэкенда | Хотите общие labels и кэш-политику с GitHub Actions / GitLab для iOS job |
// Фрагмент eas.json: развести «управляемую» сборку и внешний self-hosted job по profile
{
"build": {
"preview": { "distribution": "internal", "channel": "preview" },
"release-selfhosted": {
"extends": "production",
"env": { "EXPO_USE_FAST_RESOLVER": "1" },
"ios": { "image": "latest" }
}
},
"submit": { "production": {} }
}
Внимание: при смешении в одной ветке управляемого и self-hosted путей обязательно в README укажите, какой profile к какой модели учётных данных ведёт; иначе новые участники застрянут между связкой ключей и EXPO_TOKEN.
Помимо матрицы: «собирается» не равно «воспроизводится». Обновления образа на стороне провайдера происходят в фоне; если в репозитории не зафиксированы мажор Xcode и младший Node, нативный слой React Native после «незаметного» апгрейда может валиться пакетно. Плюс выделенного узла — закрепить xcode-select и Ruby Bundler как версию ядра и записать отпечаток в договор CI.
Шаги исходят из того, что npx expo prebuild у вас уже выдаёт iOS-проект, а на выделенном хосте нужны xcodebuild или Fastlane. Намеренно в духе администрирования VPS: учётка, ключи, уровень диска и отдача логов в одной связке.
Заморозить отпечаток тулчейна: на удалённом Mac зафиксировать xcodebuild -version, node -v, ruby -v, pod --version в BUILD_ENV.lock репозитория; любое обновление — через PR, без «чьего-то» SSH-апгрейда в обход.
Отдельный неинтерактивный пользователь для CI: не гоняйте пакетные задачи в сессии личного Apple ID; для runner или SSH job выделите пользователя и изоляцию связки ключей, в той же плоскости, что в гайде по воспроизводимым сборкам.
Разнести пути DerivedData и Pods: по репозиторию или ветке, например ~/DerivedData/$REPO/$BRANCH и ~/PodsCache/$REPO; стратегию уборки задавайте по cron, а не надеясь на агрессивную уборку ОС.
Вернуть iOS job в основной CI: self-hosted labels в GitHub Actions / GitLab на выделенный Mac, с теми же воротами согласования и конкуренции, что и Android; VNC — только в контролируемом окне отладки, а не «всегда включённый десктоп».
Артефакты и логи наружу: в контролируемое object storage или внутренний реестр — .ipa, dSYM и сырые логи xcodebuild; при требованиях регулятора — дополнительно запись сеанса до уровня команд для последующей экспертизы.
Двухнедельный контрольный эксперимент: пять характерных веток, параллельно измеряйте P50/P95 длительности и типы сбоев на EAS и на выделенном хосте; если при тяжёлом нативе выделенный хост стабильнее и дисперсия очереди меньше, переносите туда трафик Release.
Заметка: если ещё оцениваете регион и ёмкость диска, сначала сверьтесь с страницей тарифов аренды и вынесите в спецификацию ось «конкуренция × уровень диска × регион», затем сопоставьте с чек-листом выше.
xcodebuild на этапе линковки падает «случайно».Держать iOS целиком на общем пуле краткосрочно дешевле по людям; но когда нативные зависимости, требуемая детализация аудита и единая оркестрация с GitHub Actions / GitLab превращаются в жёсткие условия, «чёрная» очередь и незакреплённый образ накапливают дисперсию. С другой стороны, кластер Mac без дисциплины диска и «отпечатков» деградирует в отдельный снежный ком.
В сухом остатке командам поставки RN/Expo, которым нужны предсказуемая 24/7 очередь, проверяемые сеансы команд и единая схема runner-меток, тяжёлая нагрузка ближе к выделенной плоскости macOS, а EAS остаётся на лёгкую интеграцию и внешнюю коммуникацию. Для сценариев, где среда исполнения должна быть договариваемой нодой, с сокращением дисперсии очереди и обслуживанием iOS CI/CD и длительной автоматизации, аренда Mac Mini в облаке NodeMini часто оказывается рациональнее.
Когда iOS-контур должен вести себя как увеличиваемый узел: фиксированные диск и конкуренция, экспортируемые логи на уровне команд или нативная цепь не вписывается в образ — выделенный удалённый Mac уместен. Сначала пилот по странице тарифов, затем решение о полном переносе Release job.
Да, поэтому жёстко разводите profile и границы связки ключей: на EAS — только EAS-управляемое подписание, на self-hosted — match или корпоративный KMS; критерии переключения в README. Дополнительно — справочный центр, разделы про SSH и учётные записи.
В гайде по runner — метки, кэш и очереди; здесь — разграничение EAS и выделенной плоскости в терминах RN/Expo. Если runner уже на удалённом Mac, шестишаговый чек-лист выше можно трактовать как расширение критериев приёмки.