Как написать автотесты деплоя и сэкономить нервы DevOps-инженеров

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Привет! Меня зовут Артём Комаренко, я работаю на позиции QA Lead в команде PaaS в СберМаркете. Хочу поделиться историей, как мы придумывали способ быстро убедиться, что очередные изменения в скриптах деплоя не разломают процесс выкатки во всей компании. 

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

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

<a name="why">Зачем всё это?</a>

В Сбермаркете мы разрабатываем PaaS, и одной из важных частей нашей платформы является CI/CD pipeline. То есть наши пользователи-разработчики получают «из коробки» готовый pipeline, в котором уже есть различные задачи по запуску тестов, линтеров и прочих ништяков, а также задачи по выкатке приложения на тестовые стенды и созданию релиза для выкатки на прод.

И вот однажды ко мне пришел лид команды DevOps с запросом «Хочу автотесты!»

<a name="plan">План действий</a>

Мы определили, что для PaaS важно убедиться, что разработчик, который первый раз воспользовался нашими инструментами, сможет без проблем выкатить свежесозданный сервис. А для DevOps инженеров было важно знать, что после внесения изменений в скрипты всё ещё можно спокойно деплоить новую версию приложения поверх существующей. 

Таким образом определился базовый набор сценариев: 

  • деплой нового сервиса;

  • деплой существующего сервиса.

Сценарии отличаются началом, где мы получаем сервис, и списком проверок. И в общем виде выглядят так:

  1. Создать новый (или клонировать существующий) сервис локально

  2. Внести и запушить изменения в удаленный репозиторий

  3. Найти пайплайн МРа

  4. Выполнить джобу деплоя

  5. Проверить, что джоба завершилась успешно

  6. Проверить, что в неймспейсе появились нужные поды/контейнеры

  7. Остальные проверки

Три кита, на которых держится автотест:

  1. Для работы с локальным репозиторием нам нужен git, соответственно была выбрана библиотека GitPython.

  2. Работать с gitlab было решено по API, для этого как раз подходит библиотека python-gitlab.

  3. С 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>

На двух простых тестах мы не остановились.

Уже сделано:

  1. Мы расширили скоуп проверок в каждом тесте, проверяем как вместе с сервисом разворачиваются его ресурсы: postgres, redis и т.п.

  2. У нас есть несколько вариантов деплоя: стабильный стейдж, отдельным подом, отдельный стейдж. Добавили тесты для каждого.

  3. PaaS поддерживает несколько языков, для каждого свой деплой. Добавили тесты для основных языков.

  4. Наработки автотестов деплоя позволили реализовать автотесты рейтлимитера, в которых мы точечно проверяем как он отработал в зависимости от настроек.

Сейчас основные направления для дальнейшего развития:

  1. Автотесты для деплоя на прод

  2.  Проверка оставшихся языков, для которых есть наш деплой

  3. Отстрел сервиса в процессе деплоя

<a name="problems">Проблемы</a>

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

  • сетевая проблема в облаке (недоступно, долго отвечает, 500-тит);

  • кончились ноды и kubernetes не может поднять под;

  • заняты все раннеры в CI/CD;

  • спам тестов из-за частых коммитов;

  • неявное изменение бизнес логики, когда тест в целом проходит, но иногда падает, а на что смотреть — непонятно.

Для минимизации этих проблем мы итеративно улучшаем наши тесты: добавляем явные ожидания, retry-механизмы для нестабильных запросов, переделываем способы запуска тестов из пайплайна.

<a name="overall">Резюме или А что по цифрам?</a>

Основной набор состоит из 5 e2e-тестов. При нормальных условиях тест проходит за ~15 минут. Бегают они параллельно.

Тестовый набор запускается минимум 1 раз на МР и в среднем 10-15 раз в день, в зависимости от нагрузки инженерной команды. В месяц выходит порядка 250 запусков тестового набора.

Выполнение этих же операций вручную занимает в разы больше времени и представляет собой не самую интересную часть работы. Автотесты позволяют нам находить ошибки на ранних этапах, экономят время и никогда не устают.

Если вы хотите попробовать такие тесты у себя, то чтобы превратить их в рабочий код нужно:

  1. Реализовать методы взаимодействия с репозиторием и kubernetes. Обычно для этого достаточно взять готовую функции из официальных библиотек и добавить логи.

  2. Добавить ожидания для пайплайнов и задач. Я использую библиотеку waiting для этого.

  3. Добавить проверки для всех своих ресурсов. В общем случае я проверяю:

    • статус  задач в пайплайне

    • наличие и статус подов в kubernetes

    • статус контейнеров внутри подов в kubernetes

  4. Реализовать способ поставки тестовых репозиториев. У меня это отдельный сервис, но возможно вы найдете другой способ.

Буду рад, если наши наработки вам пригодятся. Если вы писали автотесты для деплоя как-то по-другому, то интересно будет услышать, в чём мы разошлись!

Tech-команда СберМаркета завела соцсети с новостями и анонсами. Если хочешь узнать, что под капотом высоконагруженного e-commerce, следи за нами в Telegram и на  YouTube. А также слушай подкаст «Для tech и этих» от наших it-менеджеров.

Источник: https://habr.com/ru/company/sbermarket/blog/715116/


Интересные статьи

Интересные статьи

Хотел бы написать небольшой цикл статей посвященных тому, как я написал свою RTOS с какими трудностями столкнулся и зачем вообще писать свою RTOS если уже есть FreeRTOS, RTX, embOS и т.д. список дост...
Несколько лет назад небольшой командой сделали тестовый прототип PvP-шутера, который внезапно выстрелил. Тогда ни разработчики, ни процессы не были готовы к такой популярности, мы развивали проект зде...
Если при работе с SQLite вам встречалась ситуация, когда не нашлось нужного функционала, то добро пожаловать под кат. И нет, хранимые процедуры добавить нельзя. Читать дальш...
ПривратникиВаше резюме будут читать 3 типа людей:HR или рекрутерПервый человек, который видит ваше резюме, и скорее всего не технарь. У него есть только один вопрос:Надо ...
Пол года назад я сама искала такую статью и теперь я очень хочу посоветовать себе в прошлом с чего можно начать. 1. Выбрать тему приложения Не нужно долгих мук выбора, просто начните делать т...