От ручной сборки приложений к конвейеру: опыт бэк-офиса «Магнита»

ac2afb03fde822cc2dafa5dbbbe07b59.png

Вначале было слово… Нет. Не так…

И земля была безвидна и пуста… Нет. Снова не то…

Вначале было много разнообразной ручной работы… Уже лучше…

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

Я — Первушин Дмитрий, разработчик в управлении по развитию бэк-офиса торговых точек сети «Магнит». Основной стек — Python, Firebird, немного html/js и капелька других технологий. Моя команда занимается разработкой приложений для терминалов сбора данных, отчетов, АРМ торговых точек.

Часть первая. Описательная

Предыстория

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

Задачи поставлены амбициозные: при сборке постараться свести ручную работу к коммиту в систему контроля версий, а установку приложения в целевой системе — к одной команде. При этом минимизировать возможность внесения ошибок в процессе сборки и установки.

В процессе мы научились хранить наши приложения в git и собирать их «одним кликом», перешли от подхода «один репозиторий — одна технология» к «один репозиторий — одно приложение». Реализовали установку всех частей приложения «одной командой»; собрали грабли, разложенные в самых неожиданных местах, и создали несколько инструментов, помогающих нам на разных этапах сборки и установки приложений.

Если интересно, как и чего мы достигли на сегодняшний день, — добро пожаловать.

Предпосылки

Сборка и поставка

Приложение собиралось и поставлялось отдельными кусками: отдельно SQL, Python, отчеты, Win-приложения. Исходники разной степени актуальности хранились в разных уголках SVN, эталонных баз, GIT, и практически голыми руками собирались в пакеты для Palludis (ebuild+tbz) и SQL-скрипты для корпоративного установщика Manny.

Затем практически голыми руками писался bash-скрипт для установки пакетов Palludis и соединялся с SQL-частью. Получившийся в результате гибрид передавался в службу сопровождения для дальнейшей установки в целевые системы. Естественно, такой процесс временами давал сбои как при создании поставки — что-то не то с чем-то не тем как-то не так соединили, так и при установках — какая-то часть обновилась раньше, какая-то позже или совсем никогда, в результате приложение либо совсем не работает, либо работает как-то странно.

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

Дельты SQL

SQL-часть собиралась и ставилась как изменение относительно предыдущей версии. И это работало почти для всех пользователей, у которых реально была установлена предыдущая версия. Однако у некоторого небольшого процента в силу разных причин находились какие-то отклонения от эталонного состояния.

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

Соответственно, было желание избавиться от расхождений эталонных и реальных БД и необходимости создания откатов.

Пакетный менеджер

Для установки в целевых системах использовался Palludis с пакетами ebuild, при этом родным для целевой ОС (CentOS) является yum/dnf с форматом RPM. «Раз уж ломать, то ломать всё», — подумали мы. И решили, что переедем на RPM.

Корпоративный переезд

Помимо наших внутренних болей, у нас появилась и внешняя: сверху поступила вводная хранить исходники в GitLab, артефакты — в Nexus, а всякое старое и странное вывести из эксплуатации. 

Грабли, костыли, растяжки

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

SQL

Никто не любит SQL, а редкий диалект — совсем никто. И если структуру хранения исходников нам удалось подглядеть, то процесс трансляции исходников в скрипты наката, БД разработчика, обеспечение синхронизации приемника с источником, совместимость версий серверов БД пришлось изобретать самим. И начальная идея про «просто склеить файлы из Git — и готово!» со временем превратилась в транслятор SQL c парой фронтендов и тройкой бэкендов.

При создании транслятора познакомились с синтаксическим разбором сначала с помощью sly: сложно в разработке, LR-грамматики, shift-reduce conflicts, много теории. Классика, в общем.

Затем попробовали pyparsing с его подходом Parsing Expression Grammar — документации, скажем так, немного, теории еще меньше. После sly он оказался очень простым — без подробнейшей документации вполне можно работать, легко тестируемым — каждую конструкцию можно тестировать отдельно, затем из мелких собирать крупные; много заготовок, которыми снабдили пакет авторы. За комфорт пришлось заплатить скоростью разбора: она в пять раз меньше, чем у sly. В выборе sly vs pyparsing нас поддержало известие, что разработчики CPython тоже переходят на PEG. Похоже, что сложность классического подхода оказалась слишком большой не только для нас.

И снова SQL

При первых же установках на целевых БД начались странности с блокировками и поведением метаданных. Пришлось подкрутить параметры транзакции БД. После этого начались блокировки обычных данных. Пришлось ещё раз подкрутить. Начал падать сервер. Пришлось подкрутить, помимо транзакций, ещё и установщик.

Плюс очередной разбор ошибки разрушил мечту о накате в одну транзакцию, чтобы можно было вернуть БД в состояние «как было» просто оператором rollback. Некоторые операции, оказалось, требуют обязательного подтверждения транзакции, но их, к счастью, немного.

В общем, сплошной компромисс мечты и реальности.

RPM

Удивление вызвало качество документации для RPM. Толкового, удобного, исчерпывающего руководства по созданию RPM, особенностям работы одноименной утилиты почему-то нет. Похоже, что раз дорос до создания RPM, то разберешься как-нибудь. Вот несколько удивительных вещей:

  • Нет понятия «обновить». Есть только установить и удалить.

  • При обновлении сначала выполняется preinstall нового пакета, затем postuninstall старого. В результате можно расконфигурировать что-нибудь только что законфигурированное.

  • Доступ к файловой части из pre-скриптов отсутствует.

  • Отличить в RPM-скриптах обновление от установки и удаления можно только по параметру командной строки.

  • Обращаться к RPM-командам изнутри RPM-скриптов нельзя.

  • Остановить или хотя бы сообщить о неудаче из post-скриптов нельзя.

  • Гарантированный интерпретатор скриптов — Lua. Остальные могут и отсутствовать в целевой системе.

  • rpmbuild временами слишком интеллектуален: то зависимость ненужную добавит, то откомпилирует что-нибудь по собственной инициативе

  • Миллион rpm-макросов, которые позволяют творить и, что ещё важнее, отключать магию любого цвета, документирован только в вопросах на stackoverflow. 

YUM/DNF

В этом приложении тоже немало удивительного. Может быть, это как-то связано с RPM. Итак, удивительно, что:

  • С документацией на внутреннее устройство — беда-беда. Самый дельный пункт в ней — «смотрите код, может быть, поможет».

  • YUM и DNF изнутри совсем-совсем разные.

  • Есть много точек, куда можно подключить свой код для изменения поведения YUM на всех этапах его работы. Но единственная документация — исходники других плагинов и самого YUM.

  • DNF менее гибок, чем YUM.

  • Алгоритм выбора версии зависимости не параметризуется: всегда берем максимальную версию.

  • Нет команды «поставь уже, наконец, именно эту версию». Нужно думать, это первая установка, апгрейд или даунгрейд, и выбирать правильную команду.

  • Вывод команды RPM буферизуется до ее завершения. Если RPM-скрипт что-то пытается спросить у пользователя, то пользователь получит намертво зависший YUM.

  • YUM хранит свои плагины в специальном каталоге «плагины yum», DNF хранит свои плагины в пакетах системного Python. Как результат, просто поставить плагин для DNF простым RPM нельзя. Нужно сначала узнать, какой Python нынче системный и где же его пакеты. Природа и правда отдыхает на детях?

  • Библиотека разрешения зависимостей в DNF написана на С. Потыкать в нее отладчиком из Python не выйдет.

GitLab&Runner

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

  • Обширная, подробная, но запутанная документация. Возможно, нужно просто привыкнуть, почувствовать логику.

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

  • Оказалось невозможным указать произвольную оболочку командной строки — bash под Windows так и не взлетел.

  • Shell executor deprecated. Как же так?

  • runner иногда умеет терять свои служебные файлы с ключами. Не смертельно, но неприятно.

  • Странно работает сворачивание логов выполнения CI/CD.

Gradle

От Gradle отказались практически в самом начале и ни разу не пожалели. Помимо того, что это инструмент из параллельной вселенной Java, он тоже оказался со странностями:

  • Виртуальная машина Java использует свою версию доверенных сертификатов — системные сертификаты ей не указ. Соответственно, если вам нужен https на корпоративных ресурсах, то нужны пляски.

  • Временами Gradle требует пустить его в интернет, что тоже может быть проблемой.

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

  • Плагин, занимавшийся работой с Python, недавно официально забросили.

Часть вторая. Заглянем под капот

Используемые технологии

Разнообразие каналов взаимодействия и долгая история развития обуславливает разношёрстные подходы и технологии. Вот некоторые из них:

  • Java, Python, С/С++ — обмен данными между информационными системами как внутри магазина, так и за пределами.

  • С/С++, Delphi — десктопные приложения, используемые администрацией магазина. Их можно заметить в некоторых магазинах малых форматов.

  • FastReport, Reporter — создание, просмотр, сохранение, печать отчётов. Ценники на полках — самый заметный для покупателей результат работы этих технологий.

  • JS/TS фреймворки (Vue, React, ExtJS, …) — web-клиенты для десктопных браузеров.

  • Java/Kotlin — приложения для терминалов сбора данных (ТСД). Это «прокаченные» Android-смартфоны с аппаратными сканерами штрихкодов, клавиатурами, устойчивые к пыли, воде, ударам. Терминалы — те самые «пикалки», которые можно увидеть в руках работников в торговом зале.

  • HTML, JS — клиентская часть ТСД, работающая внутри модифицированного браузера. В частности, фронт пункта проверки цен (прайсчекеров) реализован с использованием этой технологии.

  • Java, Python — серверная часть web-клиентов.

  • Firebird SQL — база данных. Практически любая активность сотрудников и покупателей находит в ней свое отражение. 

На уровне операционных систем тоже разнообразие: CentOS и Rocky различных версий на серверах, Windows 10 на пользовательских ПК, Android различных версий на терминалах.

Сборка SQL

С точки зрения разработчика

Для сборки SQL разработчик должен соблюдать файловую структуру хранения SQL-объектов. Например, определения таблиц должны храниться в sql/tables/, а триггеров —sql/triggers/.

Для корректной установки в целевую БД часто требуется обновлять не только метаданные, определяющие структуру БД, но и производить некоторые действия с пользовательскими данными. Например, до включения ограничения NOT NULL на таблице требуется как-то обработать существующие строки с NULL-ами.

Для решения подобных задач мы создали искусственный тип SQL-объектов — «блок». Блоки выполняются при установке обновления в контексте целевой БД. В файле, помимо непосредственно объявления объекта (например, create table…), также хранится дополнительная информация: права, индексы, ограничения целостности. Хотя с точки зрения SQL-сервера индекс или право являются отдельными объектами, мы приняли решение трактовать SQL-объект чуть шире. В остальном для файловой структуры работает принцип «один файл — один объект».

Кроме непосредственно SQL-объектов, важный аспект — зависимости от других проектов, которые декларируются в файле описания проекта build/project.json. Например, можно задекларировать требуемую версию базового проекта system либо конфликт с другим проектом. Информация о зависимостях позволяет явно связать доработки нескольких проектов, выполненных при реализации одной бизнес-задачи.

Наконец, есть файл с инструкцией по сборке проекта build/sql/build_full.py. Его главная функция — определить порядок выполнения SQL-блоков относительно друг друга и изменений метаданных. Это Python-скрипт с расширенным пространством имён, заточенным под описание шагов сборки. Например, все таблицы проекта можно собрать, написав table_all ().

Помимо удовлетворения нужд автосборки, описанная машинерия может быть применена локально на компьютере разработчика. Одной командой можно скопировать SQL из локальной копии репозитория проекта в БД и IDE разработки или обратно, что значительно снижает риск что-то забыть, перепутать или испортить.

С точки зрения автосборщика

Сборка SQL происходит в два шага. На первом шаге сборщик по инструкции и декларации зависимостей создаёт единый файл наката. Этот файл не является просто склейкой отдельных SQL-файлов проекта. Для каждого файла из инструкции производится синтаксический разбор и преобразование в «установочную» форму. Эта форма служит для сокрытия различий между версиями серверов БД: у нас их два, и они в некоторых деталях несовместимы.

Сборщик также обеспечивает приведение БД к виду «как у разработчика». В частности, добавляет в установочный SQL-скрипт функции удаления объектов, удалённых при разработке. В начало скрипта добавляется блок, контролирующий соблюдение задекларированных зависимостей, в конец — сохранение версии устанавливаемого проекта.

Второй шаг создает RPM-файл для установки через пакетного менеджера. Этот файл умеет проверять соблюдение SQL-зависимостей, соединяться с целевой БД и выполнять установочный SQL-скрипт.

Сборка Python

С точки зрения разработчика

Для сборки Python-модулей файловая структура должна соответствовать требованиям сборщика: исходный код модулей должен находиться в yggdrasil/MODULE_NAME, там же находятся файлы настроек линтеров, декларации зависимостей — requirements.txt, rpm_requrements.txt. Перечень собираемых модулей находится в build/project.json. В остальном структура модуля очень похожа на пакет (package), но им не является.

Две декларации зависимостей нужны для совмещения требований линтеров, которые требуют указывать все зависимости проекта, и требований к установке модулей, которые делят зависимости на два класса. Первый класс гарантированно установлен в целевой системе и изменять его нельзя, так как он активно используется ядром web-сервера, второй класс нужен (почти) исключительно конкретному модулю. Пакеты второго класса нужно ставить при установке модуля.

С точки зрения автосборщика

Сборка Python-модулей значительно проще, чем SQL-скриптов: не требуются какие-либо сложные манипуляции для перевода модуля из состояния «исходники» в состояние «продукт». Автосборщик по перечню модулей из build/project.json производит тестирование модулей, загружает пакеты-зависимости и создает RPM-файл для установки на целевой системе модуля и его пакетов-зависимостей.

Сборка Delphi

С точки зрения разработчика

Исходники и ресурсы Delphi-приложения храним в delphi/имя_приложения, сборку активируем через build/project.json. Также в поставку можно включить дополнительные файлы (внешние ресурсы), записав их в project.json. Вот такие простые правила.

С точки зрения автосборщика

Delphi — компилируемый язык, соответственно, к сборке добавляется начальный шаг — фаза компиляции, которая выполняется на отдельной Windows-машине и выдает бинарные exe/dll.

На втором шаге exe/dll и дополнительные файлы пакуются в RPM.

Сборка отчётов

С точки зрения разработчика

Чтобы файл попал в сборку в качестве отчета, его достаточно положить в reports/. Никаких дополнительных усилий применять не нужно: оба наших движка отчётов хранят все данные в одном файле, который является и исходником, и выполняемым модулем.

С точки зрения автосборщика

Отчёты выполняются на Windows-машинах и частенько имеют русские имена по историческим, административным и другим причинам. К тому же файлы отчётов устанавливаются на linux-машины, которые применяют кодировку имён файлов, отличную от кодировки, которую ожидает Windows и Samba. Поэтому перед упаковкой отчетов в RPM сборщик «портит» имена файлов, чтобы на целевой системе русские буквы в именах выжили и радовали пользователей.

Объединяем всё под одну крышу

С точки зрения разработчика

Для разработчика существует два с половиной места объединения всего собранного в нечто единое, что мы называем поставкой. Все ранее собранные части объединяются «поставкой» и получают бизнес-ценность.

Первое место, которое определяет, что будет собрано, — файл с настройками конвейеров сборки GitLab .gitlab-ci.yml. Мы его максимально упростили за счёт вынесения скучного и объёмного кода во внешний по отношению к проекту файл. В проекте каждый этап сборки занимает ровно три строки: комментарий, что будет собрано этим этапом; имя этапа; ссылка на внешнюю функцию, непосредственно реализующую этап. Неиспользуемые этапы нужно просто закомментировать. Так, сборка Delphi-приложений довольно редкий гость.

Второе место объединения — шаблон установочного bash-скрипта build/installer.tmpl. По большей части это просто последовательное выполнение команд менеджера YUM/DNF, устанавливающих RPM-пакеты на сервера целевой системы. К сожалению, некоторые проекты требуют специфических действий во время установки и полностью унифицировать этот шаблон и вынести его за пределы проекта (в автосборщик) пока не получилось.

Третье место — файл описания проекта build/project.json. Если проект полностью соответствует типовой структуре, то никаких действий не нужно, всё объединятся автоматически. Но если проект с отклонениями — название проекта и SQL-модуля отличаются, несколько веб-модулей в рамках проекта — эти отклонения нужно описать.

С точки зрения автосборщика

Файл .gitlab-ci.yml определяет, какие функции сборки будут использованы и, соответственно, какие пакеты соберутся в результате. Функциональность по обработке .gitlab-ci.yml полностью реализуется через GitLab и GitLabRunner. Мы только создали правила запуска сборочного конвейера, его стадии, задачи, функциональное наполнение сборочных задач.

Непосредственно в задаче, объединяющей пакеты в поставку, производится обработка шаблона установки в целевую систему build/installer.tmpl. По большому счёту всё сводится к подстановке нескольких переменных (версии, названия и подобных) в текстовый шаблон.

Публикация

С точки зрения разработчика

Собрать и опубликовать проект разработчик может с помощью коммита, содержащего в комментарии команду сборки. По завершении процесса разработчик получает уведомление о результате сборки на почту и в мессенджер.

С точки зрения автосборщика

При удачном исходе сборки автосборщик получает перечень артефактов для публикации из build/project.json и публикует их в корпоративных репозиториях. Помимо «настоящих» типов rpm, pypi, raw, есть пара «виртуальных» — gitlab_tag, etalon. Виртуальные репозитории не хранят установочные пакеты в каком-либо виде, а служат, например, для отметки в системе контроля версий точки и параметров конкретного конвейера.

Инструменты, полученные при автоматизации

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

Пакет сборочных задач rpmpublic

Выполняет высокоуровневую сборку проекта. Очень сильно привязан к нашей структуре проектов. Именно он выполняет обработку build/project.json, build/installer.tmpl, знает что, куда и как положить, чтобы собрать пакет с отчётами или SQL. Представляет собой Python-пакет с интерфейсом командной строки. Практически все команды без параметров, так как подразумевают типовые действия над типовыми данными. Например, публикация осуществляется командой rpmpublic publish repo/root.

Пакет работы с SQL и БД tdb

Основная функция — работа со всеми представлениями SQL: отдельные файлы в репозитории, скрипт установки, метаданные в БД. Именно этот инструмент позволяет конвертировать SQL-часть проекта между различными представлениями, обрабатывает инструкции сборки SQL, устанавливает доработки в БД разработчика и на целевой сервер.

Инструмент работает только с БД Firebird и требует поддержку со стороны БД в виде некоторых хранимых процедур и пользовательских функций. Представляет собой Python-пакет с интерфейсом командной строки. Имеет много команд с кучей ключей и подкоманд. Например, сборка установочного скрипта из репозитория производится командой tdb script path/to/build_full.py repo-to-setup.

Пакет низкоуровневой автоматизации pycicd

Это сервисный пакет, выполняющий задачи по взаимодействию с окружающим миром, — почта, GitLab, Git, Nexus, TeamCity, pip и тому подобное. Слабо зависит от нашей инфраструктуры и вполне может применяться в других окружениях. Заточен на монопольное использование: никаких многопоточных приложений, это позволило упростить интерфейс пакета. Например, публикация RPM-пакета в репозиторий Nexus выполняется вызовом nexus.push («path_to_rpm», «https://nexus_host/nexus_repo/», «rpm»).

Плагин для YUM/DNF minstall

Этот пакет служит заменой команд install/update/downgrade в зависимости от установленной версии пакета и работает по принципу «минимально необходимой версии» — в отличие от оригиналов, которые используют принцип «максимальная версия из доступных». Плюс добавлен вывод на консоль в реальном времени. В оригинале весь вывод буферируется до окончания установки. 

Из-за радикальных отличий внутреннего мира YUM и DNF у нас получились фактически два плагина под крышей одного RPM-пакета. Точнее, три. Помимо непосредственно плагинов, в пакет включена библиотека bash-скриптов с полезными (в нашем окружении) функциями. 

Это не история успеха, скорее, история о движении к успеху. Инструменты и процессы продолжают дорабатываться по внутренним и внешним запросам. Идёт медленный перевод проектов и людей на новые рельсы. Некоторые проекты пережили уже десяток полноценных внедрений, как и некоторые люди, некоторым это только предстоит.

© Habrahabr.ru