В жизни каждого тестировщика наступает момент, когда он больше не успевает тестировать все задачи, которые на него падают. Нагрузочники не являются исключением. Сначала одна пушка, потом вторая, потом их уже десять — и все надо поддерживать и запускать на очередной версии сервиса, выкатка каждые пару часов. Времени на запуски вручную не хватает, поэтому их нужно автоматизировать. Меня зовут Саша, я работаю в команде тестирования Ozon Fintech. В прошлый раз я рассказывала о типах нагрузочного тестирования (НТ) и о том, как создавать пушки под свои нужды. Сегодня же научу запускать НТ по кнопочке в CI. Статья будет полезна тем, кто уже имеет наработки по НТ, но ещё не автоматизировал их или ищет способы запускать тесты не по крону. Наша команда финтеха в последнее время сильно разрослась. Сервисов стало много, и тестировщиков стало не хватать. Иногда стали возникать ситуации, когда разработчики просят нагрузить новую версию сервиса, но у QA на это нет времени, быстро проверить не получается.  Чтобы не зависеть от нас, команда разработки попросила создать простой инструмент, который бы позволил им самостоятельно проводить весь цикл нагрузочного тестирования из CI. Иными словами, они попросили добавить в CI кнопку, при нажатии на которую всё само работает. Вся подкапотная магия — автоматическая генерация свежих патронов, загрузка их на сервер, билд пушки, загрузка пушки на сервер, сборка актуального конфига, загрузка конфига на сервер, запуск всего этого добра из консольки на сервере — их не интересует. Мы рассматривали три варианта реализации этой идеи: Сам репозиторий для нагрузки создавался как проект-библиотека для хранения шаблонов патронов и пушек: в нём не предусматривается работающий сервис, он нигде не поднимается и не крутится. Поэтому как раз первой мыслью было сделать из него сервис, добавить в него ручки, по которым всё будет генериться и стреляться. Но это оказалось слишком сложно в рамках текущего проекта. В тестируемый сервис добавить файл с методами, которые будут обращаться к нагрузочному репозиторию как к библиотеке, всё создавать и запускать тесты. Но тогда владельцам самого тестируемого сервиса надо будет за всем следить. Это расходится с принципом «Просто нажать на кнопку — и оно само будет работать». Создать триггеры для нагрузки, по которым на стороне нагрузочного репозитория с помощью простых команд будет выполняться алгоритм всей этой генерации и запуска нагрузки, а к тестируемому сервису этот триггер будет просто подключаться. В GitLab можно создавать многопроектные пайплайны, когда действие в одном пайплайне запускает пайплайн в другом проекте. Триггер — это тип джобы, которая вызывается в одном проекте, но запускает выполнение действий в другом.  В нашем примере будет два сервиса:  Сервис, который мы тестим (в терминологии триггеров — upstream-сервис), назовём его SUT (system under test). Сервис НТ, который стреляет (он же downstream-сервис), назовём его LOAD.  Так как много всего завязано на ветки гита, то в нашем примере у SUT ветка будет называться my-upstream-branch, а у LOAD — my-downstream-branch. В рамках примера мы хотим, чтобы по триггеру из проекта SUT ветки my-upstream-branch запускался пайплайн в проекте LOAD ветки my-downstream-branch.  Таким образом, для создания многопроектного пайплайна у нас есть три элемента: тесты, которые мы гоняем (LOAD); триггер, по которому они запускаются; сервис, который использует этот триггер, чтобы запустить тесты (SUT). В коде это выглядит немного иначе: Есть тесты, которые мы запускаем через обычный пайплайн, описанный в gitlab-ci. Есть файл, который содержит описание джобы с триггером. Есть gitlab-ci в тестовом сервисе, который наследует эту джобу. Приступим! В LOAD создаём триггер в отдельном файле .trigger.yml: Обязательно пушим изменения на сервер, иначе SUT не даст сделать изменения в себе. В SUT добавляем шаг, в котором наследуется джоба с триггером, в .gitlab-ci.yaml: и тоже пушим. Важно: в SUT в include:ref прописывается ветка, из которой мы будем считывать настройки триггера; в LOAD в trigger:branch указывается название ветки, прогон которой будет активирован. Еще раз: в .gitlab-ci.yml сервиса SUT указывается ветка, из которой брать условие запуска тестов, а в .trigger.yml сервиса LOAD — кодовая база для прогона.  Таким образом, у нас появляется кнопка для запуска LOAD-тестов в пайплайне. Кнопка есть, но никакие действия для downstream-проекта не прописаны. В описание джобы с триггером даже нельзя закинуть script для выполнения, ведь такого ключа нет в списке разрешённых. В случае попытки добавить это ключевое слово вылезет ошибка:  CI lint invalid: [jobs:lnl tests config contains unknown keys: trigger] Что же тогда будет выполняться?  Суть в том, что триггер запускает пайплайн целиком, а не конкретную задачу. Причём пайплайн ветки, заданной в описании триггера .run_load_tests:trigger:branch: my-downstream-branch. Если в gitlab-ci нет никаких шагов, то вылезает такая ошибка: Поэтому следующим шагом добавляем и сам .gitlab-ci.yml, если его ещё нет, и шаги в него, например: В результате при использовании триггера срабатывает пайплайн из нужной ветки: Дальше закидываем в .gitlab-ci.yml все стейджи и шаги, которые нам нужны для запуска пушки (полные файлы будут в конце статьи): Такой хардкод с названием сервиса service=sut подходит для одного тестового сервиса, но мы тут делаем универсальный инструмент. Один и тот же пайплайн хочется запускать как из сервиса sut, так и из сервиса another-sut, у которых под капотом будут разные патроны, разные пушки, да просто разные адреса и условия нагрузки.  Для этого настройки пайплайна на стороне LOAD будем определять через переменные окружения. В триггер будем передавать CI-переменную ${CI_PROJECT_NAME}, на основании которой изменять настройки: А в .gitlab-ci.yml добавим ещё один шаг aim, на котором будем определять, куда стрелять: Важно обратить внимание на необходимость артефактов и на переменные окружения.  Переменной TEST_SERVICE присваивается значение ${CI_PROJECT_NAME} на уровне upstream-проекта; в downstream-проекте она является переменной окружения и доступна со своим значением из любого места в пайплайне. А вот переменные, которые мы определяем в .load-tests:rules:variables, являются локальными и доступны только в рамках шага, выполняемого в этот момент. Поэтому их надо записать в артефакт dotenv, а в последующих шагах этот артефакт считывать:   На текущий момент в пайплайне нет никаких ограничений на запуск. Грубо говоря, при разработке новой фичи на каждый коммит будет триггериться падающий пайп, потому что в шаге .load-tests нет правила для нашего репозитория. Значение $TEST_SERVICE в рамках запуска проекта LOAD равно пустой строке ””. Каждый коммит будет сопровождаться вот таким безысходным зрелищем: Поэтому можно добавить ещё правил для пайпа с нагрузкой — запускать его, только если сработал триггер. Для этого в триггер добавляем ещё одну переменную LOAD_PIPE: "true": А на шагах, которые мы хотим запускать по триггеру, добавляем условие Это позволяет добавить пайплайн для разработки. Ведь в нашем проекте НТ тоже должны быть тесты и линтер, которые повышают качество кода, и мы хотим их запускать на всех ветках, кроме мастера. Например: НТ не требуется в каждом возможном коммите. В наших проектах мы решили его запускать уже после релиза на стейдж-окружение. То есть сначала код должен пройти юнит-тесты, функциональные тесты, ревью на merge request, интеграционные тесты — несколько проверок, позволяющие с большой уверенностью предположить, что изменения в коде сделаны нормально. Всё-таки НТ — ресурсозатратный процесс, с помощью которого хочется проверять более или менее готовый продукт. Поэтому в триггер мы добавили ограничение на тип веток: НТ может быть запущено только в релизных ветках на стадии пост-деплоя, то есть когда сервис уже раскатан, и только вручную: Чтобы иметь возможность запустить downstream-джобу, надо иметь достаточно прав для запуска пайплайна в downstream-проекте. То есть людей, которые будут запускать пайплайн с тестами, необходимо добавить к себе в репозиторий с необходимыми правами. Напомню, что в изначальной концепции был акцент на простоте — пользователям тестируемого сервиса нужно было делать минимум телодвижений или минимум изменений в своём проекте. Поэтому на уровне тестируемого upstream-проекта мы просто добавляем шаг, который наследует триггер. В проекте LOAD пишем весь код для функционирования пушки, добавляем его в мастер — и после этого считаем, что актуальный код для НТ лежит там и нигде больше. В проекте LOAD: Описание джобы load-tests берётся из '/.universal-ci.yml', который указан в include в начале файла. В сервисе SUT мы дополняем только .gitlab-ci.yml: Если нужно на уровне SUT переопределить, например, TARGET, то это можно просто добавить в описание джобы: На выходе получаем систему с таким устройством: Как вы могли заметить, в .gitlab-ci.yml в шагах генерации патронов, конфигов и пушек скрипты создания самих сущностей указаны верхнеуровнево. Для того чтобы все эти генерации работали для всех проектов, нужно и пушки с генераторами патронов сделать универсальными. Обе эти вещи выходят за пределы сегодняшней темы, поэтому и описаны без подробностей. Триггеры позволяют быстро автоматизировать процесс добавления функционала НТ в конечные сервисы. Регулярные обстрелы помогают определить, не уменьшилась ли пропускная способность сервиса, не ломаются ли интеграции с соседними сервисами при длительном взаимодействии, насколько оптимально работает железо. При таком встраивании НТ в жизненный цикл сервисов удаётся сэкономить время на генерации патронов и запуске вручную, а также тестировать сервис сразу после выкатки, а не ждать ночных стрельб по крону (если они вообще есть). Но такие строгие шаги в пайплайне требуют единообразия всех генераторов. Патроны, пушки, конфиги должны создаваться одинаковым образом для разных сервисов, иметь консистентную структуру файлов. С другой стороны, это позволяет автоматизировать и их генерацию, а разработчику остаётся самое приятное — использовать своё воображение и реализовывать свои навыки под всей этой инкапсуляцией. В этой статье я постаралась рассказать, как безболезненно встроить НТ в свой проект, как настроить и репозиторий с пушками, и репозиторий с тестируемым сервисом. Теперь вам известно, какие проблемы могут возникнуть при таком подходе и как их избежать. Надеюсь, моя статья облегчит вам жизнь и поможет быстрее настроить окружение для НТ, автоматизировать запуск тестов и улучшить качество ваших проектов. Stay tuned!