Данная статья будет интересна как тестировщикам, так и разработчикам, но рассчитана в большей степени на автоматизаторов, которые столкнулись с проблемой настройки GitLab CI/CD для проведения интеграционного тестирования в условиях недостаточности инфраструктурных ресурсов и/или отсутствия платформы оркестрации контейнеров. Я расскажу, как настроить развертывание тестируемых окружений при помощи docker compose на одном единственном GitLab shell раннере и так, чтобы при развертывании нескольких окружений запускаемые сервисы друг другу не мешали.
Содержание
- Предпосылки
- GitLab Shell Runner
- Подготовка docker-compose.yml
- Подготовка Makefile
- Подготовка .gitlab-ci.yml
- Результат
Предпосылки
В моей практике частенько случалось "лечить" интеграционное тестирование на проектах. И зачастую первой и самой значительной проблемой является CI pipeline, в котором интеграционное тестирование разрабатываемого сервиса(ов) проводится в dev/stage окружении. Это вызывало не мало проблем:
- Из-за дефектов в том или ином сервисе в процессе интеграционного тестирования тестовый контур может быть испорчен битыми данными. Бывали случаи, когда отправка запроса с битым JSON-форматом вешал сервис, что приводило стенд полностью в нерабочее состояние.
- Замедлением работы тестового контура с ростом тестовых данных. Думаю, описывать пример с очисткой/откатом БД не имеет смысла. В своей практике я не встречал проекта, где эта процедура проходила бы гладко.
- Риск нарушить работоспособность тестового контура при тестировании общих настроек системы. Например, user/group/password/application policy.
- Тестовые данные от автотестов мешают жить ручным тестировщикам.
Кто-то скажет, что хорошие автотесты должны чистить данные после себя. У меня есть аргументы против:
- Динамические стенды весьма удобны в использовании.
- Не каждый объект можно удалить из системы через API. Например вызов на удаление объекта не реализован, так как противоречит бизнес логике.
- При создании объекта через API может создаваться огромное количество метаданных, которые удалить проблематично.
- Если тесты имеют зависимость между собой, то процесс очистки данных после выполнения тестов превращается в головную боль.
- Дополнительные (и, на мой взгляд, не оправданные) вызовы к API.
- И главный аргумент: когда тестовые данные начинают чистить прямо из БД. Это превращается в настоящий PK/FK цирк! От разработчиков слышно: «Я только табличку добавил/удалил/переименовал, почему 100500 интеграционных тестов попадало?»
По моему мнению, самое оптимальное решение — это динамическое окружение.
- Много кто использует docker-compose для запуска тестового окружения, но мало кто использует docker-compose при проведении интеграционного тестирования в CI/CD. И тут я не беру в расчет kubernetes, swarm и другие платформы оркестрации контейнеров. Не в каждой компании они есть. Хорошо бы было, если бы docker-compose.yml был универсальный.
- Если даже у нас есть свой QA раннер, как нам сделать так, чтобы сервисы запускаемые через docker-compose не мешали друг-другу?
- Как собирать логи тестируемых сервисов?
- Как чистить раннер?
У меня есть собственный GitLab раннер для своих проектов и с этими вопросами я столкнулся при разработке Java клиента для TestRail. А точнее при запуске интеграционных тестов. Вот далее и будем решать эти вопросы с примерами из данного проекта.
GitLab Shell Runner
Для раннера рекомендую линуксовую виртуалку с 4 vCPU, 4 GB RAM, 50 GB HDD.
На просторах интернета очень много информации по настройке gitlab-runner, поэтому коротко:
- Заходим на машинку по SSH
Если у вас менее 8 GB RAM, то рекомендую сделать swap 10 GB, чтобы не приходил OOM killer и не убивал нам задачи из-за нехватки RAM. Такое может случится, когда запускается одновременно более 5 задач. Задачи будут проходить помедленнее, зато стабильно.
Пример с OOM killerЕсли в логах задачи вы увидите
bash: line 82: 26474 Killed
, то просто выполните на раннереsudo dmesg | grep 26474
[26474] 1002 26474 1061935 123806 339 0 0 java Out of memory: Kill process 26474 (java) score 127 or sacrifice child Killed process 26474 (java) total-vm:4247740kB, anon-rss:495224kB, file-rss:0kB, shmem-rss:0kB
И если картина выглядит примерно так, то или добавляйте swap, или докидывайте RAM.
- Устанавливаем gitlab-runner, docker, docker-compose, make.
- Добавляем пользователя
gitlab-runner
в группуdocker
sudo groupadd docker sudo usermod -aG docker gitlab-runner
- Регистрируем gitlab-runner.
Открываем на редактирование
/etc/gitlab-runner/config.toml
и добавляем
concurrent=20 [[runners]] request_concurrency = 10
Это позволит запускать параллельные задачи на одном раннере. Более подробно читать тут.
Если у вас машинка помощнее, например 8 vCPU, 16 GB RAM, то эти цифры можно сделать как минимум в 2 раза больше. Но все зависит от того, что конкретно будет запускаться на данном раннере и в каком количестве.
Этого достаточно.
Подготовка docker-compose.yml
Основная задача — это docker-compose.yml, который будет использован как локально, так и в CI pipeline.
Для запуска нескольких экземпляров окружения будет использоваться переменная COMPOSE_PROJECT_NAME (см. makefile).
Пример моего docker-compose.yml
version: "3"
# Для корректной работы web (php) и fmt нужно,
# чтобы контейнеры имели общий исполняемый контент.
# В нашем случае, это директория /var/www/testrail
volumes:
static-content:
services:
db:
image: mysql:5.7.22
environment:
MYSQL_HOST: db
MYSQL_DATABASE: mydb
MYSQL_ROOT_PASSWORD: 1234
SKIP_GRANT_TABLES: 1
SKIP_NETWORKING: 1
SERVICE_TAGS: dev
SERVICE_NAME: mysql
migration:
image: registry.gitlab.com/touchbit/image/testrail/migration:latest
links:
- db
depends_on:
- db
fpm:
image: registry.gitlab.com/touchbit/image/testrail/fpm:latest
container_name: "testrail-fpm-${CI_JOB_ID:-local}"
volumes:
- static-content:/var/www/testrail
links:
- db
web:
image: registry.gitlab.com/touchbit/image/testrail/web:latest
# Если переменные TR_HTTP_PORT или TR_HTTPS_PORTS не определены,
# то сервис поднимается на 80 и 443 порту соответственно.
ports:
- ${TR_HTTP_PORT:-80}:80
- ${TR_HTTPS_PORT:-443}:443
volumes:
- static-content:/var/www/testrail
links:
- db
- fpm
Подготовка Makefile
Я использую Makefile, так как это весьма удобно как для локального управления окружением, так и в CI.
Далее комментарии инлайн
# У меня в проектах все вспомогательные вещи лежат в директории `.indirect`,
# в том числе и `docker-compose.yml`
# Использовать bash с опцией pipefail
# pipefail - фейлит выполнение пайпа, если команда выполнилась с ошибкой
SHELL=/bin/bash -o pipefail
# Если переменная CI_JOB_ID не определена
ifeq ($(CI_JOB_ID),)
# присваиваем значение local
CI_JOB_ID := local
endif
# Экспортируем переменную окружения
export COMPOSE_PROJECT_NAME = $(CI_JOB_ID)-testrail
# Останавливаем и удаляем контейнеры, сеть, volumes
docker-down:
docker-compose -f .indirect/docker-compose.yml down
# Предварительно выполняем docker-down (опционально)
docker-up: docker-down
# Забираем последние образы из docker-registry
docker-compose -f .indirect/docker-compose.yml pull
# Запускаем окружение
# force-recreate - принудительное пересоздание контейнеров
# renew-anon-volumes - не использовать volumes предыдущих контейнеров
docker-compose -f .indirect/docker-compose.yml up --force-recreate --renew-anon-volumes -d
# Ну и, на всякий случай, вывести что там у нас в принципе запущено на машинке
docker ps
# Коллектим логи сервисов
docker-logs:
mkdir -p ./logs
docker logs $${COMPOSE_PROJECT_NAME}_web_1 >& logs/testrail-web.log || true
docker logs $${COMPOSE_PROJECT_NAME}_fpm_1 >& logs/testrail-fpm.log || true
docker logs $${COMPOSE_PROJECT_NAME}_migration_1 >& logs/testrail-migration.log || true
docker logs $${COMPOSE_PROJECT_NAME}_db_1 >& logs/testrail-mysql.log || true
# Очистка раннера
docker-clean:
@echo Останавливаем все testrail-контейнеры
docker kill $$(docker ps --filter=name=testrail -q) || true
@echo Очистка докер контейнеров
docker rm -f $$(docker ps -a -f --filter=name=testrail status=exited -q) || true
@echo Очистка dangling образов
docker rmi -f $$(docker images -f "dangling=true" -q) || true
@echo Очистка testrail образов
docker rmi -f $$(docker images --filter=reference='registry.gitlab.com/touchbit/image/testrail/*' -q) || true
@echo Очистка всех неиспользуемых volume
docker volume rm -f $$(docker volume ls -q) || true
@echo Очистка всех testrail сетей
docker network rm $(docker network ls --filter=name=testrail -q) || true
docker ps
$ make docker-up
docker-compose -f .indirect/docker-compose.yml pull
Pulling db ... done
Pulling migration ... done
Pulling fpm ... done
Pulling web ... done
docker-compose -f .indirect/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Creating network "local-testrail_default" with the default driver
Recreating local-testrail_db_1 ... done
Recreating local-testrail_migration_1 ... done
Recreating local-testrail_fpm_1 ... done
Recreating local-testrail_web_1 ... done
docker ps
CONTAINER ID NAMES
3b8f9d4af29c local-testrail_web_1
5622c7d742d5 local-testrail_fpm_1
b580e3392038 local-testrail_migration_1
e467630bd3a5 local-testrail_db_1
$ export CI_JOB_ID=123456789
$ make docker-up
docker-compose -f .indirect/docker-compose.yml pull
Pulling db ... done
Pulling migration ... done
Pulling fpm ... done
Pulling web ... done
docker-compose -f .indirect/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Creating network "123456789-testrail_default" with the default driver
Creating volume "123456789-testrail_static-content" with default driver
Creating 123456789-testrail_db_1 ... done
Creating 123456789-testrail_fpm_1 ... done
Creating 123456789-testrail_migration_1 ... done
Creating 123456789-testrail_web_1 ... done
docker ps
CONTAINER ID NAMES
ccf1ad33d0e8 123456789-testrail_web_1
bc079964f681 123456789-testrail_fpm_1
10dc9d4d8f2a 123456789-testrail_migration_1
fe98d43c380e 123456789-testrail_db_1
$ make docker-logs
mkdir -p ./logs
docker logs ${COMPOSE_PROJECT_NAME}_web_1 >& logs/testrail-web.log || true
docker logs ${COMPOSE_PROJECT_NAME}_fpm_1 >& logs/testrail-fpm.log || true
docker logs ${COMPOSE_PROJECT_NAME}_migration_1 >& logs/testrail-migration.log || true
docker logs ${COMPOSE_PROJECT_NAME}_db_1 >& logs/testrail-mysql.log || true
Подготовка .gitlab-ci.yml
Запуск интеграционных тестов
Integration:
stage: test
tags:
- my-shell-runner
before_script:
# Аутентифицируемся в registry
- docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
# Генерируем псевдоуникальные TR_HTTP_PORT и TR_HTTPS_PORT
- export TR_HTTP_PORT=$(shuf -i10000-60000 -n1)
- export TR_HTTPS_PORT=$(shuf -i10000-60000 -n1)
script:
# поднимаем наше окружение
- make docker-up
# запускаем тесты исполняемым jar (у меня так)
- java -jar itest.jar --http-port ${TR_HTTP_PORT} --https-port ${TR_HTTPS_PORT}
# или в контейнере
- docker run --network=testrail-network-${CI_JOB_ID:-local} --rm itest
after_script:
# собираем логи
- make docker-logs
# останавливаем окружение
- make docker-down
artifacts:
# сохраняем логи
when: always
paths:
- logs
expire_in: 30 days
В результате запуска такой задачи в артефактах директория logs будет содержать логи сервисов и тестов. Что очень удобно в случае возникновения ошибок. У меня каждый тест в параллели пишет свой лог, но об этом я расскажу отдельно.
Очистка раннера
Задача будет запускаться только по расписанию.
stages:
- clean
- build
- test
Clean runner:
stage: clean
only:
- schedules
tags:
- my-shell-runner
script:
- make docker-clean
Далее идем в наш GitLab проект -> CI/CD -> Schedules -> New Schedule и добавляем новое расписание
Результат
Запускаем 4 задачи в GitLab CI
В логах последней задачи с интеграционными тестами видим контейнеры от разных задач
CONTAINER ID NAMES
c6b76f9135ed 204645172-testrail-web_1
01d303262d8e 204645172-testrail-fpm_1
2cdab1edbf6a 204645172-testrail-migration_1
826aaf7c0a29 204645172-testrail-mysql_1
6dbb3fae0322 204645084-testrail-web_1
3540f8d448ce 204645084-testrail-fpm_1
70fea72aa10d 204645084-testrail-mysql_1
d8aa24b2892d 204644881-testrail-web_1
6d4ccd910fad 204644881-testrail-fpm_1
685d8023a3ec 204644881-testrail-mysql_1
1cdfc692003a 204644793-testrail-web_1
6f26dfb2683e 204644793-testrail-fpm_1
029e16b26201 204644793-testrail-mysql_1
c10443222ac6 204567103-testrail-web_1
04339229397e 204567103-testrail-fpm_1
6ae0accab28d 204567103-testrail-mysql_1
b66b60d79e43 204553690-testrail-web_1
033b1f46afa9 204553690-testrail-fpm_1
a8879c5ef941 204553690-testrail-mysql_1
069954ba6010 204553539-testrail-web_1
ed6b17d911a5 204553539-testrail-fpm_1
1a1eed057ea0 204553539-testrail-mysql_1
$ docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
WARNING! Your password will be stored unencrypted in /home/gitlab-runner/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
$ export TR_HTTP_PORT=$(shuf -i10000-60000 -n1)
$ export TR_HTTPS_PORT=$(shuf -i10000-60000 -n1)
$ mkdir ${CI_JOB_ID}
$ cp .indirect/docker-compose.yml ${CI_JOB_ID}/docker-compose.yml
$ make docker-up
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml kill
docker network rm testrail-network-${CI_JOB_ID:-local} || true
Error: No such network: testrail-network-204645172
docker network create testrail-network-${CI_JOB_ID:-local}
0a59552b4464b8ab484de6ae5054f3d5752902910bacb0a7b5eca698766d0331
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml pull
Pulling web ... done
Pulling fpm ... done
Pulling migration ... done
Pulling db ... done
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Creating volume "204645172-testrail_static-content" with default driver
Creating 204645172-testrail-mysql_1 ...
Creating 204645172-testrail-mysql_1 ... done
Creating 204645172-testrail-migration_1 ... done
Creating 204645172-testrail-fpm_1 ... done
Creating 204645172-testrail-web_1 ... done
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c6b76f9135ed registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 13 seconds ago Up 1 second 0.0.0.0:51148->80/tcp, 0.0.0.0:25426->443/tcp 204645172-testrail-web_1
01d303262d8e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 16 seconds ago Up 13 seconds 9000/tcp 204645172-testrail-fpm_1
2cdab1edbf6a registry.gitlab.com/touchbit/image/testrail/migration:latest "docker-entrypoint.s…" 16 seconds ago Up 13 seconds 3306/tcp, 33060/tcp 204645172-testrail-migration_1
826aaf7c0a29 mysql:5.7.22 "docker-entrypoint.s…" 18 seconds ago Up 16 seconds 3306/tcp 204645172-testrail-mysql_1
6dbb3fae0322 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 36 seconds ago Up 22 seconds 0.0.0.0:44202->80/tcp, 0.0.0.0:20151->443/tcp 204645084-testrail-web_1
3540f8d448ce registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 38 seconds ago Up 35 seconds 9000/tcp 204645084-testrail-fpm_1
70fea72aa10d mysql:5.7.22 "docker-entrypoint.s…" 40 seconds ago Up 37 seconds 3306/tcp 204645084-testrail-mysql_1
d8aa24b2892d registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" About a minute ago Up 53 seconds 0.0.0.0:31103->80/tcp, 0.0.0.0:43872->443/tcp 204644881-testrail-web_1
6d4ccd910fad registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" About a minute ago Up About a minute 9000/tcp 204644881-testrail-fpm_1
685d8023a3ec mysql:5.7.22 "docker-entrypoint.s…" About a minute ago Up About a minute 3306/tcp 204644881-testrail-mysql_1
1cdfc692003a registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" About a minute ago Up About a minute 0.0.0.0:44752->80/tcp, 0.0.0.0:23540->443/tcp 204644793-testrail-web_1
6f26dfb2683e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" About a minute ago Up About a minute 9000/tcp 204644793-testrail-fpm_1
029e16b26201 mysql:5.7.22 "docker-entrypoint.s…" About a minute ago Up About a minute 3306/tcp 204644793-testrail-mysql_1
c10443222ac6 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 5 hours ago Up 5 hours 0.0.0.0:57123->80/tcp, 0.0.0.0:31657->443/tcp 204567103-testrail-web_1
04339229397e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 5 hours ago Up 5 hours 9000/tcp 204567103-testrail-fpm_1
6ae0accab28d mysql:5.7.22 "docker-entrypoint.s…" 5 hours ago Up 5 hours 3306/tcp 204567103-testrail-mysql_1
b66b60d79e43 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 5 hours ago Up 5 hours 0.0.0.0:56321->80/tcp, 0.0.0.0:58749->443/tcp 204553690-testrail-web_1
033b1f46afa9 registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 5 hours ago Up 5 hours 9000/tcp 204553690-testrail-fpm_1
a8879c5ef941 mysql:5.7.22 "docker-entrypoint.s…" 5 hours ago Up 5 hours 3306/tcp 204553690-testrail-mysql_1
069954ba6010 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 5 hours ago Up 5 hours 0.0.0.0:32869->80/tcp, 0.0.0.0:16066->443/tcp 204553539-testrail-web_1
ed6b17d911a5 registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 5 hours ago Up 5 hours 9000/tcp 204553539-testrail-fpm_1
1a1eed057ea0 mysql:5.7.22 "docker-entrypoint.s…" 5 hours ago Up 5 hours 3306/tcp 204553539-testrail-mysql_1
Вроде все по красоте, но есть нюанс. Pipeline может быть принудительно отменен во время выполнения интеграционных тестов, и в этом случае запущенные контейнеры не будут остановлены. Время от времени нужно чистить раннер. К сожалению, задача на доработку в GitLab CE все еще в статусе Open
Но у нас добавлен запуск задачи по расписанию, и никто нам не запрещает ее запустить вручную.
Переходим в наш проект -> CI/CD -> Schedules и запускаем задачу Clean runner
Итого:
- У нас один shell runner.
- Конфликтов между задачами и окружением нет.
- У нас параллельный запуск задач с интеграционными тестами.
- Можно запускать интеграционные тесты как локально, так и в контейнере.
- Логи сервисов и тестов собираются и прикрепляются к pipeline-задаче.
- Есть возможность очистки раннера от старых docker-образов.
Время настройки — ~2 часа.
Вот, собственно, и все. Буду рад фидбэку.
P.S.
Особая благодарность freeseacher vvasilenok ivanych. Ваши коментарии оказались очень ценными в контектсе публикации.