Меня зовут Слава Черепанов, я работаю в 2ГИС на проекте Fiji. Мы делаем профессиональную ГИС-систему, с помощью которой картографы 2ГИС создают карту мира. Решаем разные задачи — от ручной отрисовки сложных зданий до автораспознавания дорожных знаков.
В этой статье я расскажу, как в нашем проекте за 4 года эволюционировала инфраструктура интеграционных тестов. Это будет не летопись, а история про выборы, их причины и следствия. Она поможет создать тестовую инфраструктуру, подходящую именно вам, и справиться с этим в разумные сроки.
Статья разбита на две части. В первой расскажу, как мы переизобретали инфраструктуру интеграционных тестов и зачем нам это понадобилось. Во второй будет больше Докера, выводов и планов на будущее.
С чего всё началось
Четыре года назад на проекте Fiji были:
регрессионное тестирование длиной в человеко-неделю,
очередь задач на тестирование (пара тикетов так там и умерли, не дойдя до продакшна ????),
большие и сложные релизы раз в месяц,
трудоёмкая настройка тестовых стендов,
отсутствие доверия к существующим интеграционным автотестам.
Короче, целый клубок проблем. Чтобы его распутать, мы завели кота решили возродить интеграционные тесты.
Не сказать, что в то время мы жили совсем без автотестов — разработчики исправно писали модульные. Интеграционные тоже были, но они не обновлялись и не дополнялись, и вот почему.
Чтобы сделать прогон на своем компе, требовалась здоровая локальная инфраструктура в рабочем состоянии. Основная БД в MS SQL Server, графовая база Neo4j с дорожными звеньями, база PostgreSQL для хранения векторных тайлов и так далее. Когда гоняешь тесты на локальном окружении, какой-нибудь TearDown-метод может стереть данные, подготовленные для других задач.
Главная беда — основная база с картографическими данными лежала как бэкап прямо в репозитории проекта. Для каждого прогона её надо было разворачивать на локальном MS SQL сервере. Когда кто-то из разработчиков писал новую миграцию и накатывал на тестовую базу, при мёрже в master гремели конфликты — бэкап уже успел кто-то обновить.
Мы понимали, что автотесты — не серебряная пуля. Их будет недостаточно. А если неправильно к ним подойти, они сожрут наше время и навредят проекту. Но они могли помочь нам доставлять быстрее и не тратить время на регрессию и возню с окружениями, а это уже немало. И вот, 3 года назад мы решили попробовать.
Окружения для автотестов и с чем их едят
Популярное мнение — модульные тесты дешевле и экологичнее интеграционных. И часто это так и есть. В этой статье я подразумеваю, что на вашем проекте есть реальная необходимость писать интеграционные тесты. Дальше будем разбираться на примере автотестов для тестирования бэкендов.
❗ Этот раздел довольно академичен — остерегайтесь нападения сферических коней ❗
Мы задумались, чего хотим от БД для автотестов. Идеальная база данных:
актуальна задаче, которую ты делаешь;
позволяет параллельные прогоны на агентах CI и у людей локально;
поддерживает бесконфликтные миграции БД в разных ветках;
развёртывается быстро и одной командой;
не занимает лишних ресурсов;
стабильна и доступна, когда нужно;
в начале прогона находится в определённом состоянии и предсказуемо меняется.
Выбор подходов и инструментов для построения стенда зависит от его предназначения. Например, стенд для приёмочного тестирования должен максимально повторять бой, а стенд для автотестов/отладки должен быть быстрым, лёгким и постоянно доступным.
Каким вообще бывает окружение для автотестов — отобразил в схемке.
Если нашим автотестам не нужно никакое тестовое окружение, то они находятся где-то в основании пирамиды тестирования. Эмуляцию, моки и стабы в этой статье обсуждать не будем. Ключевое деление — либо стенд стабилен и постоянен, либо он генерируется ради требуемой задачи. Разберёмся, какой подход полезен в какой ситуации.
Локальный стенд
На каждом агенте CI или рабочей станции развернута своя копия окружения. Локальный стенд подходит, если:
у вас простое окружение, которое легко поддерживать в рабочем состоянии;
вы умеете его централизованно ставить/обновлять;
вам не нужно одновременно гонять тесты и вести другие работы на машине — например, дебажить ветку;
данных мало и они меняются редко.
Выделенный стенд
Вы разворачиваете отдельный тестовый контур, который станет плацдармом для прогона автотестов. Очевидно, одного контура вам не хватит — потребуется несколько копий (как минимум одна для каждого агента билд-сервера). Это сработает, если:
вы умеете быстро разворачивать новые стенды или точно знаете, сколько их вам нужно;
вы готовы планировать и мониторить использование стендов — или у вас очень много ресурсов и вас не заботят простои;
вы хотите гонять тесты на боевых данных;
у вас есть квалифицированные администраторы со свободными ресурсами, чтобы обеспечивать работоспособность контуров 24/7.
Генерируемый стенд
Генерируемые окружения бывают разные — всё зависит от ПО, которое их создаёт. Например, если ваше окружение — только база данных, то можно попробовать решения на стороне сервера БД, снэпшоты. Для более сложных случаев есть виртуальные машины и обёртки над средствами виртуализации (Vagrant). Не забудем про контейнеризацию (Docker, LXC) и системы управления контейнерами.
В дальнейшем я буду делать акцент именно на Docker, так как знаком с ним лучше всего.
Итак, генерируемые окружения — наш путь, если:
есть опыт использования Docker — например, деплоим приложение на продакшн в докер-контейнерах;
мы хотим быстро поднимать инстанс и гасить его, когда не нужен;
не хотим греть себе голову о параллельных прогонах, конфликтах, изоляции — пусть всё работает из коробки;
наша система сложная, куча разных баз данных, а мы хотим единый подход ко всему;
внедряем infrastructure as code.
Мутанты
Есть и другие подходы — я ласково называю их мутантами ????.
Локальный стенд в Docker. Давайте поднимем у себя на машине тестовый стенд из десятка контейнеров. Какие минусы? Это всё тот же локальный стенд. Да, есть единый инструмент, не надо устанавливать разные серверы БД. Но каждому новому члену команды придется настраивать и администрировать его у себя. Не может стартануть твой докер демон — сиди, разбирайся.
Выделенный стенд из единственной машины. На одной выделенной тачке крутится куча сервисов, серверов приложений и баз данных. Минимальная изоляция приложений друг от друга, живут тесно в одной коммуналке.
Долгоживущие стенды в Docker. Когда предназначение стенда — гонять автотесты, необходимость сохранять плацдарм после прогона нужно чем-то оправдывать. Если хотим построить на контейнерах стабильный стенд, то сразу встают вопросы про рестарты, liveness-пробы и прочий Kubernetes.
Я не говорю, что подходы-мутанты — это всегда плохо. Но часто это повод задуматься, как сделать инфраструктуру для интеграционных тестов лучше.
Что выбрать
Для начала определитесь с критериями отбора. Пусть тестовый стенд должен:
подходить для сложной системы из множества компонент;
быть максимально похож по инфраструктуре на прод;
единообразно разворачивается, управляться, логироваться;
потреблять минимум аппаратных ресурсов (и психических у админов ????);
легко масштабироваться.
При таких критериях очевидный победитель — генерируемый стенд ????. Но, например, у постоянных стендов есть преимущество — при запуске тестов их не надо заново разворачивать. А для эксплуатации генерируемых стендов требуется определённая квалификация.
Ну и главное — вы не должны выбирать только одно-единственное решение. Вы можете разместить базу данных в Docker, но при этом брать недостающие данные с выделенного стенда. Смелее экспериментируйте!
???? Docker, Kubernetes?
3 года назад мы выбрали генерируемые стенды. Тестовые окружения в Docker полностью подходили под наши критерии.
Если вы решили использовать контейнеры, то нужно решить, устроит ли вас чистый Docker или нужно подключить систему оркестрации. Выбор зависит от потребностей и ресурсов.
На рынке существует несколько крупных игроков (Kubernetes, Docker Swarm, Apache Mesos) и ещё больше мелких. В дальнейшем я буду ссылаться на Kubernetes, так как лучше с ним знаком.
Когда мы приступили к контейнеризации БД для автотестирования, ни у кого в команде Fiji не было компетенций по использованию K8s. К счастью, 3 года назад в 2ГИС уже было несколько готовых кластеров K8s и команда Infrastructure & Operations, которая обслуживала и развивала инфраструктуру. Помозгоштурмив с ребятами в переговорке, туда мы и решили заехать (в K8s, а не переговорку ????).
Использовать чистый Docker или Docker Compose — вполне достаточно для простых задач. Если вы только погружаетесь в удивительный мир контейнеризации, вряд ли стоит сразу заныривать с головой в K8s.
История про то, что K8s нужно уметь готовить
Однажды мы заметили, что наши тесты покраснели. Каждый билд всё начиналось хорошо, но после момента X становилось плохо. Тесты падали, причём падали странно. Зелёная часть запускалась на актуальной БД со всеми миграциями, а красная — не пойми на чём. Мы стали копать и — о, ужас! — увидели, что K8s рестартует контейнер базы данных. Мы готовили контейнер один раз в начале прогона и про рестарты даже не думали. Оказалось, что контейнер MS SQL стабильно упирается в лимит по памяти и K8s решает, что его можно грохнуть. А циферка с лимитом была взята на глазок. Мы записали в конфигу реалистичную цифру, и наши тесты магическим образом позеленели.
Подбираем ключи к Kubernetes
Для прогона автотестов наши сервисы по-прежнему запускаются локально, но используют хранилища тестовых данных в K8s. В самом начале мы ещё не рассматривали вариант засунуть сами тестируемые сервисы в Docker.
В такой схеме хочешь запустить функциональные тесты локально — поставь только клиент kubectl. Наши pod’ы будут подниматься на произвольных машинах кластера, а мы будем к ним цепляться.
Итак, мы в начале пути, добываем себе песочницу. Какой вопрос нам зададут? Пожалуй, «где и в каких масштабах?». Когда мы с инфраструктурными инженерами 2ГИС мозгоштурмили будущую систему, то прикинули необходимое количество pod’ов в тестовом контуре, приблизительное число одновременно поднятых окружений и расход memory/cpu на каждый. Путём нехитрых перемножений получили ресурсные квоты для неймспейса Fiji.
Мы используем два вида ресурсов Kubernetes — deployment и service. В deployment размещается нужное приложение, service предоставляет доступ к нему.
Чтобы отправить запрос из внешнего мира в конкретный pod, мы отсылаем его на hostIp:nodePort (hostIp — параметр пода, а nodePort — параметр службы; и то, и другое можно получить, например, командой kubectl describe).
На одном наборе деплойментов гоняет тесты только один человек/агент. При необходимости возможен параллельный доступ на чтение. Последовательные прогоны на одном и том же окружении используются тогда и только тогда, когда нужно что-то дебажить. Во всех остальных случаях после прогона освобождаем ресурсы K8s.
Схематично можно представить так:
В одном неймспейсе имена ресурсов не должны дублироваться. Чтобы обеспечить параллельный запуск тестов, мы поднимаем каждому пользователю деплойменты с уникальными именами.
Примеры yml-конфигов K8s
Для деплоймента:
apiVersion: apps/v1
kind: Deployment
metadata:
name: $name
spec:
replicas: 1
revisionHistoryLimit: 3
selector:
matchLabels:
run: $name
template:
metadata:
labels:
run: $name
spec:
nodeSelector:
role: worker
ssd: "true"
containers:
- name: mssql
image: <Название Docker Registry>/<Название неймспейса>/<Название образа>:<Тэг образа>
ports:
- containerPort: 1433
resources:
requests:
cpu: 100m
memory: 1800Mi
limits:
cpu: 1024m
memory: 4000Mi
Здесь $name — имя деплоймента, которое генерится из кода во время подъёма тестовой инфраструктуры
Пример конфиги для службы:
apiVersion: v1
kind: Service
metadata:
name: $serviceName
labels:
run: mssql
spec:
selector:
run: $name
ports:
- protocol: TCP
port: 9898
targetPort: 1433
type: LoadBalancer
Работаем с K8s из кода
3 года назад нам не попались на глаза opensource-модули на C# для работы с K8s, поэтому для взаимодействия с ним мы написали свою обертку над консолью kubectl.
Разберём в общих чертах, что мы делаем с нашим кодом при запуске тестов.
Осознаем, гоняются ли прямо сейчас тесты на текущей машине — чтобы при необходимости сдвинуть порты локально запускаемых сервисов.
Для всех тестовых БД проверяем, подняты ли в K8s необходимые деплойменты/службы. Если нет — поднимаем, если да — проверяем, насколько они старые. Всё старьё грохаем и переподнимаем.
Получаем из K8s и запоминаем все ip, порты, времена старта контейнеров.
Проверяем коннекты к базам данных, всё должно быть ок.
Если необходимо, накатываем на БД свежие миграции. Разработчик мог написать свежую в текущей ветке, а мы всегда хотим гонять тесты на актуальной БД.
Устанавливаем новые адреса баз и сервисов в переменные среды процесса. Затем запускаемые сервисы их подхватят.
Запускаем сервисы Fiji в правильном порядке. Убеждаемся по healthcheck'ам, что всё в порядке.
Далее следуют SetUp'ы тест-сьютов, конкретных кейсов, непосредственно код тестов и соответствующие TearDown'ы. Мы не пересоздаем pod после каждого теста. Вместо этого вычищаем из БД все объекты, созданные после первого запуска контейнера. Это хорошо работает, хотя и осложняет распараллеливание тестов.
По завершении тестового прогона стопаем сервисы Fiji, а также деплойменты и службы в K8s. Для отладки новых тестов предусмотрели возможность сохранять окружение после прогона, чтобы минимизировать простои при перезапусках.
Главный недостаток текущего подхода — далеко не все тесты можно гонять параллельно. Но мы постепенно идём в эту сторону, стремимся уменьшить общее время прогона. Сейчас можем запускать несколько прогонов на одной машине и не конфликтовать за локальные порты и объекты K8s.
Что дальше
А дальше — небольшой перерыв в чтении. Встретимся во второй части статьи. Я расскажу:
как готовить образы тестовой БД в Docker,
как их актуализировать,
какие вызовы сейчас стоят перед нашей тестовой инфраструктурой.
Возвращайтесь, будет интересно ????
ivegner
Разве не может в большой уважающей себя компании быть выделенной команды, которая бы занималась поддержанием корпоративного докер-репозитория с уже готовыми образами, которые новоприбывшему надо только установить и радоваться?
cherepanov_v Автор
Привет! Спасибо за комментарий. Ответ - может. Например, у нас есть корпоративный докер-репозиторий и замечательные люди, которые его поддерживают.
Цитированный фрагмент немного про другое. Описан подход, когда каждый разработчик или агент CI генерирует стенды для автотестов на своей собственной машине, и у этого есть свои минусы.
Одно из преимуществ системы, которую мы сделали в Fiji - как раз то, что новоприбывшему человеку для прогона тестов дополнительно нужно поставить только клиент K8s. Пусть немного, но облегчает онбординг ????
ivegner
Понятно. Наверное, я просто зацепился глазом за формулировки. В контексте становится яснее, к чему вы клоните.
Возможно, мои проблемы ещё не доросли до масштаба ваших, но я всё не могу взять в толк, оправдывает ли себя добавление единицы в стэк, т.е. кубернетеса или иной системы оркестрации. В моей текущей компании мы (на данный момент) используем docker-compose. Коллега также разрабатывает ему на замену докер-образ со всеми службами запущенными внутри, который будет раздаваться через докер-репозиторий.
И сколько я ни кручу в уме эту схему, никак не могу понять, какую существенную пользу мог бы нанести ей кубернетес — ценой усложнения технологического стэка.