Всем привет, меня зовут Александр Данковцев, я lead engineer команды Antimonolith. В этой статье я расскажу, как построен CI/CD монолита Авито. Речь пойдёт про нашу архитектуру стейджинга, pre-receive хуки, то, что из себя представляет сборка и деплой, как устроен прогон автотестов и какие проверки происходят на merge. А ещё рассмотрим after-merge actions. Перед началом повествования введу пару понятий, которые будут использоваться дальше: Релиз-кандидат — версия кода, которому предстоит пройти процесс валидации тестов и деплой в продакшн при успехе. Срез релиз-кандидата — Git-ветка, созданная от мастера в процессе запуска сборки релиза. Начнём со схемы архитектуры стейджинга. Это то, где вращается сам сайт Авито и компоненты, из которых он состоит: Авито сайт, как и любой другой микросервис в стейджинг-окружении, деплоится в стейджинговый Kubernetes-кластер. У нас есть отдельный namespace avito-site-tests, в котором расположены ресурсы сайта: базы данных, баунсеры, сервис очередей, Sphinx, прочие репликации, всякие демоны. Это сделано для того, чтобы экономить ресурсы в кластере. Непосредственно каждая ветка сайта деплоится в собственный отдельный namespace, и все бэкенды натравливаются на ресурсы, которые расположены в отдельном неймспейсе. Особняком стоит Frontend Delivery Network (FDN), куда сгружается статика сайта. Эта инфраструктура очень сложно деплоится: раскатить весь стейджинг одной кнопкой нельзя. Поэтому стейджинг поделен на Helm-релизы.  Отдельным релизом мы катим сами ресурсы, потому что это требуется очень редко. Ресурсы — это база данных, Sphinx, Redis-ы, RabbitMQ. Также отдельно по срезу релиз-кандидатов или по требованию обновляются демоны монолита. На схеме это PGQ daemons, DataBus consumers и QaaS consumers. Мы вынесли их в отдельный деплой, чтобы перераскатка не пересоздавала базу данных, потому что база достаточно большая (около 10 Гб) и её пересоздание — это сброс всех данных, созданных в процессе её работы, т. е. пропадут все созданные объявления и прочие ресурсы. Мы получим неконсистентное состояние с другими сервисами. Деплой ресурсов стартует в 7 утра, когда все разработчики ещё спят, и к утру у нас есть готовая инфраструктура. Это позволяет нам тестировать в том числе и асинрохронное взаимодействие компонентов сайта. После раскатки ресурсов происходит пересоздание релиза кронов, демонов, консумеров. Каждый сайт из второй колонки схемы деплоится по отдельности. Грубо говоря, это происходит по пушу в ветку: мы закоммитили, запушили, и ветка собралась в отдельный namespace. Отдельно деплоится статика, об этом расскажу позже. Каждый бэкенд сайта — Kubernetes Pod — состоит из пяти контейнеров:  Nginx — это точка входа плюс некоторая отдача статики.  Php-fpm — сам бэкенд.  Envoy-core, который отвечает за прокси в сервисы, выполняет роль DNS resolve и keepalive. Например, мы можем стучаться по http://localhost:8888/service-item и попасть в service-item. Это всё нужно для большей производительности стейджинга.  Netramesh добавляет контекст в исходящие запросы: заголовок X-Source. Он также выполняет роль динамической подмены upstream-а сервисов по входящему заголовку X-Route. Формат такой: X-Router: src_host:src_port=dst_host:dst_port. Navigator — межкластерный маршрутизатор, который резолвит ClusterIP в набор PodIP во всех доступных дата-центрах. Посмотрим на сетевой стек стейджинга. Когда мы запрашиваем произвольную ветку сайта, запрос проходит через фронденд nginx. В нашем случае это какой-то из подмножества avi-http, который отправляет запросы на Ingress. С Ingress мы попадаем в routing-gateway. Routing-gateway занимается подмешиванием заголовка X-Route и дальше прокидывает запрос в API-gateway. API-gateway балансит, куда отправить запрос: либо это будет Avito Site, либо какой-нибудь API-сomposition. API-сomposition — это десктоп-сайт на Node.js или гошный сервис, либо любой другой сервис на любом другом языке. Когда мы в общих чертах поняли, как выглядит стейджинг, рассмотрим quality gates, которые применяются для Avito Site.  Основные этапы прохождения коммита таковы:  Гитовые pre-receive хуки. TeamCity: CI/CD прогоняет тесты, линтеры и так далее.  Проверки на merge, флаги и обязательности. Действия после merge. Давайте же покоммитим в монолит. Предположим, мы решили что-то поправить.  Pre-receive хуки помогают проверить, всё ли в порядке с коммитом до его пуша в репозиторий. Таких хуков у нас не очень много. Мы проверяем, чтобы каждый коммит содержал имя задачи в Jira. Также есть элементарные проверки синтаксиса. Допустим, если где-то точку с запятой поставили не так, pre-receive хук скажет, что допущена синтаксическая ошибка. Мы проверяем и стиль, например, если пропущен нужный пробел между операторами, линтер сообщит об этом и не даст запушить ветку. Плюс есть проверка на то, что коммит — наш доменный, он принадлежит Авито, а не какой-то левый. Вот максимально упрощённый граф зависимостей нашего CI/CD, я собрал основные этапы прохождения: У нас есть всякие схема-чеки, DI-чеки, юнит-тесты, но лишь вершина айсберга. Из всей схемы я рассмотрю самый основной и сложный процесс — прохождение end-to-end тестов от сборки Docker до прогона тестов.  Перед тем, как собирать код сайта, мы подтягиваем мастер. Это нужно, чтобы избежать shadow-коммитов и отловить на ранних стадиях ситуации, когда одновременно разрабатывались две ветки, и кто-то замерджил конфликтующую логику. После подтягивания мастера происходит непосредственно сборка и потом пуш образа. В сборке образа мы используем паттерн build-образ и push-образ. Build-образ — большой и тяжёлый, с кучей зависимостей, сишных библиотек и прочих утилит, которые нужны, чтобы собирать непосредственно код. Для рантайма же этих зависимостей не должно быть. То есть у нас собирается некоторый артефакт на этапе build code, после этого сбилженная статика загружается в Ceph. А остальная часть собранного кода запекается в Docker-образ и позже пушится. Вот как происходит build code:  Установка composer-зависимостей. Кодогенерация, например, генерация client-shop.  Сборка DI, у нас используется Symfony. Сборка фронденда. Собирается npm, собирается Twig в кэши.  Деплой хранимых процедур. Мы получаем свободного юзера, назначаем ему схему, search pass, и накатываем туда хранимки. Сборка словарей — файловое представление справочных данных из базы или сервисов.  Если всё прошло успешно, последний этап — сборка артефактов, то есть генерация app.toml, swagger, rev.txt. Rev.txt — это идентификатор сборки, окружение, в котором она собиралась, и прочая отладочная информация. Когда образ собрался и запушился, приходит следующая TeamCity сборка — деплой.  Процесс следующий:  Деплой в Kubernetes-кластер.  Валидация доступности хоста: делается curl с небольшим прогрессирующим шагом, пока хост не станет отдавать код 200. Helm предоставляет информацию о том, что он успешно раскатился, проходят health-чеки и так далее.  Регистрация хоста в routing-gateway. Автоматическая отбивка в Slack, о том, что хост собрался. Хост собрался, самое время запускать end-to-end тесты. Если говорить простым языком, у нас сначала отрабатывает сборка Build E2E. В отдельный архив собирается папка с тестами и пушится в Artifactory. От E2E-тестов идёт много связей, я приводил их на первой картинке про TeamCity.  Для простоты рассмотрим одну из них — E2E Test Suite. В этом билде настроено, какой тип тестов запускать, допустим, web или api, или заданы какие-то дополнительные параметры, например, относящиеся к конкретному юниту Авито. Этот билд общается с сервисом параллельного запуска автотестов: посылает ему задачу, сервис её исполняет и получает ответ.  Если копнуть немного глубже, то билд, прогоняющий тесты, представляет собой TeamCity meta runner, который через Docker запускает небольшое приложение, где реализован клишный parallel-client. Parallel-client делает запрос в parallel manager и передаёт ему все метаданные сборки: какие тесты к какому юниту относятся, какой артефакт был запушен в Artifactory. Parallel manager, получив результат, перекладывает всё в очередь и отвечает клиенту, что «всё окей, я принял результат». После этого клиент начинает периодически поллить parallel manager на информацию о том, прошли тесты или нет. Когда задача поставлена в очередь на исполнение тестов, подключается parallel worker. Воркер вычитывает очередь заданий, которые ему поставили для выполнения тестов, и находит в данных мета-информацию о месте хранения этих тестов. В мета-информации есть ссылка на скачивание архива, который на раннем этапе был загружен в Artifactory. Воркер скачивает себе архив с тестами и запускает command для их прогона.  Тесты ходят в Selenium, который ходит по сайту через Firefox, Chrome или любой другой браузер. Тесты также пользуются файловой системой (fs-qa), куда сохраняют скриншоты, html-странички и прочее. На результат выполнения смотрит воркер: для успеха код ответа — 0, для провала — 1.   После этого parallel worker пушит каждый выполненный кусочек задачи в отдельную очередь «результаты выполнения тестов». Эту очередь слушает parallel manager. Когда менеджер получает результаты, он отгружает их в tests reporters backend, где хранятся отчёты о том, что такой-то тест столько-то выполнялся и прочая информация. Parallel client через cli-команду запрашивает у test reporter backend финальную информацию о том, что все тесты пройдены. В ней отмечено, сколько тестов прошли, например, 150 из 170. На основе этой информации билд становится зелёным или красным.  Также после получения этой информации в TeamCity создаётся артефакт со ссылкой на frontend test reporter. Мы можем зайти в него из TeamCity и визуально посмотреть полную отчётность о том, какие тесты прошли и сколько они выполнялись. Вот как весь процесс выглядит схематически: Каждый Е2Е-тест создаёт свою коллекцию тестов, а каждая коллекция тестов создаёт свои репорты, и их довольно много. На прогоне ветки для сайта у нас получается семь Е2Е-сьютов, а для релиза их порядка 50 штук. Когда все отчёты собрались и пришли отбивки в stash, мы можем зайти в stash и замерджить ветку. Помимо стандартной проверки на наличие апрува и свалившиеся или не свалившиеся билды, мы используем свой кастомный плагин, который проверяет, можно ли мерджить ветку в мастер.  Merge-плагин анализирует diff, то, какие файлы были изменены, и выставляет флаги для текущего pull request. Если в pull request были изменения PHP-файлов, он ставит backend changes, если были изменения JS или CSS — frontend changes. Маркируются и случаи, когда произошли изменения в папке с Е2Е-тестами, например, buyer test changes. В итоге работы плагина у pull request появляются флаги, что в нём потрогали: frontend, backend и тесты байеров. Наименование флага Значение Условие Backend changes Yes *.php Frontend changes No *.css, *.js Buyer E2E changes No tests/e2e/BuyerTests/* DataBase changes Yes *.sql Также у нас есть карта всех билдов, которые мы хотим валидировать на pull request Avito Site. Но мы не хотим, чтобы каждое изменение в read.me порождало требование пройти 30 проверок. Мы хотим, чтобы если потрогали какой-нибудь текстовый файлик, было наличие только одного апрува. Это достигается тем, что каждая проверка маркируется списком флажков, на которые она должна срабатывать.  Наименование линтера Список флажков UnitTests Backend changes FrontendCI Frontend changes DockerBuild Backend changes, Frontend changes, Database changes BuyerE2E Suite Buyer E2E changes Например, для "BuyerE2E Suite" проверка прогоняется в том случае, если на pull request был выставлен флаг "Buyer E2E changes". Если мы не трогали папку с байерами, то проверки не будут обязательными, на pull request не будет требоваться, что они должны быть зелёными. Если потрогали SQL-ки, значит, обязательно должны пройти интеграционные тесты на базе, и так далее. Когда все тесты прошли, все условия соблюдены, все нужные галочки стоят, мы наконец-то можем слить ветку в мастер. Но здесь всё не останавливается. После этого у нас срабатывает механика after-merge.  Её реализует плагин в нашем stash, который слушает изменения на merge и триггерит соответствующие сборки в TeamCity. Прежде всего, он делает удаление хоста, что экономит нам ресурсы: Kubernetes не резиновый. Ceph тоже не резиновый, поэтому после мерджа происходит очистка статики. И в конце плагин снимает регистрацию с routing-gateway.  Теперь всё, мы в мастере. В статье я не стал рассматривать релизы Авито. Они, в принципе, очень похожи на описанный выше процесс. В релизах просто происходит не pull request в мастер, а срез ветки. В остальном смысл тот же: сборка сайта, прогоны Е2Е-тестов, просто их больше, а вместо проверки в stash на merge-check — валидация релиз-инженерами. Lead Engineer