Русский
Русский
English
Статистика
Реклама

Automatization

Система под контролем как автоматизировать интеграционные тесты

29.10.2020 16:15:50 | Автор: admin

Привет! Меня зовут Ксения Якиль. Я пишу core-сервисы на C и Go в бэкенд-отделе Badoo и Bumble. Наш бэкенд это высоконагруженная распределённая система, обслуживающая пользователей по всему миру. Она оперирует большими массивами данных и делает всю ту магию, благодаря которой люди находят друг друга.

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

Знакомьтесь, сервис М!

В нашем ведении находился один из очень важных сервисов M. Он был первоисточником информации о пользователях и их симпатиях друг к другу. М был самодостаточным, большим, надёжным и даже был покрыт юнит- и функциональными тестами.

Сервис состоит из фронта (Front) и нескольких шардов (S1SN):

Но с увеличением количества задач в одиночку М перестал справляться так хорошо, как раньше. Поэтому у него появились товарищи другие сервисы: мы выделили отдельные логические части M и обернули их в сервисы на Go (Search и Supervisor), добавили Kafka и Consul.

И стало так:

За короткий промежуток времени из простого сервиса на С выросла довольно сложная структура, состоящая уже из пяти компонентов и ещё большего числа инстансов. В итоге мы получили распределённую систему и ряд вопросов в подарок:

  • Работает ли функционал, в котором участвуют несколько сервисов?

  • Поднимается ли система в заданной конфигурации?

  • Что будет, если один из сервисов вернёт некорректный ответ?

  • Что сделает наша система, если один из сервисов будет недоступен: вернёт ожидаемые ошибки, повторит отправку, выберет другой инстанс и отправит запрос туда или вернет закешированные данные?

Мы знаем, как должна вести себя система. Но совпадают ли ожидания и реальность?

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

Появилось и другое узкое место: мы начали параллельно разрабатывать компоненты и очень сильно загрузили отдел тестирования. Это привело к увеличению сроков доставки новых фич на продакшен.

Поэтому мы решили создать автоматизированные интеграционные тесты для сервисов М и Ко и убить одним выстрелом двух зайцев: автоматически выявлять ошибки интеграции до продакшена и сократить сроки доставки фич на прод за счёт совместного с отделом тестирования написания интеграционных тестов.

Мы понимали, что наряду со всеми очевидными плюсами у интеграционных тестов есть не менее очевидные минусы: увеличение времени прогона тестов, нестабильность, сложность написания. Но этого было недостаточно, чтобы остановить нас.

Требования к фреймворку

Какие требования мы предъявляли к интеграционному фреймворку?

  • Легковесность: минимум абстракций и простота добавления новых тестов.

  • Обозримое (по возможности небольшое) время прохождения тестов. Требовались быстро поднять инфраструктуру и осуществить прогон тестов.

  • Запуск разных конфигураций системы. Фреймворк должен позволять настраивать каждый сервис, запускать разные наборы сервисов (подсистемы) и на них отдельно прогонять тесты. Путь от простого к сложному: сначала убеждаемся, что работает небольшая подсистема, потом усложняем её, проверяем и так далее.

  • Реализация на языке Go, поскольку наш отдел разработки пишет на нём. Нам Go очень нравится, а тестировщики быстро его освоили и тоже пишут на нем фреймворк и интеграционные тесты.

С высоты МКС (схематичный план)

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

Модули нашего фреймворка предоставляют всё необходимое для генерации данных, ожидания выполнения запросов, проверки ответов, работы с инфраструктурой и многое другое. Поскольку фреймворк написан на Go, мы решили использовать go testing, а сами тесты поместили в отдельные файлы.

Для настройки и очистки окружения мы используем модуль Testify. Он позволяет создать suite, в котором определены функции:

  • SetupSuite. Вызывается до прохождения всех тестов для данного suite. Именно здесь мы будем осуществлять подготовку окружения.

  • TearDownSuite. Вызывается после прохождения всех тестов для suite. Тут мы почистим за собой инфраструктуру.

  • SetupTest. Вызывается перед каждым тестом для suite. Здесь мы можем осуществлять какую-то локальную подготовку к тесту.

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

Собираем инфраструктуру

Инфраструктура должна предоставлять много возможностей:

  1. Настраивать разную конфигурацию наших сервисов.

  2. Поднимать сторонние сервисы (наподобие Kafka и Consul). Если использовать инстансы внешних сервисов на devel, то проведение интеграционного тестирования может влиять на его состояние. Это приведёт к нестабильному и неожиданному для наших коллег поведению системы. Кроме того, на результаты наших интеграционных тестов смогут влиять действия других отделов придётся тратить больше времени на расследование падений. Повысить стабильность и воспроизводимость тестов можно с помощью изоляции двух сред. Поэтому мы хотели использовать отдельные запущенные инстансы в своей тестовой среде. В качестве бонуса это даёт возможность использовать сервисы любой версии и конфигурации, быстрее проверять гипотезы и не согласовывать изменения с другими отделами.

  3. Работать с этой инфраструктурой: остановить Kafka/Consul/свои сервисы, исключить их из сети или включить в сеть. Нужна большая вариативность.

  4. Запускать на разных машинах, например на машинах разработчиков, QA-инженеров и CI.

  5. Воспроизводить падения тестов. Если тестировщик увидел на своей машине, что тест не прошёл, разработчик должен с минимальными усилиями получить эту ошибку на своей машине. Мы хотели избежать различий в библиотеках и зависимостях на разных машинах (в том числе и на серверах для CI).

Мы решили использовать Docker и обернули сервисы в контейнеры: тесты будут создавать свою сеть (Docker network) для каждого прогона и включать контейнеры в неё. Это хорошая изоляция для тестов из коробки.

Запуск в контейнере

Во фреймворке мы запускаем сервис в контейнере с помощью модуля testcontainers-go, который по факту представляет собой прослойку между Docker и нашими тестами на Go.

Отправляем запрос в этот модуль с характеристикой нашего сервиса. Получаем структуру контейнера и полный спектр возможностей: запускать и останавливать контейнер, узнавать его статус, включать в сеть, исключать из сети и так далее. Вся эта реализация под капотом у testcontainers-go.

Для других языков программирования есть свои модули. Скорее всего, принцип их работы примерно такой же (но это не точно).

Рабочее окружение

Недостаточно просто поднять сервис в контейнере. Нужно подготовить для него тестовое окружение.

  • Создаём иерархию каталогов на хосте.

  • Копируем все необходимые данные для нашего сервиса (скрипты, файлы, снепшоты и т.д.) в соответствующие директории.

  • Создаём дефолтный файл конфигурации и тоже помещаем его в эту иерархию.

  • Монтируем корень этой иерархии на хосте в Docker-контейнер.

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

Конфигурация

Здесь мы использовали простое решение.

Через Entrypoint задаём переменные окружения, аргументы запуска и подготовленный файл конфигурации. Когда контейнер поднимется, он выполнит всё, что указано в Entrypoint.

После этого сервис можно считать сконфигурированным. Пример:

Адрес сервиса

Итак, сервис поднялся в контейнере. У него есть рабочее окружение и определённая конфигурация для теста. Как найти другие сервисы?

Внутри Docker network всё просто.

  • При создании контейнера мы генерируем ему уникальное имя и к этому имени обращаемся как к адресу: используем имя контейнера как hostname.

  • Порты мы знаем заранее, так как на предыдущем этапе подготавливали файлы конфигурации и указывали порты для наших сервисов.

Запуск самих тестов у нас происходит вне контейнера для уменьшения оверхеда. Но можно запускать их и в контейнере тогда тесты узнают адреса сервисов, как описано выше.

Если тесты запускаются на локальной машине, они не могут обращаться к сервису по имени его контейнера, так как адресация по имени контейнера внутри Docker network это абстракция самого Docker. Нам нужно найти номер порта на локальном хосте, который соответствует порту сервиса в Docker network. После поднятия контейнера мы получим соответствие внутреннего порта сервиса (inner port) порту на локальном хосте (external port). Последний и будем использовать в тестах.

Внешние сервисы

Наверняка в вашей инфраструктуре присутствуют сторонние сервисы, например базы данных и service discovery. Их конфигурация в идеале должна совпадать с той, что на продакшене. Если сервис простой (например, Consul в конфигурации одного процесса) мы его тоже можем запустить с помощью testcontainers-go. Но если сервис многокомпонентный (например, Kafka из нескольких брокеров, где требуется ZooKeeper), то можно не страдать и использовать для этого Docker Compose.

Как правило, в процессе интеграционного тестирования не требуется обширный доступ к работе с внешними сервисами, так что Docker Compose хорошо подходит для наших целей.

Фаза загрузки

Контейнер поднят. Но означает ли это, что сервис готов принимать наши запросы? В общем случае нет. У многих сервисов есть фаза инициализации. Она может занимать продолжительное время. Если мы не дожидаемся окончания загрузки сервиса и запускаем тестирование, то получаем нестабильное поведение тестов.

Что делать?

  1. Самое простое решение использовать sleep. После запуска контейнера ждём некоторое время. Как только оно прошло, считаем, что сервисы готовы к работе. Это плохое решение, поскольку запуск тестов происходит на разных машинах, а скорость загрузки сервисов может изменяться как в большую, так и в меньшую сторону.

  2. Открывать порты сервиса по мере готовности. Как только сервис прошёл фазу загрузки и готов принимать запросы клиентов, он открывает порты. Для тестового окружения это знак разрешения на запуск тестов. Однако есть нюанс: при создании контейнера Docker сразу открывает external port для сервиса, даже если последний ещё не начал слушать соответствующий internal port в контейнере. Поэтому в тестах сразу будет установлено соединение и попытка чтения из соединения приведёт к EOF. Когда сервис откроет internal port, тестовый фреймворк сможет отправить запрос. Только после этого мы будем считать, что сервис готов к работе.

  3. Запрашивать статус сервиса. Cервис сразу открывает порты, на запрос статуса отвечает Готов, если уже загрузился, и Не готов, если нет. В тестах мы будем периодически спрашивать сервис о его статусе и, как только получим ответ Готов, перейдём к фазе тестирования.

  4. Регистрировать в стороннем сервисе или базе данных. Мы регистрируем сервисы в Consul. Можно использовать:

    1. Факт появления сервиса в Consul как сигнал о готовности. Состояние сервиса можно отслеживать с помощью блокирующего запроса с тайм-аутом. Как только сервис зарегистрируется, Consul пришлет ответ на запрос с информацией об изменении статуса сервиса.

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

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

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

Поднятие всех сервисов

Мы обсудили, как подготавливать всё необходимое для работы сервиса, как его запускать и узнавать о его готовности к работе. Поднимать мы можем как свои, так и сторонние сервисы, знаем адреса сервисов как внутри тестовой среды, так и из тестов.

В какой последовательности осуществлять запуск? Идеальный вариант не иметь строгой последовательности. Это позволяет запускать сервисы параллельно и значительно сократить время создания инфраструктуры (время запуска контейнера + время загрузки сервиса). Чем меньше связей, тем проще добавлять новый сервис в инфраструктуру.

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

Инфраструктура во время тестирования

Во время прохождения тестов мы хотим работать с нашей инфраструктурой: залезть в неё и поиграться. Когда, если не сейчас?

Изменение конфигурации сервиса

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

Добавление нового сервиса

Нам уже ничего не стоит добавить новый сервис. Мы научились создавать сервисы на этапе настройки инфраструктуры. Здесь точно такой же сценарий: подготавливаем окружение для нового сервиса, запускаем контейнер и работаем с ним во время прохождения тестов.

Работа с сетью

Включение контейнеров в сеть и их исключение из неё, приостановка (pause/unpause) работы контейнеров, iptables позволяют нам эмулировать сетевые ошибки и проверять реакцию системы на них.

Инфраструктура после тестирования

Если в рамках одного теста мы добавили новый сервис, не нужно передавать его по наследству следующему тесту: нужно быть вежливыми и убрать за собой. Это касается и данных. Тесты могут запускаться в произвольном порядке и не должны влиять друг на друга, прогоны должны быть воспроизводимыми.

  • Если было изменение конфигурации сервиса, делаем откат на предыдущую (дефолтную) конфигурацию.

  • Если было добавление нового сервиса, удаляем его.

  • Если были любые изменения в сети (iptables, приостановка контейнеров и т. д.), отменяем их.

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

После завершения test suite работа всех сервисов в инфраструктуре тоже завершается, контейнеры убиваются, тестовая сеть опускается. Если test suite не успел завершиться по истечении тайм-аута или в результате ошибки, действуем точно так же. Только в случае явного указания фреймворку оставить контейнеры после прогона (например, для отладки), инфраструктура остаётся.

Ускорение тестов

Ждать вечность, пока пройдут интеграционные тесты, как правило, никому не хочется. Хотя в это время можно выпить кофе и сделать ещё много чего интересного.

Что мы можем сделать для ускорения тестов?

  • Группировать read-only-тесты и запускать их параллельно в рамках одного теста (в Go при помощи горутин это делается максимально просто). Эти тесты должны работать на изолированном множестве данных.

  • Предоставлять обширную настройку сервиса. Тогда в тестах мы сможем выставить меньшие значения тайм-аута, retry и ожидания со стороны сервиса, что сократит и время прохождения тестов.

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

  • Запускать несколько тестовых инфраструктур параллельно (если позволяют ресурсы). По сути, это параллельный прогон test suite.

  • Переиспользовать контейнеры.

  • Задаться вопросом, действительно ли в контейнере необходим новый сервис, или достаточно мока. Под моками я имею в виду не интерфейсные моки, которые мы используем в юнит-тестировании, а отдельные серверы. Мок прикидывается одним из наших сервисов и умеет работать по его протоколу. Для остальных поднятых сервисов в текущей тестовой инфраструктуре он неотличим от оригинального. Мок позволяет задать нужную нам логику поведения реального сервиса и не поднимать его в контейнере.

В тестах мы запускаем мок на определённом адресе. Этот адрес уже поднятые сервисы в текущей инфраструктуре узнают через конфиг или service discovery (Consul в нашем случае) и могут отправлять на него запросы.

Мок получает запрос и вызывает handler, который мы указали. На Go в тесте это выглядит примерно так:

Handler из примера считает, что получает запрос статистики, обрабатывает его согласно логике тестов, формирует ответ и указывает серверу, какое действие нужно с ним выполнить: отправить сразу или с задержкой; не отправлять вовсе; завершить соединение.

Контроль над действиями сервера, будь то завершение соединения или медленная отправка, даёт дополнительную возможность проверить реакцию тестируемых сервисов на сбои в работе сети. Сервер выполняет запрошенные действия, пакует ответ от handler и отправляет его клиенту. Мок (сервер) уничтожается в defer по окончании теста.

Мы используем моки для всех наших сервисов это помогает выиграть много времени при тестировании.

Реализация

Наш фреймворк располагается в том же репозитории, что и тестируемые сервисы, лежит в отдельной директории autotests. Внутри неё есть несколько модулей:

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

Mock содержит реализацию мока-сервера для каждого нестороннего сервиса.

В suite находится общая реализация. Он умеет работать с сервисами, ожидать их загрузки, проверять работоспособность и многое другое.Environment хранит информацию о текущем тестовом окружении (какие сервисы запущены), отвечает за сеть.

Также есть вспомогательные модули и те, что оказывают помощь в генерации данных.

Помимо модулей самого фреймворка, на момент написания статьи у нас были созданы 21 test suites, в том числе и smoke test suite. Каждый создаёт свою инфраструктуру с необходимым набором сервисов. Тесты находятся в файлах внутри test suite.

Запуск конкретного test suite выглядит примерно так:

go test -count=1 -race -v ./testsuite $(TESTSFLAGS) -autotests.timeout=15m

Поскольку сервисы коллег из других отделов мы хотели тоже перевести на наш фреймворк, core-функционал фреймворка был вынесен в общий core-репозиторий.

Отладка

Ура! Мы научились подготавливать инфраструктуру и прогонять тесты. Приятно видеть зелёный результат интеграции. Но так бывает не всегда, поэтому начинаем разбираться с падениями. Первое, что приходит на ум, изучить логи сервисов.

Предположим, suite содержит огромное количеством тестов, и один из них не получил от сервиса ожидаемого ответа. Где-то в недрах лога сервиса нам нужно найти кусочек, который соответствует времени прохождения упавшего теста. В этом помогает простой и удобный инструмент маркеры.

  • Добавляем в сервис команду log_notice, при получении которой он записывает в свой лог сообщение из запроса.

  • Перед началом теста отправляем log_notice с названием теста во все поднятые сервисы. По окончании теста делаем то же самое.

Теперь у нас есть маркеры внутри лога можно легко восстановить ход событий и воспроизвести поведение сервиса при необходимости.

Как быть, если сервис не смог подняться и не успел сделать запись в лог? Скорее всего, он записал в stderr/stdout дополнительную информацию. Команда docker logs позволяет получать данные из стандартных потоков ввода-вывода это поможет нам понять, что случилось.

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

Указываем в конфигурации фреймворка необходимость оставлять инфраструктуру после прогона всех тестов в suite. Благодаря этому мы получаем полный доступ к системе. Можно узнать статус сервиса, получить данные из него, отправлять различные запросы, анализировать файлы сервиса на диске, а так же использовать gdb/strace/tcpdump и профилирование. Дальше мы строим гипотезу, пересобираем образ, запускаем тесты и итеративно находим корень проблемы.

Чтобы отладка не превращалась в мучительный отлов багов, тесты должны быть максимально воспроизводимыми. Например, если данные генерируются с помощью random, в случае ошибки нужно выводить информацию о seed и/или о том, какие данные были запрошены.

QA

Как с интеграционным фреймворком работает тестировщик? Ему не нужно самостоятельно вручную поднимать все сервисы. Интеграционные тесты делают это за него и помогают создать нужную инфраструктуру. Если suite на запланированную инфраструктуру ещё не написан, он быстро добавляет его сам.

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

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

Чтобы ускорить работу, сначала наши разработчики пишут positive-тесты, а затем тестировщики проверяют более сложные кейсы. Совместная разработка тестов и фреймворка в действии.

CI

Оказалось, что встроить интеграционные тесты в CI очень просто. Мы используем TeamCity. Код фреймворка находится в репозитории с кодом сервисов. Сначала собираются сервисы, создаются образы, далее происходят сборка фреймворка и наконец его запуск.

Мы научили TeamCity понимать по выводу тестового фреймворка, какие тесты прошли, а какие нет. После окончания прогона отображается, сколько и каких тестов не прошло. Данные всех сервисов после прогона каждого suite сохраняются и публикуются в TeamCity в качестве артефактов для конкретной сборки и прогона.

Итоги

Ниже результаты проделанной работы.

  • Жить стало спокойнее. Меньше проблем с интеграцией просачивается на продакшен. Как следствие более стабильный прод.

  • Мы научились поднимать разную инфраструктуру и покрывать больше сценариев за меньшее время.

  • Мы работаем с инфраструктурой во время прохождения тестов. Это даёт больше возможностей для реализации разных тест-кейсов.

  • Мы ловим больше багов на этапе разработки. Positive-сценарии пишут сами разработчики, отлавливая часть ошибок и сразу их решая. Уменьшается round-trip бага.

  • Мы избавили тестировщиков от написания positive-кейсов. QA-инженеры могут сосредоточиться на более сложных сценариях.

  • Перестали блокироваться на этапе тестирования, когда параллельно разрабатываются задачи для разных сервисов и переводятся на QA-инженеров примерно в одно время.

  • Мы написали MVP фреймворка для интеграционного тестирования довольно быстро за пару недель. Задача оказалась не слишком трудоёмкой.

  • Мы используем фреймворк уже больше года.

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

Однако интеграционное тестирование имеет ряд минусов, которые стоит учитывать.

  • Увеличение времени прогона тестов. Системы сложные, запросы выполняются в нескольких сервисах.

  • Нестабильность, так как система состоит из асинхронных компонентов. С этим можно и нужно бороться. Мы тоже это делаем: доля нестабильных тестов у нас близка к нулю.

  • Сложность написания тестов. Нужно понимать, как работает система в целом, каково её ожидаемое поведение и как её можно сломать.

  • Возможность расхождения инфраструктуры в тестах и на продакшене. Если не все сервисы на проде в контейнерах, то тестовое окружение не на 100% совпадает с продакшеном. У нас как раз часть сервисов на проде не в контейнерах, но мы пока не сталкивались с проблемами из-за их тестирования в контейнерах.

И, наконец, главный вопрос, на который нужно ответить: необходим ли вам фреймворк для интеграционных тестов?

Если в вашем проекте увеличивается (или уже заметно увеличилось) количество сервисов, множатся связи между ними и требуется автоматизация тестирования, значит, стоит попробовать реализовать интеграционные тесты.

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

Успехов и удачи!

Подробнее..

Идеальный пайплайн в вакууме

03.06.2021 22:22:36 | Автор: admin
Даже не зовите меня, если ваш pipeline не похож на это.Даже не зовите меня, если ваш pipeline не похож на это.

На собеседованиях на позицию, предполагающую понимание DevOps, я люблю задавать кандидатам такой вопрос (а иногда его еще задают и мне):

Каким, по вашему мнению, должен быть идеальный пайплайн от коммита до продашкена?/Опишите идеальный CI/CD / etc

Сегодня я хочу рассказать про своё видение идеального пайплайна. Материал ориентирован на людей, имеющих опыт в построении CI/CD или стремящихся его получить.

Почему это важно?

  1. Вопрос об идеальном пайплайне хорош тем, что он не содержит точного ответа.

  2. Кандидат начинает рассуждать, а в крутых специалистах ценится именно умение думать.

  3. Когда в вопрос добавляется такое абсолютное прилагательное, как "идеальный", то мы сразу развязываем кандидатам руки в просторе для творчества и фантазий. У соискателей появляется возможность показать, какие улучшения они видят (или не видят) в текущей работе, и что хотели бы добавить сами. Также мы можем узнать, есть ли у нашего предполагаемого будущего коллеги мотивация к улучшениям процессов, ведь концепция "работает не трогай" не про динамичный мир DevOps.

  4. Организационная проверка. Позволяет узнать, насколько широка картина мира у соискателя. Условно: от создания задачи в Jira до настроек ноды в production. Сюда же можно добавить понимание стратегий gitflow, gitlabFlow, githubFlow.

Итак, прежде чем перейти к построению какого-либо процесса CI, необходимо определиться, а какие шаги нам доступны?

Что можно делать в CI?

  • сканить код;

  • билдить код;

  • тестить код;

  • деплоить приложение;

  • тестить приложение;

  • делать Merge;

  • просить других людей подтверждать MR через code review.

Рассмотрим подробнее каждый пункт.

Code scanning

На этой стадии основная мысль никому нельзя верить.

Даже если Вася Senior/Lead Backend Developer. Несмотря на то, что Вася хороший человек/друг/товарищ и кум. Человеческий фактор, это все еще человеческий фактор.

Необходимо просканировать код на:

  • соотвествие общему гайдлайну;

  • уязвимости;

  • качество.

Мне нужны твои уязвимости, сапоги и мотоциклМне нужны твои уязвимости, сапоги и мотоцикл

Задачи на этой стадии следует выполнять параллельно.

И триггерить только если меняются исходные файлы, или только если было событие git push.

Пример для gitlab-ci

stages:  - code-scanning.code-scanning: only: [pushes] stage: code-scanning 

Linters

Линтеры это прекрасная вещь! Про них уже написано много статей. Подробнее можно почитать в материале "Холиварный рассказ про линтеры".

Самая важная задача линтеров приводить код к единообразию.

После внедрения этой штучки разработчики начнут вас любить. Потому что они наконец-то начнут понимать друг друга. Или ненавидеть, решив, что вы вставляете им палки в колеса линтеры в CI. Это уже зависит от ваших soft skills, культуры и обмена знаниями.

Инструменты

Инструмент

Особенности

eslint

JavaScript

pylint

Python

golint

Golang

hadolint

Dockerfile

kubeval

Kubernetes manifest

shellcheck

Bash

gixy

nginx config

etc

Code Quality

code quality этими инструментами могут быть как продвинутые линтеры, так и совмещающие в себе всякие ML-модели на поиск слабых мест в коде: утечек памяти, небезопасных методов, уязвимостей зависимостей и т.д, перетягивая на себя еще code security компетенции.

Инструменты

Инструмент

Особенности

Price

SonarQube

Поиск ошибок и слабых мест в коде

От 120

CodeQL

Github native, поиск CVE уязвимостей

OpenSource free

etc

Code Security

Но существуют также и отдельные инструменты, заточенные только для code security. Они призваны:

  1. Бороться с утечкой паролей/ключей/сертификатов.

  2. Cканировать на известные уязвимости.

Неважно, насколько большая компания, люди в ней работают одинаковые. Если разработчик "ходит" в production через сертификат, то для своего удобства разработчик добавит его в git. Поэтому придется потратить время, чтобы объяснить, что сертификат должен храниться в vault, а не в git

Инструменты

Инструмент

Особенности

Price

gitleaks

Используется в Gitlab Security, может сканить промежуток от коммита "А" до коммита "Б".

Free

shhgit

Запустили недавно Enterpise Edition.

От $336

etc

Сканер уязвимостей необходимо запускать регулярно, так как новые уязвимости имеют свойство со временем ВНЕЗАПНО обнаруживаться.

Да-да, прямо как Испанская Инквизиция!Да-да, прямо как Испанская Инквизиция!

Code Coverage

Ну и конечно, после тестирования, нужно узнать code coverage.

Процент исходного кода программы, который был выполнен в процессе тестирования.

Инструменты

Инструмент

Особенности

Price

go cover

Для Golang. Уже встроен в Golang.

Free

cobertura

Работает на основе jcoverage. Java мир

Free

codecov

Старая добрая классика

Free до 5 пользователей

etc

Unit test

Модульные тесты имеют тенденцию перетекать в инструменты code quality, которые умеют в юнит тесты.

Инструменты

Инструмент

Особенности

phpunit

PHP (My mom says I am special)

junit

Java (многие инстурменты поддерживают вывод в формате junit)

etc

Build

Этап для сборки artifacts/packages/images и т.д. Здесь уже можно задуматься о том, каким будет стратегия версионирования всего приложения.

За модель версионирования вы можете выбрать:

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

Инструменты для сборки образов

Инструмент

Особенности

docker build

Почти все знают только это.

buildx / buildkit

Проект Moby предоставил свою реализацию. Поставляется вместе с докером, включается опцией DOCKER_BUILDKIT=1.

kaniko

Инструмент от Google, позволяет собирать в юзерспейсе, то есть без докер-демона.

werf

Разработка коллег из Флант'а. Внутри stapel. All-in-one: умеет не только билдить, но и деплоить.

buildah

Open Container Initiative, Podman.

etc

Итак, сборка прошла успешно идем дальше.

Scan package

Пакет/образ собрали. Теперь нужно просканировать его на уязвимости. Современные registry уже содержат инструментарий для этого.

Инструменты

Инструмент

Особенности

Цена

harbor

Docker Registry, ChartMuseum, Robot-users.

Free

nexus

Есть все в том числе и Docker.

Free и pro

artifactory

Комбайн, чего в нем только нет.

Free и pro

etc

Deploy

Стадия для развертывания приложения в различных окружениях.

Деплоим контейнер в прод, как можем.Деплоим контейнер в прод, как можем.

Не все окружения хорошо сочетаются со стратегиями развертывания.

  • rolling классика;

  • recreate все что угодно, но не production;

  • blue/green в 90% процентов случаев этот способ применим только к production окружениям;

  • canary в 99% процентов случаев этот способ применим только к production окружениям.

Stateful

Нужно еще помнить, что даже имея одинаковый код в stage и production, production может развалиться именно из-за того, что stateful у них разный. Миграции могут отлично пройти на пустой базе, но появившись на проде, сломать зеленые кружочки/галочки в пайплайне. Поэтому для stage/pre-production следует предоставлять обезличенный бэкап основной базы.

И не забудьте придумать способ откатывания ваших релизов на последний/конкретный релиз.

Инструменты

Инструмент

Особенности

helmwave

Docker-compose для helm. Наша разработка.

helm

Собираем ямлики в одном месте.

argoCD

"Клуб любителей пощекотать GitOps".

werf.io

Было выше.

kubectl / kustomize

Для тех, кто любит сам придумывать шаблонизацию.

etc

На правах рекламы скажу что helmwav'у очень не хватает ваших звезд на GitHub. Первая публикация про helmwave.

Integration testing

Приложение задеплоили. Оно где-то живет в отдельном контуре.Наступает этап интеграционного тестирования. Тестирование может быть как ручным, так и автоматизированным. Автоматизированные тесты можно встроить в пайплайн.

Инструменты

Инструмент

Особенности

Selenium

Можно запустить в кубере.

Selenoid

Беды с образами. Требует Docker-in-Docker.

etc

Performance testing (load/stress testing)

Данный вид тестирования имеет смысл проводить на stage/pre-production окружениях. С тем условием, что ресурсные мощности на нем такие же, как в production.

Инструменты, чтобы дать нагрузку

Инструмент

Особенности

wrk

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

k6.io

Cтильно-модно-JavaScript! Используется в AutoDevOps.

Artillery.io

Снова JS. Сравнение с k6

jmeter

OldSchool.

yandex-tank

Перестаньте дудосить конурентов.

etc

Инструменты, чтобы оценить работу сервиса

Инструмент

Особенности

sitespeed.io

Внутри: coach, browserTime, compare, PageXray.

Lighthouse

Тулза от Google. Красиво, можешь показать это своему менеджеру. Он будет в восторге. Жаль, только собаки не пляшут.

etc

Code Review / Approved

Одним из важнейших этапов являются Merge Request. Именно в них могут производиться отдельные действия в pipeline перед слиянием, а также назначаться группы лиц, требующих одобрения перед cлиянием.

Список команд/ролей:

  • QA;

  • Security;

  • Tech leads;

  • Release managers;

  • Maintainers;

  • DevOps;

  • etc.

Очевидно, что созывать весь консилиум перед каждым MR не нужно, каждая команда должна появится в свой определённый момент MR:

  • вызывать безопасников имеет смысл только перед сливанием в production;

  • QA перед release ветками;

  • DevOps'ов беспокоить, только если затрагиваются их компетенции: изменения в helm-charts / pipeline / конфигурации сервера / etc.

Developing flow

Чаще всего каждая компания, а то и каждый проект в компании, решает изобрести свой велосипед-флоу. Что после нескольких итераций приходит к чему-то, что может напоминать gitflow, gitlabFlow, githubFlow или все сразу.

Это и не хорошо, и не плохо это специфика проекта. Есть мнения, что gitflow не торт. GithubFlow для относительно маленьких команд. А про gitlabFlow мне нечего добавить, но есть наблюдение, что его не очень любят продакты - за то, что нельзя отслеживать feature-ветки.

Если вкратце, то:

  • Gitflow: feature -> develop -> release-vX.X.X -> master (aka main) -> tag;

  • GitHubFlow: branch -> master (aka main);

  • GitLabFlow: environmental branches.

TL;DR

Общий концепт

_

Feature-ветка

Pre-Production -> Production

P.S.

Если я где-то опечатался, упустил важную деталь или, по вашему мнению, пайплайн недостаточно идеальный, напишите об этом мне сделаю update.

Разработчик создал ветку и запушил в нее код. Что дальше?

Оставляйте варианты ваших сценариев в комментариях.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru