Мобильная разработка под iOS особенная: собрать приложение можно только на macOS, среда разработки по сути только одна, большая часть принятого в сообществе тулинга написана на Ruby, свой пакетный менеджер появился только пару лет назад. Тяжко.

А когда речь заходит про автоматизацию тестирования и сборок — тушите свет: Xcode Cloud появился совсем недавно и почти ничего не умеет, популярные облачные решения могут месяцами не обновлять стек на новые мажорные релизы среды разработки или ОС, а ценник при этом может быть в 10 раз больше, чем за машинки на Linux. Ещё тяжелее.

Меня зовут Леха Берёзка, я iOS-техлид в Додо Пицце и сейчас я расскажу как мы собрали свой CI на М1, с виртуализацией и на полном нативе.

Тигр в аквариуме

Когда я пришёл в Додо в январе 2019 года у нас уже был CI. Это был развёрнутый в компании сервер TeamCity, который собирал AdHoc-сборки. Релизные сборки вроде бы вообще ручками на машинах разработчиков собирались и загружались в App Store Connect напрямую через Xcode. Тестов в проекте вообще ещё не было.

Единственный воркфлоу с AdHoc-сборками гонялся на Mac Mini 2012 года, который стоял прямо в кабинете разработки «Аквариум». А сертификаты и провижны на этот «миник» заливались вручную через VNC по необходимости. Эту машинку мы называли как Валерой, так и Тигром — в честь нашего тестировщика Пети, у которого на проблемы с качеством была тигриная хватка. Петь, привет!

Кадр из архивного видео
Кадр из архивного видео
В аквариуме было уютно, он был «обжитым» местом: даже самовар чей-то был, которым мы чай заваривали
В аквариуме было уютно, он был «обжитым» местом: даже самовар чей-то был, которым мы чай заваривали

Случалось и до слёз бесячее: иногда уборщица выключала Тигра (не Петю) из розетки и у нас переставали собираться сборки.

Bitrise

И вдруг кто-то хороший в компании предложил нам переехать на какой-нибудь облачный сервис. Сказал, что деньги есть, и пора бы нам уже как взрослым дядям быть. Дальше деталей не помню, но по итогу мы выбрали Bitrise. Миша Рубанов, нынешний хед мобильной команды, сел перевозить нас туда с тимсити, но не смог дотолкать до конца — не хватило опыта. Мне же эта тема была чуточку ближе: я какое-то время обслуживал кафедру информатики на кафедре своего ВУЗа: настраивал там локальную сеть, поднимал Active Directory, ставил софт и делал так, чтобы студенты ничего не могли сломать. Не настройка сиая, конечно, но хоть что-то. Вызвался помочь, мне дали добро и доступы, и я успешно допереехал.

С битрайзом ушло ограничение на одну сборку в параллели и ушла проблема внезапно отключенного сиая, но появились и новые моментики:

  1. С выходом новых икскодов и макосей битрайз не торопился их ставить на свои тачки, что нас огорчало — из-за этого мы месяцами не получали фич новых свифтов.

    Это мы не можем найти Xcode 11.1, хотя он уже давно вышел
    Это мы не можем найти Xcode 11.1, хотя он уже давно вышел
  2. Сборки замедлились раза в 2 и стали тянуться иногда по 40-50 минут. Это нас тоже огорчало, но пока терпимо.

    Саммари прогона джобы на битрайзе
    Саммари прогона джобы на битрайзе

Кроме того, осталась проблема с ручным обновлением сертификатов:

Интерфейс обновления сертификатов для подписи приложения на битрайзе. В случае любых изменений с провижном надо было сюда зайти, ручками удалить старое и загрузить новое.
Интерфейс обновления сертификатов для подписи приложения на битрайзе. В случае любых изменений с провижном надо было сюда зайти, ручками удалить старое и загрузить новое.

Потихоньку я это всё докручивал до ума: подключил к проекту fastlane match для удобной работы с сертификатами и провижнами, настроил оповещения о сборках и тестах в слаке, добавил воркфлоу с релизной сборкой.

Тут надо сделать отступление, чтобы плавно подготовить к новой проблеме, с которой мы столкнулись.

Мы стараемся поддерживать процесс разработки максимально быстрым, и в то время процесс у нас выглядел так:

  • Не держим ветки с новым кодом по несколько дней, а вливаем его в основную ветку, develop, как можно раньше.

  • Фичатоглами пока ещё не пользуемся, потому что не умеем.

  • Не создаём PR и не просим никого отревьюить код. Все, кому мы про это рассказывали, мягко говоря, ох как удивлялись и не понимали, как мы так работали.

Не совсем то, о чем говорит Дядя Мартин описывая CI, но уже близко к этому.

С маленькой командой из трёх человек это ещё работало, пока не перестало из-за...

Заканчиваем отступление, возвращаемся к проблеме.

В Додо Пицце для iOS появились юнит-тесты. А с ними и новая особенность: они иногда ломаются. А в нашем случае это могло произойти незаметно, потому что в develop кто угодно мог пушнуть любые изменения. Конечно же мы пытались так не делать и перед пушем в дев прогоняли тесты локально, но иногда не прогоняли — человеческий фактор.

Сначала тесты ломались редко, но с ростом команды шанс стянуть красный develop повышался. И каждый такой раз приходилось выяснять почему у тебя тесты красные: это ты что-то сломал своими изменениями или кто-то до тебя успел. И на эти выяснения тратилось время.

Тест-ревью: как прошли два года написания unit-тестов

В какой-то момент мы от такого устали и решили, что пора добавить прогон тестов на CI: пусть на каждый пуш в develop у нас будут запускаться тесты и сообщать свой статус в слаке. Это еще не обязательные PR с проверками, но лучше чем ничего. На какое-то время это даже помогло, потому что все разработчики теперь прямо в слаке видели кто, каким комитом и что именно сломал.

Но битрайзу стало тяжеловато: на каждый пуш теперь гонялись сразу две сборки, адхкод и тесты, а наш тариф давал нам лишь 3 слабых машинки. Результат закономерный: очереди на 1-2 часа. Огорчает, но в тот момент было терпимо.

Прошло какое-то время, в Додо появились два новых стартапа: Дринкит и Донер 42. Их мы тоже подключили к битрайзу и проблема с очередями усилилась: теперь приходилось ждать по 2-4 часа. Чтобы хоть как-то можно было жить мы стали собирать AdHoc-сборки не на каждый комит, а 3 раза в день, по расписанию. Стало полегче.

А ведь решение то было: купить платный тариф поприличнее, который бы дал больше тачек, да и тачки были бы мощнее. Мы этот тариф даже на триале погоняли, но покупать почему-то не стали — не помню как так вышло. Скорее всего из-за размытых ответственностей и нашей «зелености» никто не сформировал такой запрос и не попросил компанию об этом.

Self-hosted GitHub Actions Runners

В 2018 году запустился GitHub Action — CI от GitHub. Примерно в 2020 мы в Додо начали перевозить туда свои проекты. Как раз тем же летом Глеб, наш архитектор, предложил перевезти туда и мобилу.

Мы в мобиле посмотрели на наше среднее время сборки, на количество этих сборок, глянули ценник на macOS-раннеры гитхаба, ахнули и купили шесть Mac Mini на M1. Поставили их в нашем офисе и постепенно перевезли все наши проекты из битрайза на GHA. В терминах гитахаба «свои» тачки называются Self-Hosted Runners.

Скажу честно: было долго и больно. Сам переезд был быстрым, но вот поддержка — больное место. Главная проблема одна — self-hosted раннер в конце прогона ничего на себе не подчищает. Там нет виртуалки, это просто процесс запущенный на пользователе, который ловит джобы и выполняет их.

А последствий у этой проблемы много:

  • Все очистки приходится писать прямо в воркфлове и есть шанс что-то забыть.

  • Иногда забываешь что-нибудь почистить и на раннере внезапно кончается место.

  • Иногда забываешь что-нибудь почистить и случайно ломаешь прогоны у другого проекта.

Зато из плюсов — сборки по 15-20 минут и никаких очередей.

Шучу: из-за этих проблем всё постоянно было раздолбано и были очереди. Иногда в строю из шести тачек была лишь одна. А как красиво начинали.

Со времени мы порешали одну проблему за другой, и стало можно жить.

Но в какой-то для GHA-раннера вышел апдейт, который добавил нативную поддержку Apple Silicon. На самом деле всё это время мы хоть и гоняли прогоны на M1, но толку от железа было мало: GHA-раннер, установленный на каждую машинку, запускался из под розетты.

Ну мы взяли и пошли обновлять наши раннеры, а там опять проблемы:

  • Ruby не работает.

  • Сломался Ruby.

  • Какие-то проблемы с Ruby.

    Очередная проблема, внезапно возникшая из-за попытки обновления рубей
    Очередная проблема, внезапно возникшая из-за попытки обновления рубей

У проблем с рубями две причины:

  1. GHA долгое время не предоставлял облачных тачек на Apple Silicon, то есть на arm64.

  2. Разработчики степа setup-ruby, который мы используем, не могли добавить поддержку arm64 пока такие облачные тачки не начнет предоставлять GHA.

Было несколько вариантов, чтобы всё починить:

  1. Предустановить нативные руби на тачки заранее, не используя степ setup-ruby.

  2. Попробовать всё-таки что-то там поколупать, чтобы тачки были как можно чище.

Мы пошли вторым путём: поперебирали версии рубей, нашли ту, что хорошо работает из под розетты и спокойно устанавливается степом setup-ruby, зафиксировали эту версию и пошли работать работу.

Ретроспективно я понимаю, что выбрал неправильный путь и стоило идти в предустановку рубей на раннерах.

Cilicon

Осенью 2021 Apple выпустили свой фреймворк для создания нативных виртуалок на М1. А тулинг не выпустили.

Мы посмотрели на документацию, почитали гайды и поняли, что пока что не готовы вкладываться в написание своего тулинга.

А уже летом 2022 Глеб принёс нам ссылку на Cilicon — тулинг поверх того самого эплового фреймворка виртуализации. Мы посмотрели, почитали и поняли, что хотим.

Работает тулза так:

  1. Создаёшь образ виртуальной ОС.

  2. Запускаешь его в режиме редактирования, настраиваешь и ставишь нужный софт.

  3. Запускаешь в режиме «только чтение» и виртуалка начинает гонять на себе сборки.

При запуске виртуалки в режиме чтения Cilicon создаёт её копию, гоняет сборку на ней, а в конце прогона эту копию грохает. Казалось бы не самое быстрое решение, особенно с учётом что образ весит десятки гигабайт, но при использовании APFS это работает чудесно:

  • Копия на самом деле не полная копия, а лишь ссылка на оригинальные файлы. Это позволяет создать её моментально и не занимать в 2 раза больше места на диске.

  • При внесении изменений в копию изменения вносятся именно в копию, не задевая оригинал. Это позволяет оставлять исходник в нетронутом состоянии.

На самом деле APFS работает сложнее, но такое грубое поверхностное объяснение хорошо передаёт суть.

Выглядит отлично: быстро, дешево и решает основную массу наших проблем. Ну мы взяли и установили Cilicon на наши тачки.

Сама установка не сильно сложная, хотя документация могла бы и получше быть. Мне понадобился один день, чтобы всё настроить в Cilicon, создать и полностью подготовить образ виртуалки, внести все шаги во внутреннюю документацию и даже полноценно запустить всё это на одной из наших тачек.

Все тачки мы перевели на виртуалки 24 марта 2023. Спустя 9 месяцев опыта вот что я могу сказать:

  • Полностью ушли проблемы с тем, что кто-то за собой что-то не подчистил и этим повлиял на другие прогоны.

  • Производительность не просела ни на капельку — сборки собираются столько же, сколько и напрямую.

  • Если что-то надо обновить на сборочных, то теперь достаточно единожды внести изменения в образ и раскидать его по остальным тачкам.

Cilicon работает стабильно и очень нам помогает.

А может надо было на облаке сидеть всё таки?

Мы постучались в АПИ гитхаба и узнали, сколько всего времени гоняли сборочки используя их воркфловы и наши тачки: 1 287 256 минут.

Если сходить в раздел About billing for GitHub Actions, то там сегодня ценники на машинки с macOS такие:

Operating system

vCPUs

Per-minute rate (USD)

macOS

3 or 4

$0.08

macOS

12

$0.12

macOS

6 (M1)

$0.16

Давайте рассмотрим каждую из машинок.

macOS, 3-4 vCPUs

Этот раннер — на интеле.

Наши воркфловы на таких интел-тачках проходят в среднем в 2 раза дольше, чем на наших М1. Это ощутимо дольше. Мы на таких тачках собираем наши релизные сборки.

macOS, 12 vCPUs

Этот раннер тоже на интеле.

Мы попробовали его буквально раз — буст в сравнении с 3-4 vCPUs получили, но слабый. Это из-за того, что непосредственно сборка — не самое тяжелое в нашем воркфлофе: установка разного тулинга из brew, резолвинг SPM-зависимостей, пребилднутых картажных зависимостей из кеша и установка сертификатов для подписи через match в сумме занимают времени больше, чем сборка. Ускорить эти шаги — наша точка для роста, но сейчас вот так. Вообще это смешно, конечно.

macOS, 6 vCPUs, M1

Третий раннер — на Apple Silicon, на M1. На нем мы собираемся побыстрее чем на интеле, но, опять же, упираемся в разное вокруг билда:

Прямо сейчас пробуем пересесть на этот раннер с младшего интела и понять сколько выиграем или проиграем.

Если знаете как ускорить резолвинг SPM-зависимостей или как устанавливать ченжлонг для сборок в тестфлайте без ожидания их процессинга — напишите, пожалуйста, в комментариях.

Приколы нашего городка

Мы не можем использовать интел-тачки для юнит-тестов:

  1. У нас есть скриншот-тесты, которыми мы проверяем вьюхи.

  2. Рендер графики напрямую зависит от чипа.

  3. У всех разработчиков тачки на Apple Silicon.

  4. Скриншоты для тестов мы записываем с тачек разработчиков, то есть с Apple Silicon.

Если скриншот-тесты, записанные на M1, запустить на тачке с чипом Intel, то все тесты завялятся из-за разницы в скриншотах: скругления, тени и прочие моменты с полупрозраночностями будут отличаться.

В это время гитхаб при запуске раннеров на M1 отдали им тот же тег macos-latest-xlarge, что и для 12-ядерных раннеров на интеле. То есть джоба при запуске на раннере с таким тегом может запуститься как на Intel, так и на Apple Silicon — тут как повезет.

12-ядерные интелы при этом постепенно уезжают на свой отдельный тегmacos-latest-large:

The 12-core macOS larger runner is moving from xlarge to large

Но сколько времени будет идти этот перезд — я не знаю: записи в блоге уже 2 месяца, а у нас часть сборок все еще попадает на интел.

Получается, сейчас мы вообще никак не можем гонять юнит-тесты на облачных тачках гитхаба.

Денюжки

Зная, сколько минут мы гоняем наши воркфловы, посчитаем сколько бы мы потратили, если бы была возможность сидеть на облачных раннерах.

Берем:

  1. Стоимость минуты самого дешевого интел-раннер, который macOS, 3-4 vCPU: $0,08

  2. Замедление относительно наших раннеров: ×2

  3. Время, которое потратилось на джобы на наших раннерах: 1 287 256 мин.

Перемножаем: 1 287 256 × 2 × 0,08 = 206 000 долларов.

У нас не самые оптимизированные джобы, в них есть что ускорять. Но если мы поднажмем, хорошо вложимся и ускоримся в 2, 3 или даже в 4 раза — свои тачки для нас всё равно будут выгоднее: шесть топовых Mac mini на M2 Pro по $1300 каждый стоят в сумме $7800, а потраченное мной время на поддержку этой инфры явно не стоит $200000.

Наш CI на джобы для пиццы тратит времени больше, чем для всех других проектов вместе взятых
Наш CI на джобы для пиццы тратит времени больше, чем для всех других проектов вместе взятых

Что делаем с CI дальше

Осталось несколько вещей:

  • Научиться удобно распространять образы между тачками. Сейчас мы по крону запускаем rsync и актуализируем образы. Оно работает, но это неудобно.

  • Настроить мониторинг, чтобы быть в курсе состояния машин

И с тем и с другим может помочь tart. Осталось сесть и прикрутить.

А как у вас?

Расскажите, как мобильный CI устроен в вашей компании:

  1. Каким сервисом пользуетесь?

  2. У вас свои тачки или облачные?

  3. Есть ли отдельная команда, которая за все это отвечает?

  4. Может где-то уже статью свою написали или пост в канале про то, как у вас устроено?

Комментарии (5)


  1. Timbaev
    29.12.2023 12:06

    Привет!
    Спасибо за статью, кажется, вы сэкономили нам время на исследование Cilicon и tart, потому что тоже уже очень хотим двигаться в эту сторону.

    Интересно было бы узнать, как вы совмещаете Ruby и Swift? У нас сейчас вся инфра в основном на Ruby (Fastlane и куча логики вокруг него), но хотим подумать в сторону Swift, чтобы большая часть инфры была на ней. Может уже смотрели в эту сторону, были ли с этим у вас какие-то сложности/интересные моменты?


    1. AllDmeat Автор
      29.12.2023 12:06
      +1

      Руби мы используем только для фастлейна, по сути. И долю фастлейна мы в Пицце постепенно сокращаем, прям намеренно:

      • Сборки и тесты мы в этом году начали запускать напрямую через xcodebuild, там всё просто оказалось. Хотя не без приколов — в Xcode 14.3 в команде test-withoud-building отломали флаг -testPlan , пришлось учиться работать напрямую с .xctestrun. Но там тоже ничего сложного.

      • Из фастлейна остались только запуски matchdeliver и pilot, хотелось бы и от них отказаться. В качестве замены мы присматриваемся к swiftlane или App Store Connect Swift SDK, но ни то ни другое пока не потрогали.

      • Часть тулинга пишем сами на свифте, что-то даже в опенсурц выкладываем. Готовой странички со ссылками на все пакеты нет, но в нашем канале постов 5 подряд есть про это.

      • Часть тулинга пишем на баше вместе с ChatGTP, очень довольны. Она конечно иногда ересь несет, но по итогу скрипты собираем.

      Из интересного — абсолютно всё. Каждая задача по инфре для меня как для iOS-разраба — новая, сложная, непонятная. Но после каждой закрытой задачки ощущение, что преисполнился.


    1. tokarev
      29.12.2023 12:06

      мы уже давно делаем альтернативу fastlane, но используя обычные shell команды https://github.com/codemagic-ci-cd/cli-tools Очень просто в обучении и переносе между локальной средой и любым CI/CD провайдером

      В качестве runtime выбран Python, потому что по-умолчанию присутсвует на всех macOS компьютерах и нет таких проблем совместимости как с Ruby


  1. AllDmeat Автор
    29.12.2023 12:06

    (удалено)


  1. tokarev
    29.12.2023 12:06

    Если знаете как ускорить резолвинг SPM-зависимостей

    пробовали Tuist? мы здесь писали и результат на тестовых проектах просто офигенный

    или как устанавливать ченжлонг для сборок в тестфлайте без ожидания их процессинга — напишите, пожалуйста, в комментариях.

    если я правильно понял, то Codemagic делает это после того как основной билд закончился. В результате можно запускать новые билды и они не будут ждать в очереди и скорость TestFlight / App Store не влияет на количество потраченных минут.

    И кстати типичная ошибка считать затраты поминутно как будто вы билдите 24x7. Для таких команд всегда надо брать план с фиксированной стоимостью (у нас 3 M2 машины с анлим минутами стоят $4k в год, можете посчитать что выгоднее со всеми затратами на обновление Xcode / Ruby версий) да простите меня за наглую рекламу :)