- PVSM.RU - https://www.pvsm.ru -
Всем привет!
Я бы хотел немного рассказать о проекте, над которым я работал последние полгода. Проект я делаю в свободное время, но мотивация к его созданию пришла из наблюдений, сделанных на основной работе.
На рабочем проекте мы используем архитектуру микросервисов, и одна из главных проблем, которая проявилась со временем и выросшим количеством этих самых сервисов — это тестирование. Когда некий сервис зависит от пяти-семи других сервисов, плюс ещё какая-нибудь база данных (а то и несколько) в придачу, то тестировать это в "живом", так сказать виде, весьма неудобно. Приходится обкладываться моками со всех сторон так плотно, что самого теста и не разглядеть. Ну или каким-то образом организовывать тестовое окружение, где все зависимости могли бы реально быть запущены.
Собственно для облегчения второго варианта я как раз и сел писать xenvman [1]. Если совсем в двух словах, то это что-то вроде гибрида docker-compose и test containers [2], только без привязки к Java (или любому другому языку) и с возможностью динамически создавать и конфигурировать окружения через HTTP API.
xenvman
написан на Go и реализован как простой HTTP сервер, что позволяет пользоваться всей доступной функциональностью из любого языка, умеющего разговаривать на этом протоколе.
Основное, что xenvman умеет, это:
Главным действующим лицом в xenvman является окружение (environment). Это такой-себе изолированный пузырь, в котором запускаются все необходимые зависимости (упакованные в Docker контейнеры) вашего сервиса.
На рисунке выше, показан xenvman сервер и активные окружения, в которых запущены разные сервисы и базы данных. Каждое окружение было создано прямо из кода интеграционных тестов, и будет удалено по их завершению.
Что непосредственно входит в состав окружения, определяется шаблонами (templates), которые представляют собой небольшие скрипты на JS. xenvman имеет встроенный интерпретатор этого языка, и при получении запроса на создание нового окружения, он просто выполняет указанные шаблоны, каждый из которых добавляет один или более контейнеров в список на выполнение.
JavaScript был выбран для того, чтобы позволить динамически менять/добавлять шаблоны без необходимости пересборки сервера. Кроме того, в шаблонах как правило используются только базовые возможности и типы данных языка (старый добрый ES5, никакого DOM, React и прочей магии), поэтому работа с шаблонами не должна вызвать особых трудностей даже у тех, кто совсем на знает JS.
Шаблоны параметризуемы, то есть мы можем полностью контролировать логику шаблона путём передачи тех или иных параметров в нашем HTTP запросе.
Одна из наиболее удобных возможностей xenvman, на мой взгляд, это создание Docker образов прямо по ходу конфигурирования окружения. Зачем это может быть нужно?
Ну вот например у нас на проекте, чтобы получить образ сервиса, нужно закомитить изменения в отдельную ветку, запушить и подождать пока Gitlab CI соберет и зальёт образ.
Если изменился только один сервис, то занять это может 3-5 минут.
А если мы активно пилим новые фичи в наш сервис, или же пытаемся понять почему он не работает, добавляя старый добрый fmt.Printf
туда-сюда, или ещё как-нибудь часто изменяя код, то даже задержка в 5 минут будет здорово гасить производительность (нашу, как писателей кода). Вместо этого, мы можем просто добавить всю необходимую отладку в код, скомпилировать его локально, и потом просто приложить готовый бинарь в HTTP запрос.
Получив такое добро, шаблон возьмёт этот бинарь и прямо на ходу создаст временный образ, из которого мы уже сможем запустить контейнер как ни в чем не бывало.
На нашем проекте, в основном шаблоне для сервисов, например, мы проверяем присутствует ли бинарь в параметрах, и если да, то собираем образ на ходу, иначе просто скачиваем latest
версию dev
ветки. Дальнейший код для создания контейнеров идентичен для обоих вариантов.
Для наглядности, давайте рассмотрим микро-примерчик.
Скажем, пишем мы какой-то чудо-сервер (назовём-ка его — wut
), которому нужна база данных, чтобы всё там хранить. Ну и в качестве базы, выбрали мы MongoDB. Стало быть для полноценного тестирования нам нужен работающий сервер Mongo. Можно, конечно, установить и запустить его локально, но для простоты и наглядности примера мы предположим, что по какой-то причине это сделать сложно (при других, более сложных конфигурациях в реальных системах это будет больше похоже на правду).
Значит мы попробуем использовать xenvman для того, чтобы создать окружение с запущенным Mongo и нашим wut
сервером.
Первым делом нам надо создать базовый каталог [3], в котором будут храниться все шаблоны:
$ mkdir xenv-templates && cd xenv-templates
Дальше создадим два шаблона, один для Mongo, другой для нашего сервера:
$ touch mongo.tpl.js wut.tpl.js
Откроем mongo.tpl.js
и запишем туда следующее:
function execute(tpl, params) {
var img = tpl.FetchImage(fmt("mongo:%s", params.tag));
var cont = img.NewContainer("mongo");
cont.SetLabel("mongo", "true");
cont.SetPorts(27017);
cont.AddReadinessCheck("net", {
"protocol": "tcp",
"address": '{{.ExternalAddress}}:{{.Self.ExposedPort 27017}}'
});
}
В шаблоне должна присутствовать функция execute() с двумя параметрами.
Первый — это экземпляр tpl объекта, через который происходит конфигурация окружения. Второй аргумент (params) это просто JSON объект, с помощью которого мы будем параметризовать наш шаблон.
В строке
var img = tpl.FetchImage(fmt("mongo:%s", params.tag));
мы просим xenvman скачать docker образ mongo:<tag>
, где <tag>
это версия образа, который мы хотим использовать. В принципе, эта строка эквивалентна команде docker pull mongo:<tag>
, с той лишь разницей, что все функции tpl
объекта по-сути декларативны, то есть реально образ будет скачан только после того как xenvman выполнит все шаблоны, указанные в конфигурации окружения.
После того, как у нас есть образ, мы можем создать контейнер:
var cont = img.NewContainer("mongo");
Опять-таки, контейнер моментально не будет создан в этом месте, мы просто декларируем намерение создать его, так сказать.
Далее мы вешаем ярлык на наш контейнер:
cont.SetLabel("mongo", "true");
Ярлыки используются для того, чтобы контейнеры могли находить друг-друга в окружении, например чтобы вписать IP адрес или имя хоста в конфигурационный файл.
Теперь нам нужно вывесить внутренний порт Mongo (27017) наружу. Это легко делается так:
cont.SetPorts(27017);
Перед тем, как xenvman отрапортует нам об успешном создании окружения, было бы здорово убедиться, что все сервисы не просто запущены, а уже и готовы принимать запросы. В xenvman для этого имеются проверки готовности [4].
Добавим одно такую для нашего mongo контейнера:
cont.AddReadinessCheck("net", {
"protocol": "tcp",
"address": '{{.ExternalAddress}}:{{.Self.ExposedPort 27017}}'
});
Как мы видим, здесь в строке адреса имеются заглушки, в которые будут динамически подставлены нужные значения прямо перед запуском контейнеров.
Вместо {{.ExternalAddress}}
будет подставлен внешний адрес хоста, на котором запущен xenvman, а вместо {{.Self.ExposedPort 27017}}
будет подставлен внешний порт, который был проброшен на внутренний 27017.
Подробнее об интерполяции можно почитать здесь [5].
В итоге всего этого, мы сможем подключаться к Mongo, запущенному в окружении, прямо снаружи, например с хоста, на котором мы запускаем наш тест.
Так-c, разобравшись с монгой, напишем ещё шаблончик для нашего wut
сервера.
Так как мы хотим собирать образ на ходу, шаблон будет немного отличаться:
function execute(tpl, params) {
var img = tpl.BuildImage("wut-image");
img.CopyDataToWorkspace("Dockerfile");
// Extract server binary
var bin = type.FromBase64("binary", params.binary);
img.AddFileToWorkspace("wut", bin, 0755);
// Create container
var cont = img.NewContainer("wut");
cont.MountData("config.toml", "/config.toml", {"interpolate": true});
cont.SetPorts(params.port);
cont.AddReadinessCheck("http", {
"url": fmt('http://{{.ExternalAddress}}:{{.Self.ExposedPort %v}}/', params.port),
"codes": [200]
});
}
Так как здесь мы собираем образ, то мы используем BuildImage()
вместо FetchImage()
:
var img = tpl.BuildImage("wut-image");
Для того, чтобы собрать образ, нам будут нужны несколько файлов:
Dockerfile — собственно инструкция как собирать образ
config.toml — конфигурационный файл для нашего wut
сервера
С помощью метода img.CopyDataToWorkspace("Dockerfile");
мы копируем Dockerfile из каталога данных шаблона [6] во временный рабочий каталог [7].
Каталог данных — это каталог, в котором мы можем хранить все файлы, необходимые нашему шаблону в работе.
Во временный рабочий каталог мы копируем файлы (с помощью img.CopyDataToWorkspace()), которые попадут в образ.
Далее следует вот такое:
// Extract server binary
var bin = type.FromBase64("binary", params.binary);
img.AddFileToWorkspace("wut", bin, 0755);
Мы передаём бинарь нашего сервера прямо в параметрах, в закодированном (base64) виде. А в шаблоне мы его просто раскодируем, и получившуюся строку сохраняем в рабочий каталог в виде файла под именем wut
.
Потом создаем контейнер и монтируем в него конфигурационный файл:
var cont = img.NewContainer("wut");
cont.MountData("config.toml", "/config.toml", {"interpolate": true});
Вызов MountData()
означает, что файл config.toml
из каталога данных будет смонтирован внутрь контейнера под именем /config.toml
. Флаг interpolate
указывает xenvman серверу, что перед монтированием в файле следует заменить все имеющиеся там заглушки.
Вот как может выглядеть конфиг:
{{with .ContainerWithLabel "mongo" "" -}}
mongo = "{{.Hostname}}/wut"
{{- end}}
Тут мы ищем контейнер с ярлыком mongo
, и подставляем имя его хоста, какое бы оно не было в данном окружении.
После подстановки, файл может выглядеть как:
mongo = “mongo.0.mongo.xenv/wut”
Далее мы опять вывешиваем порт и заводим проверку готовности, на этот раз HTTP:
cont.SetPorts(params.port);
cont.AddReadinessCheck("http", {
"url": fmt('http://{{.ExternalAddress}}:{{.Self.ExposedPort %v}}/', params.port),
"codes": [200]
});
На этом наши шаблоны готовы, и мы можем использовать их в коде интеграционных тестов:
import "github.com/syhpoon/xenvman/pkg/client"
import "github.com/syhpoon/xenvman/pkg/def"
// Создаём xenvman клиент
cl := client.New(client.Params{})
// Требуем создать для нас окружение
env := cl.MustCreateEnv(&def.InputEnv{
Name: "wut-test",
Description: "Testing Wut",
// Указываем, какие шаблоны добавить в окружение
Templates: []*def.Tpl{
{
Tpl: "wut",
Parameters: def.TplParams{
"binary": client.FileToBase64("wut"),
"port": 5555,
},
},
{
Tpl: "mongo",
Parameters: def.TplParams{"tag": “latest”},
},
},
})
// Завершить окружение после окончания теста
defer env.Terminate()
// Получаем данные по нашему wut контейнеру
wutCont, err := env.GetContainer("wut", 0, "wut")
require.Nil(t, err)
// Тоже самое для монго контейнера
mongoCont, err := env.GetContainer("mongo", 0, "mongo")
require.Nil(t, err)
// Теперь формируем адреса
wutUrl := fmt.Sprintf("http://%s:%d/v1/wut/", env.ExternalAddress, wutCont.Ports[“5555”])
mongoUrl := fmt.Sprintf("%s:%d/wut", env.ExternalAddress, mongoCont.Ports["27017"])
// Всё! Теперь мы можем использовать эти адреса, что подключиться к данным сервисам из нашего теста и делать с ними, что захочется
Может показаться, что написание шаблонов будем занимать слишком много времени.
Однако при правильном дизайне, это одноразовая задача, а потом те же самые шаблоны можно переиспользовать ещё и ещё (и даже для разных языков!) просто тонко настраивая их путём передачи тех или иных параметров. Как видно в примере выше, непосредственно код теста очень простой, из-за того, что всю шелуху по настройке окружения мы вынесли в шаблоны.
В этом небольшом примере показаны далеко не все возможности xenvman, более подробное пошаговое руководство доступно здесь (на англ.) [8]
На данный момент имеются клиенты для двух языков:
Но добавить новые не составит труда, так как предоставляемый API очень и весьма простой.
В версии 2.0.0 был добавлен простенький веб интерфейс, с помощью которого можно управлять окружениями и просматривать доступные шаблоны.
Конечно схожего много, но xenvman мне представляется немного более гибким и динамичным подходом, в сравнении со статической конфигурацией в файле.
Вот главные отличительные особенности, на мой взгляд:
Github страничка проекта [1]
Подробный пошаговый пример, на англ. [11]
Вот, собственно и все. В ближайшее время я планирую добавить возможность
вызывать шаблоны из шаблонов и тем самым позволить комбинировать их с большей эффективностью.
Постараюсь ответить на любые вопросы, и буду рад, если кому-нибудь ещё этот проект окажется полезным.
Автор: syhpoon
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/golang/307925
Ссылки в тексте:
[1] xenvman: https://github.com/syhpoon/xenvman
[2] test containers: https://www.testcontainers.org/
[3] базовый каталог: https://github.com/syhpoon/xenvman#Templates
[4] проверки готовности: https://github.com/syhpoon/xenvman#readiness-checks
[5] здесь: https://github.com/syhpoon/xenvman#interpolation
[6] каталога данных шаблона: https://github.com/syhpoon/xenvman#data-directory
[7] временный рабочий каталог: https://github.com/syhpoon/xenvman#workspace-directory
[8] здесь (на англ.): http://syhpoon.ca/posts/xenvman-tutorial/
[9] Go: https://godoc.org/github.com/syhpoon/xenvman/pkg/client
[10] Python: https://github.com/syhpoon/xenvman-python
[11] Подробный пошаговый пример, на англ.: http://syhpoon.ca/posts/xenvman-tutorial
[12] Источник: https://habr.com/ru/post/439236/?utm_campaign=439236
Нажмите здесь для печати.