С чего вы начинаете
Арендовав выделенный облачный Mac mini M4 у KVMFLUX, вы получаете фактически root-доступ на настоящем железе Apple: 16ГБ единой памяти, 256ГБ SSD, предустановленный macOS с доступом по SSH и VNC. Никакого совместного использования, никакой виртуализации — поэтому xcodebuild видит полноценный M4, а Secure Enclave ведёт себя так же, как на вашей настольной машине.
Выбирайте регион ближе к хранилищу артефактов, а не к команде. Раннер обращается к репозиторию и CDN намного чаще, чем к нему подключается человек — доступны регионы Сингапур, Япония, Южная Корея, Гонконг, Восточное побережье США и Западное побережье США.
Шаг 1 — сначала заблокируйте SSH, потом всё остальное
Машина поставляется с включённым входом по паролю для первого подключения. Первая же сессия должна это изменить: сначала добавьте ключ, потом отключите пароль.
ssh-keygen -t ed25519 -f ~/.ssh/kvmflux_ci -C "ci-runner"
ssh-copy-id -i ~/.ssh/kvmflux_ci.pub admin@<YOUR_MAC_HOST>
sudo tee -a /etc/ssh/sshd_config <<'EOF'
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
EOF
sudo launchctl kickstart -k system/com.openssh.sshd
Для узла CI критичны ещё две настройки: запретить машине уходить в сон и не давать фоновым демонам приостанавливаться при отсутствии подключённого монитора.
sudo pmset -a sleep 0 displaysleep 0 disksleep 0
sudo systemsetup -setrestartfreeze on
Держите VNC как аварийный канал. Некоторые диалоги Xcode — подтверждение лицензии, первый запрос доступа для симулятора — появляются только в графическом интерфейсе, и вам нужен путь внутрь, работающий даже если конфигурация SSH сломается.
Шаг 2 — установите тулчейн Xcode
Пропустите App Store. xcodes устанавливает нужную версию Xcode неинтерактивно — именно это нужно безголовому узлу. Сначала Homebrew, потом тулчейн.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install xcodesorg/made/xcodes aria2
xcodes install 16.2 --experimental-unxip
sudo xcodes select 16.2
sudo xcodebuild -license accept
sudo xcodebuild -runFirstLaunch
Определитесь с целевыми платформами. Если конвейер запускает тесты в симуляторе, загрузите нужные runtime заранее, а не при первой же задаче.
xcodebuild -downloadPlatform iOS
xcrun simctl list runtimes
xcodebuild -version
Добавьте инструменты, типичные для мобильных конвейеров: fastlane для lane-сценариев и подписи, git-lfs, если в репозитории есть бинарные ресурсы.
brew install fastlane git-lfs jq
git lfs install --system
Шаг 3 — зарегистрируйте машину как раннер
GitHub Actions, GitLab и Buildkite работают по одной схеме: скачиваете агент, даёте ему токен с ограниченной областью действия и запускаете как сервис. Вот как это делается для GitHub Actions на Apple Silicon.
mkdir ~/actions-runner && cd ~/actions-runner
curl -o runner.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gz
tar xzf runner.tar.gz
./config.sh --url https://github.com/your-org/your-app \
--token <REGISTRATION_TOKEN> \
--labels macos,arm64,m4 --unattended
./svc.sh install && ./svc.sh start
Установка через svc.sh подключает раннер к launchd, так что он продолжает работать после перезагрузки даже без вошедшей в систему графической сессии. Направляйте задачи по меткам:
jobs:
build:
runs-on: [self-hosted, macos, m4]
steps:
- uses: actions/checkout@v4
- run: xcodebuild -scheme App -destination \
'platform=iOS Simulator,name=iPhone 16' test
Токен регистрации истекает через час и одноразовый — это нормально. Долгосрочные учётные данные раннера хранятся в файле .credentials внутри его каталога, так что держите эту учётную запись «пустой»: без личных SSH-ключей, без сохранённых сессий в браузере.
Шаг 4 — дайте CI брелок, который он сам может разблокировать
Подпись — самое частое место, где ломается самостоятельная настройка Mac. Решение — отдельный брелок, который создаёт, разблокирует и уничтожает после использования сам lane-сценарий: так брелок входа никогда не выдаёт диалог и не протекает между задачами.
KEYCHAIN=ci.keychain-db
security create-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN
security set-keychain-settings -lut 3600 $KEYCHAIN
security unlock-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN
security import dist.p12 -k $KEYCHAIN -P "$P12_PASS" \
-T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASS" $KEYCHAIN
security list-keychains -d user -s $KEYCHAIN login.keychain-db
Строку с set-key-partition-list легко упустить — без неё codesign зависает, ожидая графический диалог с паролем, который никогда не появится. Если вы используете fastlane, команда match с приватным репозиторием сертификатов автоматизирует всё это целиком.
Кэш: оставляйте результаты на диске
Главное преимущество выделенной машины перед эфемерным раннером — сохраняемое состояние. Используйте это, не скачивая всё заново на каждой задаче:
- DerivedData — укажите фиксированный путь через
-derivedDataPath ~/ci-cache/dd, и время инкрементальной сборки обычно падает с минут до секунд. - Пакеты Swift — задайте
-clonedSourcePackagesDirPath ~/ci-cache/spm, чтобы разрешение зависимостей повторно использовало уже выполненные checkout между задачами. - CocoaPods и Homebrew — оба по умолчанию кэшируются в домашней папке пользователя раннера; просто не очищайте рабочую область между сборками.
Честно оцените использование диска. На SSD 256ГБ сам Xcode с runtime занимает около 40ГБ, а кэш активного проекта постепенно вырастает до 60–80ГБ. Чистите по расписанию, а не когда уже возникла проблема:
find ~/ci-cache/dd -maxdepth 1 -mtime +14 -exec rm -rf {} +
xcrun simctl delete unavailable
df -h /
Если в проекте крупные ресурсы или нужно держать несколько версий Xcode одновременно, опция доп. SSD +1ТБ стоит всего $11.7/мес. — это дешевле, чем разбираться с забитым диском перед релизом.
Параллелизм: сколько задач выдержит один M4?
Один M4 с 16ГБ памяти отлично справляется с одной тяжёлой задачей: полная чистая сборка плюс набор тестов в симуляторе загружает все ядра. Одновременный запуск двух задач с симулятором подойдёт для небольших приложений, но при росте нагрузки на память обе замедлятся.
Вот эмпирические правила, к которым мы пришли после месяцев обслуживания конвейеров:
- Для нагрузок «сборка плюс тесты» один процесс раннера, обрабатывающий одну задачу за раз, — самая надёжная настройка по умолчанию.
- Разделяйте lint, юнит-тесты и UI-тесты на отдельные задачи только после того, как добавите вторую машину — на одной машине это лишь создаёт очередь и накладные расходы.
- Ночные задачи — бесплатные вычисления: планируйте аудит зависимостей и прогон скриншотов на нерабочие часы часового пояса региона.
Когда время ожидания в очереди становится проблемой, добавление ещё одного арендованного узла масштабируется линейно — просто зарегистрируйте его с теми же метками, и планировщик сам распределит нагрузку. Расчёт выгодности суточного и постоянного узла — отдельная тема, которую мы разобрали в статье о расходах на периоды аренды; соответствующие рабочие процессы также показаны в обзоре сценариев использования.
Вся настройка в сжатом виде
- Вход по SSH только по ключу, сон отключён, VNC оставлен как резервный вариант.
- Установите Xcode через
xcodes, примите лицензию, заранее скачайте runtime. - Зарегистрируйте раннер как сервис
launchdс осмысленными метками. - Отдельный брелок для CI с настроенным списком партиций.
- Постоянный кэш с регулярной очисткой по расписанию.
Вся процедура занимает меньше часа практической работы, а результат — узел сборки, о котором команде больше не нужно думать. Это и есть главная цель.
Проверьте это на настоящем железе
Каждая команда выше написана и проверена на той же машине, которую мы сдаём в аренду. Возьмите одну, повторите шаги, и если что-то не подойдёт — отмените аренду. В любом случае никаких обязательств по железу не остаётся.
Выделенное железо, оплата в USD, отмена в любой момент.