Предисловие
Привет, Хабр, я QA инженер в команде интеграции. Моя команда интегрирует локальных платежных провайдеров, чтобы наши клиенты могли заводить и выводить деньги. Из-за большого количество провайдеров у нас возникла проблема с деплоем сервисов на нужную ветку для тестирования. В этой статье, я представлю наше решение этой проблемы в виде Slack бота и не только. Надеюсь кому-то это тоже поможет решить подобную проблему.
Описание контекста и проблем
Как я и сказал, мы интегрируем платежных провайдеров. Каждый провайдер - это отдельный сервис, в котором заключена реализация взаимодействия с провайдером. Кодовая база всех сервисов располагается в одном репозитории, где есть общая логика и отдельные реализации конкретных провайдеров, которых мы называем драйверами. Автотесты и мок, который содержит необходимые примеры ответов от реальных провайдеров и тестовые кейсы, располагаются в отдельных репозиториях. Сервисы располагаются в Kubernetes, а управление настройками и версионностью сервисов идет через Helm charts. На данном этапе у нас 2 окружения: Prod и Dev и 60+ сервисов.
В команде 8 разработчиков и 3 тестировщика. Все мы работаем в одном тестовом окружении, в котором можно запустить только одну версию каждого сервиса, поэтому мы прописываем нужный tag/branch сервису в репозиторий gitops. По сути, это монорепа с отдельной версионностью для каждого сервиса. После работы над сервисом, мы конечно же забываем вернуть его на основную ветку. Каждую ночь у нас запускается smoke прогон, а в выходные большой regress, и как результат, многие тесты красные, так как сервисы задеплоены на dev ветки, которые еще не смержены. Как итог, приходится тратить много времени на разбор падений, которые в итоге не релевантны, так как тестировалась неактуальная ветка.
Следующая проблема которая была обнаружена - это фиксы базовой логики. Данные изменения затрагивают либо все сервисы, либо большое их количество. Чаще всего разработчику лень деплоить такое количество сервисов и он тестирует свои изменения на нескольких, что, конечно, недос��аточно, ведь не раз после этого, мы ловили баги на проде от тех сервисов, которые не были протестированы перед релизом.
Еще одна минорная проблема - это сложность запуска тестов через CI. Во-первых, чтобы запустить тесты на основной ветке в репозитории с автотестами в Gitlab CI, нужно иметь права на мерж, и не хочется всем их выдавать. Также не всегда очевидно какие и как правильно запустить тесты для тех или иных сервисов, для этого каждый раз приходится спрашивать тестировщика и отвлекать его от текущей задачи. Вместе с нужной веткой тестов, необходимо так же задеплоить мок сервис.
Все это создает трудности перед командой, что увеличивает шансы не запустить тесты и зарелизить сервис с багами.
После обсуждения этих проблем и пары серьезных багов с прода, мы сформулировали наши пожелания и требования к решению.
Что мы хотим:
Повысить стабильность и актуальность тестов Smoke (ночные) и Regress ( еженедельные) прогонов
Облегчить деплой нужной версии драйвера и мока, и запуск только тех тестов, чьи сервисы были затронуты
Сделать удобный интерфейс, чтобы не нужно было заходить в CI по стандартным случаям
Требования к решению:
Обновление или деплой сервиса и мока на основную ветку перед Smoke и Regression тестированием
Откат сервисов на текущую ветку после тестирования
Деплой одного или списка сервисов и мока на нужную ветку и запуск тестов для этих сервисов
Сделать интерфейс, что бы не заходить в Gitlab CI
Реализация
В итоге получилось решение состоящее из 3-х частей:
Python скрипты для деплоя и определения скоупа для тестирования
Gitlab pipeline, который управляет деплоем сервисов, запуском тестов и откатом
Slack bot в виде сервиса, который реализует необходимый интерфейс
Скрипт по управлению версионностью сервисов - это первая часть решения. Как было упомянуто раньше, конфигурация сервисов хранится в git, конфиг flux. Чтобы задеплоить сервис с нужной версией, необходимо отредактировать файл соответствующего сервиса в этом репозитории, закоммитить и запушить.
Скрипт использует особенность flux конфига, а именно kustomization patchesStrategicMerge. Эта стратегия подразумевает, что нижняя структура переопределяет настройки верхней. Для этого был создан файл “ci.yaml” в директории, где располагаются настройки сервисов для конкретного неймспейса. Чтобы это файл считался самым нижним и имел финальный приоритет настроек, необходимо прописать его в “kustomization.yaml”.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: psp
resources:
- ../../base/cards/psp-driver1
- ../../base/psp/psp-driver2
patchesStrategicMerge:
- psp-driver1.yaml
- psp-driver2.yaml
# Autotest automation, must be the last one in patches list
- ci.yaml
После мы определили формат шаблона настроек, который будет записываться в файл “ci.yaml”.
KUSTOMIZATION_TEMPLATE = """
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: {name}
spec:
values:
image:
tag: {tag}
env:
X_UPDATE: {serial}
"""
Элементы, которые вставляются в этот шаблон при записи:
Name - это имя сервиса, к которому будет применяться данные настройки
Tag - это тэг или имя ветки, на которую нужно будет задеплоить сервис
X_UPDATE: {serial} - это переменная окружения, в которую записывается случайный набор цифр. Она не влияет на работу сервиса, но помогает в его деплое.
Еще одна особенность работы flux, в том, что если он видит, что новые настройки не отличаются от предыдущих, то сервис не будет обновлен. В итоге, X_UPDATE, со случайным набор цифр, делает принудительную разницу в настройках и “заставляет” kubernetes задеплоить сервис на нужную ветку. Это дает нам возможность подгрузить свежую версию текущей ветки, если сервис давно не редеплоился.
В итоге, скрипт на вход получает список сервисов и тэг(ветку). Заполняет и записывает шаблон для каждого сервиса в файл “ci.yaml”.
Но мало просто запушить настройки, нужно еще понять когда они применились. Поэтому в скрипте была реализована работа с API kubernetes, которая получает список сервисов и ожидает когда они перезагрузятся и будут готовы.
Откат изменений тоже происходит через этот скрипт. Тут все гораздо проще. Скрипт очищает файл “ci.yaml” и ждет когда список сервисов перезагрузится и будут готовы к работе. Когда “ci.yaml” очищается , то применяются те настройки, которые прописаны в верхних файлах, тем самым, после тестового прогона, можно спокойно продолжить работу, без необходимости возвращать сервис, например, c основной на свою dev-ветку руками.
Вторая часть решения - это Gitlab pipeline. Он был реализован в репозитории с тестами.

Полный шаблон деплой джобы
.gitops_tests_deploy:
timeout: 40m
image: $CONTAINER_RELEASE_IMAGE
variables:
# required variables (aka arguments)
_NAMESPACE: ''
_DRIVERS: ''
_TAG: ''
_DEPLOY_JOB_NAME: ''
before_script:
# use before_script to add behavior
script:
- !reference [.enable_debug, script]
# check arguments
- if [[ -z $_NAMESPACE ]]; then exit 1; fi
- if [[ -z $_TAG ]]; then exit 1; fi
- if [[ -z $_DRIVERS ]]; then exit 1; fi
- if [[ -z $_DEPLOY_JOB_NAME ]]; then exit 1; fi
- !reference [.setup_update_drivers_variables, script]
- !reference [.setup_git, script]
- !reference [.fix_driver_names, script]
- git clone -b $GITOPS_BRANCH $GITOPS_REPO_URL gitops
- cd gitops
- 'echo "Updating drivers: $_DRIVERS"'
- $SCRIPTS_DIR/update-drivers.py update "$_NAMESPACE"
--tag "$_TAG"
--drivers "$_DRIVERS"
--env "$UPDATE_DRIVERS_ARG_ENV"
- git add apps && git commit -m "[AUTOTEST] [$_NAMESPACE] Update $_DRIVERS to $_TAG"
- git pull origin $GITOPS_BRANCH --rebase
- |
for try in $(seq 1 5); do
if git push origin $GITOPS_BRANCH; then
break
else
git pull origin $GITOPS_BRANCH --rebase
fi
done
- $SCRIPTS_DIR/update-drivers.py wait "$_NAMESPACE"
--tag "$_TAG"
--drivers "$_DRIVERS"
--env "$UPDATE_DRIVERS_ARG_ENV"
- echo "$_DEPLOY_JOB_NAME=1" >> $CI_PROJECT_DIR/build.env
artifacts:
expire_in: 1 days
reports:
dotenv: build.env- git clone -b $GITOPS_BRANCH $GITOPS_REPO_URL gitops
- cd gitops
- 'echo "Updating drivers: $_DRIVERS"'
- $SCRIPTS_DIR/update-drivers.py update "$_NAMESPACE"
--tag "$_TAG"
--drivers "$_DRIVERS"
--env "$UPDATE_DRIVERS_ARG_ENV"
- git add apps && git commit -m "[AUTOTEST] [$_NAMESPACE] Update $_DRIVERS to $_TAG"
- git pull origin $GITOPS_BRANCH --rebase
- |
for try in $(seq 1 5); do
if git push origin $GITOPS_BRANCH; then
break
else
git pull origin $GITOPS_BRANCH --rebase
fi
В шаблоне идет скачивание GitOps репозитория, вызов скрипта по деплою. В шаблон на вход подаем:
NAMESPACE - содержит имя директории в структуре Gitops репозитория. Так как мок и сервисы лежат в разных “директориях”, тем самым мы указываем в какой именно “ci.yaml” нужно внести изменения
TAG - тэг или имя ветки, на которую нужно задеплоить сервис
DRIVERS - это список драйверов\сервисов. DRIVERS может в себе содержать как имя одного сервиса, так и список, также может содержать значение “all”, тогда скрипт считывает из Gitops репозитория имена всех сервисов и подает их на вход функции деплоя
Скрипту подается команда "update", по которой он обновляет “ci.yaml” файл. Далее, джоба пытается пушнуть изменения в репозиторий. Потом скрипту подается команда “wait” с аналогичным набором аргументов, и он ждем когда все сервисы задеплояться и будут готовы к тестированию.
Этот шаблон применяется для джоб по деплою сервисов и мока. Отличие только в списке имени сервиса(ов), имени ветки и NAMESPACE, так как мок лежит отдельно от основных сервисов.
Пример ввыода джобы деплоя
...
Skipping Git submodules setup
Executing "step_script" stage of the job script
02:08
$ if ! docker info &> /dev/null ;then echo non_docker_environment ; else mkdir -p /root/.docker/; echo $DOCKER_AUTH_CONFIG > /root/.docker/config.json; docker login ; fi
non_docker_environment
$ if [[ -n "$ARG_TEST_SCOPE" ]]; then # collapsed multi-line command
$ if [[ "$ENABLE_DEBUG" == 1 ]]; then set -x; env; fi
$ if [[ -z $_NAMESPACE ]]; then exit 1; fi
$ if [[ -z $_TAG ]]; then exit 1; fi
$ if [[ -z $_DRIVERS ]]; then exit 1; fi
$ if [[ -z $_DEPLOY_JOB_NAME ]]; then exit 1; fi
$ [[ "$CLUSTER" == "gt-stage" ]] && export UPDATE_DRIVERS_ARG_ENV="gt-stage-rke"
$ git config --global user.email "<>"
$ git config --global user.name $GITOPS_GIT_NAME
$ _DRIVERS=$($SCRIPTS_DIR/translate_drivers_names.py "$FIX_DRIVER_NAME" "$_DRIVERS")
$ git clone -b $GITOPS_BRANCH $GITOPS_REPO_URL gitops
Cloning into 'gitops'...
$ cd gitops
$ echo "Updating drivers: $_DRIVERS"
Updating drivers: driver1
$ $SCRIPTS_DIR/update-drivers.py update "$_NAMESPACE" --tag "$_TAG" --drivers "$_DRIVERS" --env "$UPDATE_DRIVERS_ARG_ENV"
$ git add apps && git commit -m "[AUTOTEST] [$_NAMESPACE] Update $_DRIVERS to $_TAG"
[develop fbea0174a] [AUTOTEST] [psp] Update friver1 to master
1 file changed, 11 insertions(+)
$ git pull origin $GITOPS_BRANCH --rebase
From https://git.ops.gt.env/devops/platform/gitops
* branch develop -> FETCH_HEAD
Current branch develop is up to date.
$ for try in $(seq 1 5); do # collapsed multi-line command
remote:
remote: To create a merge request for develop, visit:
remote: https://git.ops.gt.env/devops/platform/gitops/-/merge_requests/new?merge_request%5Bsource_branch%5D=develop
remote:
To https://git.ops.gt.env/devops/platform/gitops.git
c16e890fb..fbea0174a develop -> develop
$ $SCRIPTS_DIR/update-drivers.py wait "$_NAMESPACE" --tag "$_TAG" --drivers "$_DRIVERS" --env "$UPDATE_DRIVERS_ARG_ENV"
INFO:update-drivers:waiting for kustomization to apply using k8s API
INFO:update-drivers:waiting for 1 driver(s):
psp-driver1
INFO:update-drivers:[1/1] psp/psp-driver1 has been updated to master
INFO:update-drivers:All drivers have been updated successfully
$ echo "$_DEPLOY_JOB_NAME=1" >> $CI_PROJECT_DIR/build.env
Uploading artifacts for successful job
00:01
Uploading artifacts...
build.env: found 1 matching files and directories
Uploading artifacts as "dotenv" to coordinator... ok id=10567162 responseStatus=201 Created token=64_WEHMH
Cleaning up project directory and file based variables
00:01
Job succeededДалее идет вызов джобы тестов.
- |
if [[ -n "$ARG_TEST_SCOPE" ]]; then
export PYTEST_TEST_ARGS="-m $ARG_TEST_SCOPE"
if [[ "$IS_CUSTOM_MARKER" == 1 ]]; then
export PYTEST_TEST_ARGS=$PSP_MARKER
fi
else
ARG_DRIVERS=$($SCRIPTS_DIR/translate_drivers_names.py "$FIX_DRIVER_NAME" "$ARG_DRIVERS")
export PYTEST_TEST_ARGS=$($SCRIPTS_DIR/translate_drivers_names.py "$DRIVER_NAME_TO_TEST_PATH" "$ARG_DRIVERS")
if [ -z "$PYTEST_TEST_ARGS" ]; then
export PYTEST_TEST_ARGS=$PSP_MARKER
fi
fi
- cd "$PSP_TEST_PATH"
- 'echo "pytest arguments: $PSP_TEST_ARGS $PYTEST_TEST_ARGS"'
- echo "Selenium URL $SELENIUM_EXECUTABLE_PATH"
- >
if [[ -n "${SLACK_WEBHOOK}" ]];
then
$SCRIPTS_DIR/slack.sh
else
echo "WARN: SLACK_WEBHOOK variables is not set";
echo "WARN: Slack notifications are disabled";
fi
if [[ "$ENABLE_ALLURE" == 1 ]]; then
echo 'Running tests with upload an allure report to Allure TestOps'
/allurectl watch -- $(which pytest) \
--slack_hook=${SLACK_WEBHOOK} \
--slack_username="Testing results" \
--slack_report_link=${CI_JOB_URL} \
--alluredir=$ALLURE_RESULTS \
$PSP_TEST_ARGS $PYTEST_TEST_ARGS
else
echo 'Running tests with pytest save N allure repost to artifacts'
pytest $PSP_TEST_ARGS $PYTEST_TEST_ARGS --alluredir=$ALLURE_RESULTS
fi
В начале идет работа по определению маркера, с которым будут запускаться тесты. В зависимости от scope будет выбран соответствующий маркер для smoke или regress тестов. Далее, вызывается скрипт по определению директорий тестов, который сопоставляет имена драйверов с папками с тестами. В итоге, нужный маркер и список директорий подаются на вход pytest и начинается прогон тестов.
Шаблон revert джоб
- git clone -b $GITOPS_BRANCH $GITOPS_REPO_URL gitops
- cd gitops
- $SCRIPTS_DIR/update-drivers.py revert "$_NAMESPACE"
--env "$UPDATE_DRIVERS_ARG_ENV"
- git add apps && git commit -m "[AUTOTEST] [$_NAMESPACE] Revert $_DRIVERS"
- git pull origin $GITOPS_BRANCH --rebase
- |
for try in $(seq 1 5); do
if git push origin $GITOPS_BRANCH; then
break
else
git pull origin $GITOPS_BRANCH --rebase
fi
done
- $SCRIPTS_DIR/update-drivers.py wait "$_NAMESPACE"
--tag "$TAG_ANY_VERSION"
--drivers "$_DRIVERS"
--env "$UPDATE_DRIVERS_ARG_ENV"
Этот шаблон, очень похож на шаблон деплоя. Так же идет скачивание GitOps репозитория. На втором шаге происходит вызов скрипта с командой “revert” и переменными, которые имеют те же значения что и в шаблоне деплоя. Скрипт очищает “ci.yaml” в соответствующем NAMESPACE. Далее, идет пуш изменений в репозиторий и ожидание, когда список сервисов обновится. На этом этапе вместо ветки, передается значение “any”, которая говорит скрипту, что нужно дождаться только деплоя сервиса и не проверять его ветку. Так же, как и шаблон деплоя, шаблон отката используется для основных сервисов и мока и отличается только входными переменными.
В итоге получился pipeline, который деплоит нужную ветку на тестируемые сервисы и нужную версию мока, прогоняет тесты только нужных сервисов, и откатывает тестируемую ветку. Вызов этого pipeline был добавлен в Schedules, тем самым прогон smoke и regress тестов идет через него.
Последняя часть решения - Slack bot. Это сервис, который располагается в нашем же окружении. Он реализует интерфейс и принимает на вход все необходимые переменные: список сервисов и их ветки, мок и его ветка и ветка тестов.

В бота были добавлены функции-помощники, которые помогают не держать в голове правильные имена сервисов и веток. Бот с периодичностью выкачивает необходимые репозитории, чтобы обновить список сервисов и доступных веток.

Все опции интерфейса Slack бота


Итоги
В результате реализации решения, мы смогли повысить стабильность ежедневных и еженедельных прогонов. Так как перед запуском тестов, все сервисы деплоятся на основную ветку, берется основная ветка мока и тестов. Тем самым мы получаем меньше ложных падений из-за того, что сервис был на dev ветке. Плюс, мы не прерываем разработку, так как разработчику не нужно возвращаться на свою ветку обратно после ночного прогона, он может спокойно продолжить разработку и тестирование.
Еще один плюс который был получен, это легкий способ запуска тестов. Разработчику теперь не нужно идти в репозиторий тестов и gitops, все настраивать и запускать CI через GitLab. Теперь достаточно просто в боте указать нужные сервисы, ветки и получить результат тестов. Это повысило количество запускаемых тестов, качество выпускаемых сервисов, скорость тестирования, а самое главное разработчики смело делают рефакторинги, большие изменения в нескольких сервисах или в базовой логике, так как знают что смогут легко прогнать автотесты и отловить ошибки.
И неожиданный бонус, который был обнаружен. Нам пришлось актуализировать некоторые тесты. Дело в том, что у нас есть “старые” сервисы, которые давно не обновлялись в дев окружении. Это связанно с тем, что в код этих сервисах давно не вносились изменения, так как все баги и проблемы были исправлены уже давно. Но после запуска скрипта, была подтянута свежая версия основной ветки и наши тесты посыпались. Как оказалось, базовая логика под этими сервисами давно была изменена, а наши тесты нет. Как результат, после принудительного обновления сервисов, тесты и сервисы разъехались. Получается, что еженедельное обновление всех сервисов, теперь помогает нам держать тесты в актуальном состоянии.
Но в нашем решении есть пару недостатков:
Во-первых, данное решение не подойдет для сложных сервисов, в которых есть взаимная зависимость или зависимость от миграций БД и т.д. Всем этим сложно управлять в рамках такого небольшого и простого скрипта. Такие сервисы мы деплоим и откатываем по старому.
Второй минус, это невозможность параллельной работы. Тут есть несколько блокеров. Первый - это реверт скрипт, который очищает полностью файл “ci.yaml”, что ломает соседний прогон. Второй момент, вероятность пересечения сервисов, не до конца понятно как и в какой момент это разруливать. Но думаю, что это решаемые проблемы и когда это будет нас замедлять в выпуске сервисов, то мы займемся ими.
Реализовал все это решение Александр Скурихин. Спасибо за ревью статьи Александр Скурихину и Владиславу Александрову
