Привет! Меня зовут Артём Комаренко, я работаю на позиции 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-менеджеров.

Комментарии (13)


  1. pifagor_mc
    00.00.0000 00:00
    +2

    Годно написано и по существу! Вопрос, что называется, на злобу дня, в поддержку/развитие этих автотестов вовлекается команда разработки или обеспечиваете их работоспособность и озеленяете их силами команды QA?


    1. artyom_komarenko Автор
      00.00.0000 00:00
      +2

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


  1. 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 у него будет присутствовать


    1. artyom_komarenko Автор
      00.00.0000 00:00
      +3

      О, спасибо за совет! Не сталкивался ещё с таким кейсом, но подумаем как это прикрутить в наших тестах.


  1. gigimon
    00.00.0000 00:00
    +1

    Не совсем понятно, а что вы тестируете то? Вы проверяете действие каких-то уже готовых пайплайнов, с помощью которых все выкатывается? Но что будет, если этот пайплайн создаст что-то дополнительное, не ожиданое? или уничтожит левый pod?

    А что если в пайплан настоящего сервиса добавят что-то свое?


    1. artyom_komarenko Автор
      00.00.0000 00:00

      Готовый пайплайн и скрипты, которые запускаются в джобах этого пайплайна, лежат в отдельном проекте, который развивают наши DevOps-инженеры. Этот пайплайн подключается в gitlab-ci.yml всех сервисов, написанных с ипользованием нашей платформы. DevOps-инженеры регулярно изменяют как сами скрипты, так и список джоб в пайплайне. Тесты помогают убедиться, что очередное изменение в скриптах или около того не разломало функционал выкатки сервиса, мы считаем эту часть пайплайна самой важной.

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

      Если в неймспейсе сервиса появится что-то неожиданное - тут мы знаем какой набор подов должен появиться, и в каком количестве должен быть каждый под. Например, если мы ждем, что поднимется 2 пода приложения, а их поднимается 1 или 4, то тест сообщит об этом. Если мы ждём, что должен подняться под с определенным ресурсом, а его нет - тест нам сообщит об этом. Если все что мы ждем поднялось, но сбоку образовался еще какой-то непонятный под - тест умеет работать только с заранее известной информацией. Можно посчитать общее количество подов и ориентироваться на него, но это не очень стабильная проверка и, на мой взгляд, ненужная. Автотесты не будут проверять 100% всех возможных кейсов и функциональности, они сосредоточены на самом важном. Аномальные изменения будем ловить при ручном тестировании.

      Настоящий сервис может изменить свой пайплайн, у нас даже есть рекомендуемый способ расширения пайплайна (например, чтобы добавить свою задачу с тестами). Но это не имеет смысла с точки зрения пользователя-разработчика, если только он не пытается навредить себе. Если он переопределит джобу деплоя - он просто не сможет выкатиться. А в тестовых репозиториях, на которых гоняются автотесты, используется эталонный пайплайн.

      Надеюсь, смог ответить на ваши вопросы)


  1. OlegSpectr
    00.00.0000 00:00

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

    Подскажите, а как именно вы боретесь с этой проблемой?


    1. artyom_komarenko Автор
      00.00.0000 00:00
      +1

      Рядом с джобой с автотестами мы сделали еще одну джобу (пусть ее имя будет guard), которая в течение 5 минут ожидает, что джоба с автотестами была запущена. А саму джобу с автотестами перевели на ручной запуск.

      Мы имеем следующие кейсы:

      1. Автотесты не были запущены => джоба guard зафейлилась => пайплайн красный

      2. Автотесты были запущены => джоба guard зеленая => автотесты нашли ошибку => пайплайн красный

      3. Автотесты были запущены => джоба guard зеленая => автотесты успешно прошли => пайплайн зеленый

      Таким образом, мы гарантируем, что у нас не будет МРа, который вмержили без тестов.


      1. OlegSpectr
        00.00.0000 00:00
        +1

        Интересно, спасибо за ответ)
        Только мне кажется ручной запуск утомляет немного (слишком часто приходится лезть в пайплайны)


  1. megamrmax
    00.00.0000 00:00

    Интересная статья, спасибо большое. Предстаит задача создать нечто подобное, "в одно лицо", поэтому есть шкурный вопрос - а есть ли какой телеграм канал или что-то подобное где Вы и ваша команда присутствует и куда можно позадовать глупых вопросов? Не корпоративный канал - а именно такой, лампово-qa?


    1. artyom_komarenko Автор
      00.00.0000 00:00
      +1

      Чатика такого, к сожалению, нет. Создавать его немного побаиваюсь, он может сожрать много времени, а работать надо)


  1. BATAZOR
    00.00.0000 00:00

    А разве для этих кейсов не подойдет kyverno или OPA? Также можно задать правила и автоматически проверять, что у разработчиков все правильно (в плане описания информации о сервисах команды, что установлены лимиты по ресурсам, есть пробы).

    • можно еще kind поднимать прямо в CI и прогонять тесты еще до деплоя в k8s - в этом случае, конечно, свои тесты имеют смысл.


    1. artyom_komarenko Автор
      00.00.0000 00:00
      +1

      Скажу честно, я не сталкивался с этими инструментами, и даже не слышал. Но спасибо за наводку, почитаю, что это за зверь, может пригодится)