Разработка
Как мы достигли Непрерывного развертывания с нативным приложением
Может показаться, что это всего лишь несколько изменений, но я уверяю вас, что это был долгий путь, полный обучения и, конечно же, ошибок!
В этой статье я хотел бы поделиться своим опытом последних месяцев. Точнее, с октября прошлого года, когда мы поставили перед собой задачу добиться непрерывного рабочего процесса развертывания (Continuous Deployment) нативного приложения, до того, что мы наконец достигли месяц назад. Теперь у нас есть более безопасный и быстрый процесс релизов!
Меня зовут Луис, я инженер-программист в Mercadona Tech. С октября 2019 года я работаю над DeliverApp, приложением, которое наши водители используют, чтобы упростить процесс доставки. С помощью приложения наши водители могут видеть список заказов, которые они должны доставить, а также выполнять различные другие действия: переход к адресу заказа в Картах Google, сканирование этикеток заказа, чтобы убедиться, что доставляемые продукты являются правильными, или сообщение об инциденте с Службу поддержки.
Первые шаги: релизный поезд
Вначале я придерживался той же стратегии, что и в приложении Shop. Каждые две недели мы готовим и выпускаем новую версию. Эта стратегия довольно хорошо объясняется в этом выступлении Антонио Эскудеро.
Но между двумя приложениями была большая разница: Shop был общедоступным, а DeliverApp предназначен для внутреннего использования (нашими единственными пользователями были бы наши водители). Таким образом, приложение Shop находилось в Google Play, тогда как DeliverApp находилось в корзине в нашем Google Cloud Storage, что позволило нам избежать проверки Google и ускорить развертывание.
Как только мы утвердили эту модель и почувствовали себя комфортно с ней, мы решили двигаться дальше и выпускать релизы каждый понедельник. Так мы смогли принести больше пользы и получить обратную связь раньше (обратная связь — это образ жизни, о чем наш технический директор Фернандо Диас говорит снова и снова).
Чтобы влияние релиза было минимальным, мы использовали поэтапный подход к развертыванию. Сначала мы выпускали новую версию на одном складе (в данном случае в Валенсии), а через два дня мы размещали ее на остальных складах. Если с релизом что-то не так, мы могли исправить это, не затрагивая все склады.
Как уведомить пользователя о наличии новой версии?
Поскольку у нас было внутреннее приложение, которое мы не выпускали в Google Play, нам пришлось реализовать собственный процесс обновления внутри приложения, который работал следующим образом:
На бэкэнде:
- Предоставляем конечную точку, которая выдает диапазон поддерживаемых версий с минимальной и максимальной допустимой версией.
- Посредством middleware проверяем, находится ли версия в пределах допустимого диапазона, и, если нет, возвращаем статус 416.
В приложении:
- Если мы получаем 416, мы показываем диалог, чтобы принудительно обновить версию приложения. Этот процесс загружает APK из Google Cloud Storage и запускает процесс установки (хотя для завершения установки нам потребуется ручное вмешательство — Изображение 1).
- Во время ежедневного использования приложение проверяет, доступна ли более новая версия, и, если да, загружает APK в фоновом режиме. Затем, когда пользователь выходит из системы, она предлагает установить обновление (Изображение 2).
Когда выпускается новая версия приложения, мы делаем запрос к этой конечной точке, чтобы обновить максимальную поддерживаемую версию.
Автоматизацию и доработка процесса релиза
Следующим шагом была автоматизация процесса, для чего мы использовали Fastlane и Jenkins. В Fastlane мы создали сценарии, которые Jenkins запускал на различных этапах: линтинг, модульные тесты, интеграционные тесты и тесты e2e. Тем не менее, наш процесс выпуска по-прежнему был ручным, и нужно было найти способ его автоматизировать.
Мы создавали новую дорожку в Fastfile для подготовки релиза: это создавало новый тег в GitHub, и после этого нам приходилось обращаться к Jenkins, чтобы создать этот тег вручную. Это было начало, но мы избежали создания и подписания APK вручную и загрузки его в корзину Google Cloud Storage.
По мере роста проекта мы выпускали версии с большим количеством изменений (помните, что мы все еще ждали понедельника, чтобы их запустить). Это создало проблему: чем больше изменений в новой версии, тем больше может быть ошибок. Это дало нам следующий шаг для улучшения.
Мы обсудили этот момент с командой, и все согласились с тем, что, несмотря на то, что мы планировали сначала релизить на одном складе, а затем, через два дня, выпускать на остальных, нам нужно было предпринять дополнительные меры, чтобы чувствовать себя в безопасности, когда мы выпускаем новые версии. Первый шаг был очевиден — выпускать более мелкие версии. Второй — обеспечить быстрый и автоматический механизм отката. И третий — использовать флаги функций, чтобы дать нам возможность быстро удалять ошибки, вызванные новыми функциями, без необходимости отката или выпуска новой версии.
Как мы можем откатить нативное приложение?
Посмотрев на другие проекты, мы увидели, что они могут легко безболезненно выполнять откат. Они просто переходили в Jenkins, создавали новую версию и позволяли Jenkins делать свое дело. К сожалению, мы не могли делать то же самое в Android.
Если заглянуть в официальную документацию, для идентификации APK используются два элемента:
- «VersionName»: строка, используемая в качестве номера версии, показываемой пользователям.
- «VersionCode»: используется системой Android для защиты от перехода на более раннюю версию, не позволяя пользователям устанавливать APK с более низким versionCode, чем версия, установленная в настоящее время на их устройстве.
Означает ли это, что прямой откат к более ранней версии невозможен? Что ж, документация довольно понятна, но помните, что мы работаем с внутренним приложением, поэтому мы смогли найти способ обойти это и выполнить откат.
Мы изменили наш код и способ управления нашими версиями. Мы решили использовать «versionName» как фактическую версию, а затем вместо того, чтобы обновлять «versionCode» с каждым выпуском, мы решили заморозить его. Давайте посмотрим на это на примере:
- Мы выпускаем новую версию v32 с «versionName» = 32 и «versionCode» = 1.
- Мы выпускаем новую версию v33 с «versionName» = 33 и «versionCode» = 1.
- Мы понимаем, что в версии 33 есть ошибка, поэтому нам нужно выполнить откат. Это возможно, потому что v32 имеет тот же код версии, что и v33 («versionCode» = 1), и поэтому система Android позволяет нам устанавливать APK с тем же кодом версии.
Вот так мы и создали механизм отката!
Чтобы показать вам реальный пример, мы поняли, что у нас есть ошибка в версии 168, и поэтому откатились до версии 167. Обратите внимание, как версия v168 теряет пользователей, когда отображается диалоговое окно о том, что версия вне допустимого диапазона!
Зависимость от других
Я хочу подробно объяснить эту часть, чтобы лучше понять процесс. На складе есть менеджер по доставке, который отвечает за координацию всех водителей.
Когда начинается смена, она встречает всех их. Там она раздает маршруты и уведомляет их, если в приложении есть обновления. Чтобы предоставить более подробную информацию о новых функциях в приложении, мы создаем документ или видео, которыми диспетчер доставки делится с водителями.
Через несколько месяцев к команде присоединился новый человек. Она отвечала за создание этих пояснительных документов и видеороликов, ожидалось, что они будут получены, когда версия уже будет выпущена или, по крайней мере, близка к выпуску. Новый игрок в игре!
Через несколько недель мы поняли, что нам нужно разделить два процесса, чтобы не создавать узкое место, где один из нас ждал другого. Иногда документ не был готов в понедельник, и нам приходилось откладывать выпуск до вторника или даже среды, и наоборот. Эти два процесса были тесно связаны, и нам нужно было найти решение. Это еще больше подчеркнуло необходимость использования флагов функций, и я должен сказать, что, начав их использовать, вы никогда не вернетесь назад.
Флаги функций
Когда мы начали использовать флаги функций, мы решили использовать Firebase Realtime Database. Как следует из названия, это база данных, которая передает изменения в режиме реального времени. После включения библиотеки в приложение появляется слушатель, который уведомляет об изменении значения ключа.
Использование флагов функций дало два улучшения: во-первых, у нас был способ скрыть новую функцию, если мы обнаружили ошибку в новом коде, а во-вторых, мы могли выпускать версии, даже если внутренняя пояснительная документация еще не была готова.
Как мы видим на изображении ниже, мы определяем один флаг функции для каждого склада (Vlc1 — Валенсия, Bcn1 — Барселона, Mad1 — Мадрид), поэтому мы можем показать или скрыть новую функцию на каждом отдельном складе и чувствовать себя в большей безопасности, когда ее релизим. Мы уменьшили влияние ошибок и риск.
За последние недели мы изменили способ управления флагами функций, перестали использовать Firebase Realtime Database и заменили ее нашим собственным API. Мы используем заголовки в HTTP-ответе, чтобы предоставить список с включенными флагами функций. Если флаг функции не включен в список, это означает, что функция выключена.
Используя флаги функций мы не только достигли двух из трех упомянутых ранее целей, чтобы сделать процесс выпуска более безопасным, но и устранили нашу зависимость от внутренней пояснительной документации.
Это позволило нам начать выпускать больше релизов в неделю, а не только по понедельникам.
Последние шаги: мы почти у цели!
Следующим шагом было еще больше уменьшить размер версий и автоматизировать то, что, по сути, было ручным непрерывным развертыванием — мы выпускали версии с одним коммитом для тестирования всего процесса. Через несколько дней мы изменили наш Jenkinsfile, чтобы он выпускал версию с каждым мерджем в master, и объявлял об этом всей нашей команде разработчиков!
Подводя итог эволюции приложения за последние полтора года, я хотел бы поделиться с вами некоторыми показателями за последние 12 месяцев. Как видите, количество коммитов на выпуск уменьшается, в то время как количество выпусков увеличивается.
Выводы
До:
- Релизный поезд
- Огромные версии (в начале в некоторых версиях было до 30 коммитов!)
- Нет механизма отката
- Нет флагов функций
- Зависимость от третьей стороны
После:
- Нет беспокойства о том, когда выпускать версию, каждое изменение релизится прямо сейчас
- Версии с одним коммитом
- Механизм отката
- Флаги функций
- Нет зависимости от каких-либо третьих лиц
- Более безопасная среда выпуска (иногда мы выполняем до 10 выпусков в пятницу, и это не проблема)
Может показаться, что это всего лишь несколько изменений, но я уверяю вас, что это был долгий путь, полный обучения и, конечно же, не без нескольких ошибок!
На мой взгляд, этого нельзя достичь, не зная продукт и область, над которой вы работаете. Я бы не стал начинать проект с непрерывного развертывания в начале, я бы создавал инструменты по мере необходимости. Наличие автоматизированного процесса экономит ваше время и снижает все трение, связанное с процессом выпуска. Я помню первые несколько раз, когда мне приходилось создавать и подписывать APK через Android Studio, а затем, когда он был готов, загружать его в удаленную папку, обновляя номер последней версии в службе версий, отправляя сообщение, чтобы уведомить, что есть новое обновление. Сейчас это кажется безумным объемом работы, тогда как теперь мне нужно только смерджить пул-реквест и вуаля!
С другой стороны, бесполезно иметь автоматический процесс, если позже у нас будет много ошибок и мы будем слишком медленно их решать. Мы должны чувствовать себя комфортно в том, что релизим не только быстро, но и безопасно.
Следующая задача — адаптировать наш поток для использования Google Play вместо нашей удаленной папки в Google Cloud. Вскоре мы собираемся работать с Android Enterprise и хотим использовать все возможности Google Play. Но я сохраню это для следующей статьи!
Будем на связи!
-
Новости2 недели назад
Видеозвонки с Лили, Приключения и пианино — обновления Duolingo
-
Новости2 недели назад
Видео и подкасты о мобильной разработке 2024.39
-
Видео и подкасты для разработчиков5 дней назад
Lua – идеальный встраиваемый язык
-
Разработка2 недели назад
Android сломался или я чего-то не понимаю? — Обсуждение на Reddit