Привет. Я Эрнест Пивнев, девопс-инженер в команде мобильной разработки Контура. Мы относительно новое направление в компании и наша задача –– предоставлять услуги по разработке мобильных приложений другим командам и продуктам.
Среди нас есть инженеры, которые создают продуктовые приложения, и те, кто занимается инфраструктурой разработки:
создает инструменты для ускорения разработки приложений
поддерживает дизайн-систему и системы CI/CD и автотестов
развивает переиспользуемые между приложениями модули
исследует новые мобильные технологии
У нашей команды на текущий момент в репозитории 40+ разных проектов, что накладывает свои технические требования к CI/CD. Основные требования следующие:
Мультиплатформенность
Быстрое подключение в проект
Быстрый процесс обновления
Все виды тестирования
Единое место хранения секретов
Но давайте для начала посмотрим, как это было устроено до и с чем приходилось мириться.
Как было и почему это проблема
Особенности
Этот раздел статьи мог бы называться «Минусы». Но часто случается так, что то, что для одного контекста минус, то для другого плюс. Поэтому мы оперировали термином «особенность». Итак, пройдемся по некоторым особенностям, с которыми я столкнулся, когда только пришёл в команду.
Разный CI для Android и для iOS
Первой моей задачей стало создание автотегирования. Я написал код, прошёл ревью. Приношу тимлиду, на что он мне сказал, что сделано только для Android – теперь надо для iOS.
Разный gitflow на Android и на iOS
Ну кажется, что проблем не много, код то уже написанный, скопировал-вставил и заработает. Давайте посмотрим, как выглядят pipeline для android и для ios.
Pipeline Android:
Pipeline iOS:
Как вы поняли, оказалось, что для двух платформ не просто разные процессы, а разный гитфлоу. И то решение, которые я создал, пришлось переписать с другими вводными и под другие условия.
Submodules
Итак, я наконец выкатываю решение. Сообщаю разработчикам, что можно пользоваться, но у них ничего не происходит. Дело в том, CI подключался в проект через сабмодули, которые не умеют «налету» подхватывать обновления.
То есть чтобы появился новый CI в проекте, нужно удалить сабмодули, собрать их заново, и это не всегда помогает с первого раза. В общем, боль, унижение, принятие. Идём дальше.
Разный процесс работы с секретами
У команды iOS секреты хранились в Vault и вытаскивались на этапе fastlane. А команда Android хранили секреты в CI/CD переменных. По сути, оба эти подхода имеют место быть, но когда речь заходит о мультиплатформенности, то очевидно, что срастить их будет достаточно сложно.
Более того, у команды iOS была боль, что секреты нужно получать до начала сборки. Когда как у команды Android проблема заключалась в том, что доступ к CI/CD переменным не всем хочется давать. И если нужно что-то поправить, то все ходят через одного человека.
Итого:
Приходится поддерживать обе версии CI
Submodules не подтягивают обновления
Дублирование кода
Сложная реализация мультиплатформы
Плюсы
Основа CI — fastlane
Инженеры обеих команд уже использовали в работе fastlane. Это платформа с открытым исходным кодом, которая упрощает процесс сборки и выкладки мобильных Android и iOS приложений. В ней множество готовых функций и плагинов, а также можно писать собственные функции и плагины, используя синтаксис языка ruby.
Как работает fastlane? Внутрь репозитория помещается Fastfile –– файл с кодом для fastlane. В этом файле описываются lane. Это определенные дорожки, которые описывают последовательность команд, которые необходимо выполнить для того, чтобы приложение оказалось на продовой площадке. В качестве команд могут использоваться как готовые плагины, так и самописные функции. Из CI в таком случае вызывается конкретный Lane, который, например, собирает приложение, или выкладывает его на продуктовую площадку..
Удобная тестовая площадка
Уже был создан классный тестовый стенд с множеством разных телефонов с различными версиями ОС и через центр приложений от Microsoft можно было подтягивать все тестовые версии. Соответственно, этот вопрос был закрыт.
Хорошо настроенное тестирование
В этот процесс тоже лезть не пришлось, потому что уже писались тесты под разный формат, активно внедрялись и продолжают внедряться UI-тесты.
Как стало
Перевели CI для обеих платформ на один шаблон
Чтобы это сделать, пришлось убедить разработчиков перейти на единый гитфлоу, объединить процессы, чтобы деплой был одинаковый, и унифицировать сам CI для обеих платформ.
После того, как мы это сделали, например, функция запуска тестов выглядит вот так для любой платформы:
script:
- bundle exec fastlane test
С точки зрения CI это одна и та же команда, но в зависимости от fastfile внутри проекта вызывает разные lane. Выглядит красиво и вызывать удобно.
Ушли от submodules
Конфиги для тестов, которые подтягивались через сабмодули, инженеры научились подтягивать через плагин gradle. А сам CI мы стали подтягивать через функцию импорта, которая есть у fastlane. Она позволяет импортировать любой файл в проект в рамках гита.
Написали скрипт для вытягивания секретов в before_script
Скрипт доставал секреты из Vault на этапе before_script в CI. Команда Android перенесла секреты в Vault. Команда iOS получила возможность вытаскивать секреты до запуска fastlane, что позже облегчило возможность реализации мультиплатформенного решения.
Перешли на модульную структуру fastlane
На этом хочется остановиться и разобрать подробнее.
Модульная структура fastlane
Казалось бы, причем тут модули? Ведь у нас есть файл .gitlab-ci для Android, есть файл .gitlab-ci для iOS, которые наследуются от общего шаблона. К Android подключается свой Fastfile, к iOS – свой. Вроде бы выработана идеальная схема.
Теперь начинаем писать CI для мультиплатформы, и нам потребуется написать новый Fastfile или с нуля, или переработать то, что уже есть. Получается, что наша новая система не упрощает нам работу при добавлении новой платформы. То есть мы будем сталкиваться с той же проблемой, от которой уходили при переходе на шаблон, только уже на этапе fastlane.
В поисках решения этой проблемы обнаружили в документации, что fastlane может легко собираться из модулей.
То есть текущий fastfile делим на модули: модуль тестирования для Android, модуль тестирования для iOS и тд. И уже из этих модулей, как из кубиков, мы заново формируем fastfile.
Получается следующая схема работы шаблона CI и модулей:
Сверху – шаблон CI, в котором наследуется gitlab всей фабрики. Снизу – модули fastlane, из которых собираются Fastfile.
Мы нарисовали удобную для нас систему модулей:
Из этих модулей собирается fastfile для Android и iOS, и здесь же формируется fastfile для КММ –– мультиплатформы типологий.
После этих внедрений Fastfile iOS стал выглядеть так (до этого он выглядел на 400+ строк кода):
Теперь сам fastlane предоставляется для команд разработки как сервис. Я вношу изменения в модули, а они могут просто собирать из необходимых модулей CI, и у них всё работает.
Почему всё это хорошо
Больше не надо поддерживать разные CI
Изменения в проекты подтягиваются автоматически
Появился мультиплатформенный CI
Появилась возможность быстро создавать новые pipeline
Спустя короткое время добавился packages iOS, который я собрал достаточно быстро, и React Native, который был собран без моего участия. И актуальная структура модулей выглядит так:
Выводы или история успеха
Изначально мобильной разработке понадобился девопс, потому что сложилась классическая ситуация. Было 2 сеньора: один на anrdoid, второй на iOS. И у каждого сеньора был этот огромный fastfile, который только они и понимали. А это критично, ведь люди имеют свойство выходить из проектов.
По итогу после внедрения всех этих изменений так и случилось: оба покинули проект. Но катастрофы не произошло, ведь, как я писал выше, CI и Fastlane сейчас предоставляется по сути как сервис. При желании инженеры могут из готовых модулей собирать себе готовый CI, и в модульную структуру вносить изменения гораздо проще.
На данный момент я уже полгода как тоже вышел из проекта и команда живёт без девопса. Меня звали что-то починить только пару раз. В остальном разработчики самостоятельно поддерживают и развивают модульный CI.
PetyaUmniy
Весьма странная формулировка, есличесна. Что значит на лету не подхватывают обновления? И зачем налету их подхватывать? Использовать "latest" = незакрепленные версии чего угодно в разработке ПО является антипатерном. Потом у вас обновится это "что-то" вместе с вашими изменениями кода и вы замучаетесь разбираться, что это на самом деле отъехало? А если с это же время вам нужно будет срочно катить изменения, пока вы будете разбираться - это будет факап. (Буквально неделю назад у меня в одной конфигурации ansible обнаружился незакрепленный модуль python-requests, и он, как несложно догадаться, обновился. И нет, это был ни мажор update, ни минор, ни даже патч - это было обновление билда от майнтейнеров дистра! Но этого оказалось достаточно чтобы развертывание инстанса поломалось с экзотическими ошибками).
Но конечно распространять через submodules куски пайплайнов это та еще боль. Особенно если в сабмодулях так же работает несколько человек и у сабмодулей тоже есть сабмодули. Прямо пишу и чувствую всю эту боль, как надо ребейзить несколько взаимосвязанных веток в нескольких вложеных репозиториях. Отвратительно!
Не знаю будет ли кому-то полезно. Но для себя я принял за основной подход - импорта кусков пайплайнов, и не только: но и скриптов, конфигураций сборки, вспомогательных файлов и т.д. c помощью конструкций gitlab-ci:
include
- с указание версии импортируемого контента.!reference
- для инжекта его в script gitlab-ci.Причем, ВАЖНО, под инжектом подразумевается не только возможность что-то запустить здесь и сейчас в секции script. А возможность вкорячить это что-то в директорию с кодом (не обязательно точно туда) и потом уже использовать на своё усмотрение. Чтобы было понятнее, пример: все мы запускаем шаги пайплайна в
кубирнетисахконтейнерах (я в них гоняю в том числе, если касаться мобилы, flutter, ванила android, android emulator... и даже тесты 1c если касаться bdsm практик), и у всех постоянно есть боль, как сделать вменяемую нотификацию в наш корпоративный чатик из каждой job? Ведь в каждом образе своё окружение, каждый образ в общем случае собирается из своего отдельного репо, или вообще не собирается нами. Пересобирать все образы, чтобы вкорячить свой код для отправки? На практике не реально (а что если потом их обновлять?). Так вот такие скрипты "посылатели в чатик" и т.п. хорошие кандидаты на раскладывание через конструкцииinclude
+!reference
. Сослался на шелл скрипт, который делаетcat << 'EOF' > send-to-chatik.sh
, получил на выходе запускаемый код, отправил с помощью него в чатик. Аналогично ложу dockerfiles (не dockerfile, но неважно), makefiles, fastlane и много чего еще. Подкладывают гиганские, сложные скрипты реализующие например кеширование выполнения job на основе дайжеса докер образа в kv хранилище и оборачиваю ими основной код job. Таким образом даже ради достаточно сложных вещей не обязательно что-то коммитить в каждый репозитарий. Сделал некоторое подобие фреймворка, в котором формализовал типовые "методы" (если их можно так назвать), который должен реализовывать подключаемый через!reference
модуль, разные способы вызова (непосредственно через reference (где нельзя передать аргументы) и через заранее определяемую в инициализации переменную с путем до подкладываемого кода (где аргументы можно))... Когда-нибудь может попытаюсь запилить статью.Таким образом можно:
Отказаться от бесячих сабмодулей.
Продолжать держать все под контролем git.
Не раскладывать одни и те же сущности по разным репозиториям руками.
Не отказываться от версифицирования сущностей. Причем их версифицирование еще и становится per сущность. Можно разрабатывать их параллельно меньше боясь пересечений (пространство пересечений уже не 1 на весь сабмодуль)
???
PROFIT? Или не PROFIT?
Ernest_Pivnev Автор
Интересно будет почитать вашу статью, чтобы подробнее ознакомится с вашим решением.
В нашем случае расматривали разные варианты ухода от submodules, в том числе и использование include, !reference (кстати в шаблоне CI они активно используются). Но именно для импорта fastlane решили воспользоваться функцией самого fastlane, так как во-первых все равно у нас все проекты собираются с помощью fastlane, во-вторых эта функция позволяет импортить не просто latest версии, а конкретные ветки или теги из репозитория.
Как итог получили возможность все конфиги и сам CI хранить в одном месте и гранулярно включать в проект, что позволяет параллельно разрабатывать новые версии, и фризить какие-то проекты на старых версиях, если проект не готов пока переходить на новую версию CI. При этом обновления появляются в проектах мгновенно, достаточно просто указать нужную ветку, или зарелизить новую версию CI (если проект все таки подключил себе latest версию).
А какую версию подключать себе в проект решают сами разработчики.