Предисловие
Привет, Хабр, я 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”, что ломает соседний прогон. Второй момент, вероятность пересечения сервисов, не до конца понятно как и в какой момент это разруливать. Но думаю, что это решаемые проблемы и когда это будет нас замедлять в выпуске сервисов, то мы займемся ими.
Реализовал все это решение Александр Скурихин. Спасибо за ревью статьи Александру Скурихину и Владиславу Александрову