Привет! Меня зовут Артём Комаренко, я работаю на позиции QA Lead в команде PaaS в СберМаркете. Хочу поделиться историей, как мы придумывали способ быстро убедиться, что очередные изменения в скриптах деплоя не разломают процесс выкатки во всей компании.
Статья будет полезна QA-специалистам и DevOps-инженерам, которые хотят автоматизировать тесты инфраструктуры. Вы узнаете как и с помощью чего можно проверить такую сущность как деплой.
В статье я буду рассказывать о примерном ходе работы, опуская специфику конкретно нашей компании. Фрагменты кода также будут отражать только идею, без промежуточных переменных, с упрощенными наименованиями, без точного количество аргументов и т.п.
<a name="why">Зачем всё это?</a>
В Сбермаркете мы разрабатываем PaaS, и одной из важных частей нашей платформы является CI/CD pipeline. То есть наши пользователи-разработчики получают «из коробки» готовый pipeline, в котором уже есть различные задачи по запуску тестов, линтеров и прочих ништяков, а также задачи по выкатке приложения на тестовые стенды и созданию релиза для выкатки на прод.
И вот однажды ко мне пришел лид команды DevOps с запросом «Хочу автотесты!»
<a name="plan">План действий</a>
Мы определили, что для PaaS важно убедиться, что разработчик, который первый раз воспользовался нашими инструментами, сможет без проблем выкатить свежесозданный сервис. А для DevOps инженеров было важно знать, что после внесения изменений в скрипты всё ещё можно спокойно деплоить новую версию приложения поверх существующей.
Таким образом определился базовый набор сценариев:
деплой нового сервиса;
деплой существующего сервиса.
Сценарии отличаются началом, где мы получаем сервис, и списком проверок. И в общем виде выглядят так:
Создать новый (или клонировать существующий) сервис локально.
Внести и запушить изменения в удаленный репозиторий.
Найти пайплайн МРа.
Выполнить джобу деплоя.
Проверить, что джоба завершилась успешно.
Проверить, что в неймспейсе появились нужные поды/контейнеры.
Остальные проверки.
Три кита, на которых держится автотест:
Для работы с локальным репозиторием нам нужен git, соответственно была выбрана библиотека GitPython.
Работать с gitlab было решено по API, для этого как раз подходит библиотека python-gitlab.
С k8s так же решено работать по API, и здесь так же есть библиотека kubernetes.
С вводными определились, можно приступать к написанию теста.
<a name="script">Скриптуем в лоб</a>
Для начала мне нужно было выстроить логику теста, понять как взаимодействовать с сущностями. Поэтому я решил написать тест как обычный скрипт. Этот этап не требуется повторять, можно сразу писать по правилам тестового фреймворка.
Основные действия я вынес в методы хелперов:
хелпер для взаимодействия с локальным и удаленным репозиторием;
хелпер для работы с Kubernetes.
Для красивых проверок я использовал библиотеку assertpy.
if name == '__main__':
repo = RepositoryHelper(remote_host)
kubectl = Kubernetes(service_name)
repo.clone(service_name)
os.chdir(service_path)
repo.make_local_changes()
repo.push_local_branch(branch_name)
repo.create_mr(branch_name)
repo.find_pipeline(mr)
repo.run_job('deploy', pipeline)
assert_that(job).has_status('success')
kubernetes.find_pod('app')
assert_that(app_pod).has_status('running')
Скрипт сработал. И тут же я столкнулся с первой трудностью – следующий прогон падал, потому что состояние тестового репозитория изменилось. Нужно за собой прибраться: дописываем в конце нашего скрипта инструкции по откату изменений в репозитории, закрытию МРа, удалению ветки, чистке неймспейса.
if name == '__main__':
...
repo.checkout_local_branch(recovery)
repo.push_local_branch(recovery, force=True)
repo.close_mr(mr)
repo.remove_remote_branch(branch_name)
kubectl.remove_namespace(namespace_name)
Теперь можно запускать прогоны один за другим, все работает. Но как только что-то пошло не так, тест упал — чистка не произошла. Пришло время использовать привычный тестовый фреймворк – pytest.
<a name="test">Тест здорового человека</a>
Основным вызовом при переходе к pytest стало понять что и как перенести в фикстуры, чтобы в тесте осталась только логика самого теста. В фикстуры переехал функционал по подготовке локальной среды к тесту, инициализации хелперов, клонированию репозитория, и самое важное — чистка по завершению.
Как итог, наш скрипт преобразился в стандартный тест:
class TestDeploy:
def test_deploy_new_service(repo, kubernetes, branch_name):
repo.local.make_changes()
repo.local.push(branch_name)
repo.remote.create_mr(branch_name)
repo.remote.find_pipeline(mr)
repo.remote.run_job('deploy', pipeline)
assert_that(job).has_status('success')
kubernetes.find_pod('app')
assert_that(app_pod).has_status('running')
За счет очистки в финализаторах фикстур, тест стал проходить даже если в середине произойдет сбой. А также теперь можно создавать и запускать любое количество тестов, а не по одному, как было раньше.
<a name="cicd">Перемещаемся в пайплайн</a>
При запуске локально тесты работают. Но нам нужно переместить их в пайплайны. Сразу перейду к списку того, что потребовалось тюнить:
Git. На локальной машине для идентификации используется ssh-ключ, а в пайплайне – нет. Если для работы с удаленным репозиторием вместо ssh использовать http-протокол, то после вызова команды потребуется ввести логин и пароль. Но у git'а есть возможность указать значение
store
в настройкуcredential.helper
, и тогда можно сохранить креды в форматеhttps://{user}:{token}@{host}
в файл.git-credentials
Учетная запись. На локальной машине используется моя собственная учетка, логин и пароль знаю только я, доступы есть везде, куда нужно. Но транслировать свои креды на удаленную машину – плохая идея. Мы создали сервисную учетку с доступами только до требуемых проектов.
<a name="manydevs">Раз разработчик, два разработчик…</a>
Следующим вызовом стала проблема параллельного запуска тестов. Когда я их писал, то и запускал только я один. Но теперь их используют несколько разработчиков. Прогоны начали мешать друг другу, скапливаться в очереди, потому что тестовый репозиторий всего один.
Нужно создать больше тестовых репозиториев. Но как тест узнает, какой репозиторий использовать? В рантайме эту информацию не удержать, случайный выбор не даёт гарантий.
Я решил создать отдельный сервис, который заберет на себя эту работу.
<a name="box">Коробка с болванчиками</a>
Я использовал наш PaaS и написал небольшой сервис на Golang. Список тестовых репозиториев и их статус хранятся в Postgres.
Сервис предоставляет три gRPC-ручки:
LockRepo — блокирует самый давно неиспользуемый сервис и отдает его данные;
UnlockRepo — разблокирует указанный сервис;
GetReposList — возвращает список всех сервисов.
Также рядом с сервисом есть кронджоба, которая разблокирует сервисы, которые заблокировали и забыли.
Блокировку и разблокировку тестового сервиса я вынес в фикстуру:
@pytest.fixture()
def project_info():
repo = DummiesBox.lock_repo()
yield repo
DummiesBox.unlock_repo(repo.id)
И теперь каждый тест деплоя проходит на отдельном сервисе, их состояние изолированно друг от друга. Плюс у инженеров есть время отладить тестовый сервис, если тест найдет ошибку, потому что вперед выделяются более старые сервисы.
<a name="future">Развитие</a>
На двух простых тестах мы не остановились.
Уже сделано:
Мы расширили скоуп проверок в каждом тесте, проверяем как вместе с сервисом разворачиваются его ресурсы: postgres, redis и т.п.
У нас есть несколько вариантов деплоя: стабильный стейдж, отдельным подом, отдельный стейдж. Добавили тесты для каждого.
PaaS поддерживает несколько языков, для каждого свой деплой. Добавили тесты для основных языков.
Наработки автотестов деплоя позволили реализовать автотесты рейтлимитера, в которых мы точечно проверяем как он отработал в зависимости от настроек.
Сейчас основные направления для дальнейшего развития:
Автотесты для деплоя на прод.
Проверка оставшихся языков, для которых есть наш деплой.
Отстрел сервиса в процессе деплоя.
<a name="problems">Проблемы</a>
Разумеется, трудности встречались регулярно и до конца никогда не исчезнут. Инфраструктура может сбоить, что приводит к падению теста. Вот примеры некоторых проблем:
сетевая проблема в облаке (недоступно, долго отвечает, 500-тит);
кончились ноды и kubernetes не может поднять под;
заняты все раннеры в CI/CD;
спам тестов из-за частых коммитов;
неявное изменение бизнес логики, когда тест в целом проходит, но иногда падает, а на что смотреть — непонятно.
Для минимизации этих проблем мы итеративно улучшаем наши тесты: добавляем явные ожидания, retry-механизмы для нестабильных запросов, переделываем способы запуска тестов из пайплайна.
<a name="overall">Резюме или А что по цифрам?</a>
Основной набор состоит из 5 e2e-тестов. При нормальных условиях тест проходит за ~15 минут. Бегают они параллельно.
Тестовый набор запускается минимум 1 раз на МР и в среднем 10-15 раз в день, в зависимости от нагрузки инженерной команды. В месяц выходит порядка 250 запусков тестового набора.
Выполнение этих же операций вручную занимает в разы больше времени и представляет собой не самую интересную часть работы. Автотесты позволяют нам находить ошибки на ранних этапах, экономят время и никогда не устают.
Если вы хотите попробовать такие тесты у себя, то чтобы превратить их в рабочий код нужно:
Реализовать методы взаимодействия с репозиторием и kubernetes. Обычно для этого достаточно взять готовую функции из официальных библиотек и добавить логи.
Добавить ожидания для пайплайнов и задач. Я использую библиотеку waiting для этого.
-
Добавить проверки для всех своих ресурсов. В общем случае я проверяю:
статус задач в пайплайне;
наличие и статус подов в kubernetes;
статус контейнеров внутри подов в kubernetes.
Реализовать способ поставки тестовых репозиториев. У меня это отдельный сервис, но возможно вы найдете другой способ.
Буду рад, если наши наработки вам пригодятся. Если вы писали автотесты для деплоя как-то по-другому, то интересно будет услышать, в чём мы разошлись!
Tech-команда СберМаркета завела соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.
Комментарии (13)
irony_iron
00.00.0000 00:00+2как вариант точки роста: вместо pod.status"running" воспользоваться liveness пробой, ее результаты можно получать из команды kubectl
describe pod namepod
Warning Unhealthy 10s (x3 over 20s)
...
суть в том, что если у pod зависимость от другого сервиса (деплой которого наше изменение пайплайна опосредованно сломало), тестовый pod может начать ребутиться каждые 30сек но статус running у него будет присутствовать
artyom_komarenko Автор
00.00.0000 00:00+3О, спасибо за совет! Не сталкивался ещё с таким кейсом, но подумаем как это прикрутить в наших тестах.
gigimon
00.00.0000 00:00+1Не совсем понятно, а что вы тестируете то? Вы проверяете действие каких-то уже готовых пайплайнов, с помощью которых все выкатывается? Но что будет, если этот пайплайн создаст что-то дополнительное, не ожиданое? или уничтожит левый pod?
А что если в пайплан настоящего сервиса добавят что-то свое?
artyom_komarenko Автор
00.00.0000 00:00Готовый пайплайн и скрипты, которые запускаются в джобах этого пайплайна, лежат в отдельном проекте, который развивают наши DevOps-инженеры. Этот пайплайн подключается в gitlab-ci.yml всех сервисов, написанных с ипользованием нашей платформы. DevOps-инженеры регулярно изменяют как сами скрипты, так и список джоб в пайплайне. Тесты помогают убедиться, что очередное изменение в скриптах или около того не разломало функционал выкатки сервиса, мы считаем эту часть пайплайна самой важной.
Если в пайплайне появится что-то неожиданное - если речь про новую джобу, то в зависимости от важности джобы будет и результат. Если эта джоба не относится к процессу деплоя - мы не будем смотреть на нее в тестах. Если эта джоба требуется для деплоя, тест без нее больше не проходит - надо актуализировать тест, автотесты не умеют предсказывать и делать аналитические выводы наперед.
Если в неймспейсе сервиса появится что-то неожиданное - тут мы знаем какой набор подов должен появиться, и в каком количестве должен быть каждый под. Например, если мы ждем, что поднимется 2 пода приложения, а их поднимается 1 или 4, то тест сообщит об этом. Если мы ждём, что должен подняться под с определенным ресурсом, а его нет - тест нам сообщит об этом. Если все что мы ждем поднялось, но сбоку образовался еще какой-то непонятный под - тест умеет работать только с заранее известной информацией. Можно посчитать общее количество подов и ориентироваться на него, но это не очень стабильная проверка и, на мой взгляд, ненужная. Автотесты не будут проверять 100% всех возможных кейсов и функциональности, они сосредоточены на самом важном. Аномальные изменения будем ловить при ручном тестировании.
Настоящий сервис может изменить свой пайплайн, у нас даже есть рекомендуемый способ расширения пайплайна (например, чтобы добавить свою задачу с тестами). Но это не имеет смысла с точки зрения пользователя-разработчика, если только он не пытается навредить себе. Если он переопределит джобу деплоя - он просто не сможет выкатиться. А в тестовых репозиториях, на которых гоняются автотесты, используется эталонный пайплайн.
Надеюсь, смог ответить на ваши вопросы)
OlegSpectr
00.00.0000 00:00спам тестов из-за частых коммитов
Подскажите, а как именно вы боретесь с этой проблемой?
artyom_komarenko Автор
00.00.0000 00:00+1Рядом с джобой с автотестами мы сделали еще одну джобу (пусть ее имя будет guard), которая в течение 5 минут ожидает, что джоба с автотестами была запущена. А саму джобу с автотестами перевели на ручной запуск.
Мы имеем следующие кейсы:
Автотесты не были запущены => джоба guard зафейлилась => пайплайн красный
Автотесты были запущены => джоба guard зеленая => автотесты нашли ошибку => пайплайн красный
Автотесты были запущены => джоба guard зеленая => автотесты успешно прошли => пайплайн зеленый
Таким образом, мы гарантируем, что у нас не будет МРа, который вмержили без тестов.
OlegSpectr
00.00.0000 00:00+1Интересно, спасибо за ответ)
Только мне кажется ручной запуск утомляет немного (слишком часто приходится лезть в пайплайны)
megamrmax
00.00.0000 00:00Интересная статья, спасибо большое. Предстаит задача создать нечто подобное, "в одно лицо", поэтому есть шкурный вопрос - а есть ли какой телеграм канал или что-то подобное где Вы и ваша команда присутствует и куда можно позадовать глупых вопросов? Не корпоративный канал - а именно такой, лампово-qa?
artyom_komarenko Автор
00.00.0000 00:00+1Чатика такого, к сожалению, нет. Создавать его немного побаиваюсь, он может сожрать много времени, а работать надо)
BATAZOR
00.00.0000 00:00А разве для этих кейсов не подойдет kyverno или OPA? Также можно задать правила и автоматически проверять, что у разработчиков все правильно (в плане описания информации о сервисах команды, что установлены лимиты по ресурсам, есть пробы).
можно еще kind поднимать прямо в CI и прогонять тесты еще до деплоя в k8s - в этом случае, конечно, свои тесты имеют смысл.
artyom_komarenko Автор
00.00.0000 00:00+1Скажу честно, я не сталкивался с этими инструментами, и даже не слышал. Но спасибо за наводку, почитаю, что это за зверь, может пригодится)
pifagor_mc
Годно написано и по существу! Вопрос, что называется, на злобу дня, в поддержку/развитие этих автотестов вовлекается команда разработки или обеспечиваете их работоспособность и озеленяете их силами команды QA?
artyom_komarenko Автор
QA-инженеры пишут новые автотесты, актуализируют существующие, следят за ночными прогонами, помогают разобраться с падениями. Сценарии придумываем совместно с DevOps-инженерами, исходя из наших актуальных потребностей. DevOps-инженеры по желанию помогают с актуализацией тестов, но чаще с точки зрения изменения тестовых репозиториев, а не кода самих тестов.