Мобильная разработка под 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, хотя он уже давно вышел
  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 дальше

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

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

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

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

А как у вас?

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

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

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

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

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