Как Додо Пицца доставляет свежий код: история мобильного CI для iOS
Мобильная разработка под 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, ставил софт и делал так, чтобы студенты ничего не могли сломать. Не настройка сиая, конечно, но хоть что-то. Вызвался помочь, мне дали добро и доступы, и я успешно допереехал.
С битрайзом ушло ограничение на одну сборку в параллели и ушла проблема внезапно отключенного сиая, но появились и новые моментики:
С выходом новых икскодов и макосей битрайз не торопился их ставить на свои тачки, что нас огорчало — из-за этого мы месяцами не получали фич новых свифтов.
Это мы не можем найти Xcode 11.1, хотя он уже давно вышел Сборки замедлились раза в 2 и стали тянуться иногда по 40-50 минут. Это нас тоже огорчало, но пока терпимо.
Саммари прогона джобы на битрайзе
Кроме того, осталась проблема с ручным обновлением сертификатов:
Потихоньку я это всё докручивал до ума: подключил к проекту fastlane match для удобной работы с сертификатами и провижнами, настроил оповещения о сборках и тестах в слаке, добавил воркфлоу с релизной сборкой.
Тут надо сделать отступление, чтобы плавно подготовить к новой проблеме, с которой мы столкнулись.
Мы стараемся поддерживать процесс разработки максимально быстрым, и в то время процесс у нас выглядел так:
Не держим ветки с новым кодом по несколько дней, а вливаем его в основную ветку, develop, как можно раньше.
Фичатоглами пока ещё не пользуемся, потому что не умеем.
Не создаём PR и не просим никого отревьюить код. Все, кому мы про это рассказывали, мягко говоря, ох как удивлялись и не понимали, как мы так работали.
Не совсем то, о чем говорит Дядя Мартин описывая CI, но уже близко к этому.
С маленькой командой из трёх человек это ещё работало, пока не перестало из-за...
Заканчиваем отступление, возвращаемся к проблеме.
В Додо Пицце для iOS появились юнит-тесты. А с ними и новая особенность: они иногда ломаются. А в нашем случае это могло произойти незаметно, потому что в develop кто угодно мог пушнуть любые изменения. Конечно же мы пытались так не делать и перед пушем в дев прогоняли тесты локально, но иногда не прогоняли — человеческий фактор.
Сначала тесты ломались редко, но с ростом команды шанс стянуть красный develop повышался. И каждый такой раз приходилось выяснять почему у тебя тесты красные: это ты что-то сломал своими изменениями или кто-то до тебя успел. И на эти выяснения тратилось время.
В какой-то момент мы от такого устали и решили, что пора добавить прогон тестов на 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.
Очередная проблема, внезапно возникшая из-за попытки обновления рубей
У проблем с рубями две причины:
GHA долгое время не предоставлял облачных тачек на Apple Silicon, то есть на arm64.
Разработчики степа setup-ruby, который мы используем, не могли добавить поддержку arm64 пока такие облачные тачки не начнет предоставлять GHA.
Было несколько вариантов, чтобы всё починить:
Предустановить нативные руби на тачки заранее, не используя степ setup-ruby.
Попробовать всё-таки что-то там поколупать, чтобы тачки были как можно чище.
Мы пошли вторым путём: поперебирали версии рубей, нашли ту, что хорошо работает из под розетты и спокойно устанавливается степом setup-ruby, зафиксировали эту версию и пошли работать работу.
Ретроспективно я понимаю, что выбрал неправильный путь и стоило идти в предустановку рубей на раннерах.
Cilicon
Осенью 2021 Apple выпустили свой фреймворк для создания нативных виртуалок на М1. А тулинг не выпустили.
Мы посмотрели на документацию, почитали гайды и поняли, что пока что не готовы вкладываться в написание своего тулинга.
А уже летом 2022 Глеб принёс нам ссылку на Cilicon — тулинг поверх того самого эплового фреймворка виртуализации. Мы посмотрели, почитали и поняли, что хотим.
Работает тулза так:
Создаёшь образ виртуальной ОС.
Запускаешь его в режиме редактирования, настраиваешь и ставишь нужный софт.
Запускаешь в режиме «только чтение» и виртуалка начинает гонять на себе сборки.
При запуске виртуалки в режиме чтения 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-зависимостей или как устанавливать ченжлонг для сборок в тестфлайте без ожидания их процессинга — напишите, пожалуйста, в комментариях.
Приколы нашего городка
Мы не можем использовать интел-тачки для юнит-тестов:
У нас есть скриншот-тесты, которыми мы проверяем вьюхи.
Рендер графики напрямую зависит от чипа.
У всех разработчиков тачки на Apple Silicon.
Скриншоты для тестов мы записываем с тачек разработчиков, то есть с 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 месяца, а у нас часть сборок все еще попадает на интел.
Получается, сейчас мы вообще никак не можем гонять юнит-тесты на облачных тачках гитхаба.
Денюжки
Зная, сколько минут мы гоняем наши воркфловы, посчитаем сколько бы мы потратили, если бы была возможность сидеть на облачных раннерах.
Берем:
Стоимость минуты самого дешевого интел-раннер, который macOS, 3-4 vCPU: $0,08
Замедление относительно наших раннеров: ×2
Время, которое потратилось на джобы на наших раннерах: 1 287 256 мин.
Перемножаем: 1 287 256 × 2 × 0,08 = 206 000 долларов.
У нас не самые оптимизированные джобы, в них есть что ускорять. Но если мы поднажмем, хорошо вложимся и ускоримся в 2, 3 или даже в 4 раза — свои тачки для нас всё равно будут выгоднее: шесть топовых Mac mini на M2 Pro по $1300 каждый стоят в сумме $7800, а потраченное мной время на поддержку этой инфры явно не стоит $200000.
Что делаем с CI дальше
Осталось несколько вещей:
Научиться удобно распространять образы между тачками. Сейчас мы по крону запускаем rsync и актуализируем образы. Оно работает, но это неудобно.
Настроить мониторинг, чтобы быть в курсе состояния машин
И с тем и с другим может помочь tart. Осталось сесть и прикрутить.
А как у вас?
Расскажите, как мобильный CI устроен в вашей компании:
Каким сервисом пользуетесь?
У вас свои тачки или облачные?
Есть ли отдельная команда, которая за все это отвечает?
Может где-то уже статью свою написали или пост в канале про то, как у вас устроено?