Тази статия ще бъде интересна както на тестьорите, така и на разработчиците, но е насочена най-вече към автоматизаторите, които са се сблъскали с проблема за настройка на GitLab CI/CD за извършване на интеграционно тестване при недостатъчни инфраструктурни ресурси и/или отсъствие на платформа за оркестрация на контейнери. Ще обясня как да настроите разгръщането на тестваните среди с помощта на docker compose на един единствен GitLab shell ранър, така че разгръщаните услуги да не си пречат помежду си.
Съдържание
Предпоследите
В моята практика често се налагаше да "ремонтирам" интеграционното тестване по проекти. И често първата и най-съществена проблем е 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 раннер за проектите си и с тези въпроси се сблъсках при разработката за . По-точно при стартиране на интеграционните тестове. По-нататък ще решаваме тези въпроси с примери от този проект.
GitLab Shell Runner
За раннера препоръчвам линукс виртуална машина с 4 vCPU, 4 GB RAM, 50 GB HDD.
В интернет има много информация за настройка на gitlab-runner, затова накратко:
- Влизаме на машината по SSH
Ако имате по-малко от 8 GB RAM, препоръчвам , за да не дойде 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: Убий процес 26474 (java) score 127 или жертвай дете Убит процес 26474 (java) total-vm:4247740kB, anon-rss:495224kB, file-rss:0kB, shmem-rss:0kBИ ако картината изглежда приблизително така, то или добавете swap, или увеличете RAM.
- Инсталираме , , , make.
- Добавяме потребителя
gitlab-runnerв групатаdockersudo 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, тези цифри могат да бъдат увеличени поне двойно. Но всичко зависи от това какво точно ще се стартира на този ранър и в какво количество.
Това е достатъчно.
Подготовка на docker-compose.yml
Основната задача е универсален docker-compose.yml, който разработчици/тестери могат да използват както локално, така и в CI pipeline.
На първо място, правим уникални имена на услугите за CI. Една от уникалните променливи в GitLab CI е променливата CI_JOB_ID. Ако посочим container_name со значением "service-${CI_JOB_ID:-local}", то в случай на:
- ако
CI_JOB_IDне е определена в променливите на средата,
името на услугата ще бъдеservice-local - ако
CI_JOB_IDопределена в променливите на средата (например 123),
името на услугата ще бъдеservice-123
На второ място, правим обща мрежа за стартираните услуги. Това ни осигурява изолация на мрежово ниво при стартиране на няколко тестови среди.
networks:
default:
external:
name: service-network-${CI_JOB_ID:-local}Същност, това е първата стъпка към успеха =)
Пример за моя docker-compose.yml с коментари
version: "3"
# За правилната работа на web (php) и fmt е нужно,
# контейнерите да имат общо изпълнимо съдържание.
# В нашия случай, това е директорията /var/www/testrail
volumes:
static-content:
# Изолираме средата на ниво мрежа
networks:
default:
external:
name: testrail-network-${CI_JOB_ID:-local}
services:
db:
image: mysql:5.7.22
# Всеки container_name съдържа ${CI_JOB_ID:-local}
container_name: "testrail-mysql-${CI_JOB_ID:-local}"
environment:
MYSQL_HOST: db
MYSQL_DATABASE: mydb
MYSQL_ROOT_PASSWORD: 1234
SKIP_GRANT_TABLES: 1
SKIP_NETWORKING: 1
SERVICE_TAGS: dev
SERVICE_NAME: mysql
networks:
- default
migration:
image: registry.gitlab.com/touchbit/image/testrail/migration:latest
container_name: "testrail-migration-${CI_JOB_ID:-local}"
links:
- db
depends_on:
- db
networks:
- default
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
networks:
- default
web:
image: registry.gitlab.com/touchbit/image/testrail/web:latest
container_name: "testrail-web-${CI_JOB_ID:-local}"
# Ако променливите 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
networks:
- defaultПример за локално стартиране
docker-compose -f docker-compose.yml up -d
Starting testrail-mysql-local ... done
Starting testrail-migration-local ... done
Starting testrail-fpm-local ... done
Recreating testrail-web-local ... doneНо не всичко е толкова просто при стартиране в CI.
Подготовка на Makefile
Използвам Makefile, тъй като е много удобно както за локално управление на средата, така и в CI. Следват коментари в инлайн
# У меня в проектах все вспомогательные вещи лежат в директории `.indirect`,
# в том числе и `docker-compose.yml`
# Использовать bash с опцией pipefail
# pipefail - фейлит выполнение пайпа, если команда выполнилась с ошибкой
SHELL=/bin/bash -o pipefail
# Останавливаем контейнеры и удаляем сеть
docker-kill:
docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml kill
docker network rm network-$${CI_JOB_ID:-testrail} || true
# Предварительно выполняем docker-kill
docker-up: docker-kill
# Создаем сеть для окружения
docker network create network-$${CI_JOB_ID:-testrail}
# Забираем последние образы из docker-registry
docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml pull
# Запускаем окружение
# force-recreate - принудительное пересоздание контейнеров
# renew-anon-volumes - не использовать volumes предыдущих контейнеров
docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
# Ну и, на всякий случай, вывести что там у нас в принципе запущено на машинке
docker ps
# Коллектим логи сервисов
docker-logs:
mkdir ./logs || true
docker logs testrail-web-$${CI_JOB_ID:-local} >& logs/testrail-web.log
docker logs testrail-fpm-$${CI_JOB_ID:-local} >& logs/testrail-fpm.log
docker logs testrail-migration-$${CI_JOB_ID:-local} >& logs/testrail-migration.log
docker logs testrail-mysql-$${CI_JOB_ID:-local} >& logs/testrail-mysql.log
# Очистка раннера
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
$ make docker-up
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml kill
Убивам testrail-web-local ... готово
Убивам testrail-fpm-local ... готово
Убивам testrail-mysql-local ... готово
docker network rm network-${CI_JOB_ID:-testrail} || true
network-testrail
docker network create network-${CI_JOB_ID:-testrail}
d2ec063324081c8bbc1b08fd92242c2ea59d70cf4025fab8efcbc5c6360f083f
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml pull
Изтеглям db ... готово
Изтеглям миграция ... готово
Изтеглям fpm ... готово
Изтеглям уеб ... готово
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Създавам отново testrail-mysql-local ... готово
Създавам отново testrail-fpm-local ... готово
Създавам отново testrail-migration-local ... готово
Създавам отново testrail-web-local ... готово
docker ps
CONTAINER ID PORTS NAMES
a845d3cb0e5a 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp testrail-web-local
19d8ef001398 9000/tcp testrail-fpm-local
e28840a2369c 3306/tcp, 33060/tcp testrail-migration-local
0e7900c23f37 3306/tcp testrail-mysql-local
make docker-logs
$ make docker-logs
mkdir ./logs || true
mkdir: не може да се създаде директория ‘./logs’: Директорията съществува
docker logs testrail-web-${CI_JOB_ID:-local} > logs/testrail-web.log
docker logs testrail-fpm-${CI_JOB_ID:-local} > logs/testrail-fpm.log
docker logs testrail-migration-${CI_JOB_ID:-local} > logs/testrail-migration.log
docker logs testrail-mysql-${CI_JOB_ID:-local} > logs/testrail-mysql.log
Подготовка на .gitlab-ci.yml
Стартиране на интеграционни тестове
Интеграция:
етап: тест
етикети:
- 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)
# създаваме директория с идентификатора на задачата
- mkdir ${CI_JOB_ID}
# копираме в създадената директория нашия docker-compose.yml
# за да бъде контекстът различен за всяка задача
- cp .indirect/docker-compose.yml ${CI_JOB_ID}/docker-compose.yml
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-kill
артефакти:
# запазваме логовете
когато: винаги
пътища:
- logs
изтичане: 30 дниВ резултат на стартирането на такава задача, в артефактите папката logs ще съдържа логове на услугите и тестовете. Което е много удобно в случай на грешки. Всеки тест на паралелно изпълнение записва своя лог, но за това ще говоря отделно.

Почистване на раннера
Задачата ще се изпълнява само по график.
етапи:
- почистване
- изграждане
- тест
Почистващ изпълнител:
етап: почистване
само:
- графици
етикети:
- my-shell-runner
скрипт:
- make docker-cleanСледваме в нашия проект GitLab -> CI/CD -> Разписания -> Ново разписание и добавяме ново разписание
Резултат
Стартираме 4 задачи в GitLab CI
В логовете на последната задача с интеграционните тестове виждаме контейнери от различни задачи
CONTAINER ID NAMES
c6b76f9135ed testrail-web-204645172
01d303262d8e testrail-fpm-204645172
2cdab1edbf6a testrail-migration-204645172
826aaf7c0a29 testrail-mysql-204645172
6dbb3fae0322 testrail-web-204645084
3540f8d448ce testrail-fpm-204645084
70fea72aa10d testrail-mysql-204645084
d8aa24b2892d testrail-web-204644881
6d4ccd910fad testrail-fpm-204644881
685d8023a3ec testrail-mysql-204644881
1cdfc692003a testrail-web-204644793
6f26dfb2683e testrail-fpm-204644793
029e16b26201 testrail-mysql-204644793
c10443222ac6 testrail-web-204567103
04339229397e testrail-fpm-204567103
6ae0accab28d testrail-mysql-204567103
b66b60d79e43 testrail-web-204553690
033b1f46afa9 testrail-fpm-204553690
a8879c5ef941 testrail-mysql-204553690
069954ba6010 testrail-web-204553539
ed6b17d911a5 testrail-fpm-204553539
1a1eed057ea0 testrail-mysql-204553539По-подробен лог
$ docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
ПРЕДУПРЕЖДЕНИЕ! Използването на --password през CLI е небезопасно. Използвайте --password-stdin.
ПРЕДУПРЕЖДЕНИЕ! Вашата парола ще бъде съхранена нешифрована в /home/gitlab-runner/.docker/config.json.
Конфигурирайте помощна програма за удостоверяване, за да премахнете това предупреждение. Вижте
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Входът е успешен
$ 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
Грешка: Не съществува такова мрежа: testrail-network-204645172
docker network create testrail-network-${CI_JOB_ID:-local}
0a59552b4464b8ab484de6ae5054f3d5752902910bacb0a7b5eca698766d0331
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml pull
Изтегляне на web ... завършено
Изтегляне на fpm ... завършено
Изтегляне на migration ... завършено
Изтегляне на db ... завършено
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Създаване на том "204645172_static-content" с подразбиране драйвер
Създаване на testrail-mysql-204645172 ...
Създаване на testrail-mysql-204645172 ... завършено
Създаване на testrail-migration-204645172 ... завършено
Създаване на testrail-fpm-204645172 ... завършено
Създаване на testrail-web-204645172 ... завършено
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 testrail-web-204645172
01d303262d8e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 16 seconds ago Up 13 seconds 9000/tcp testrail-fpm-204645172
2cdab1edbf6a registry.gitlab.com/touchbit/image/testrail/migration:latest "docker-entrypoint.s…" 16 seconds ago Up 13 seconds 3306/tcp, 33060/tcp testrail-migration-204645172
826aaf7c0a29 mysql:5.7.22 "docker-entrypoint.s…" 18 seconds ago Up 16 seconds 3306/tcp testrail-mysql-204645172
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 testrail-web-204645084
3540f8d448ce registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 38 seconds ago Up 35 seconds 9000/tcp testrail-fpm-204645084
70fea72aa10d mysql:5.7.22 "docker-entrypoint.s…" 40 seconds ago Up 37 seconds 3306/tcp testrail-mysql-204645084
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 testrail-web-204644881
6d4ccd910fad registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" About a minute ago Up About a minute 9000/tcp testrail-fpm-204644881
685d8023a3ec mysql:5.7.22 "docker-entrypoint.s…" About a minute ago Up About a minute 3306/tcp testrail-mysql-204644881
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 testrail-web-204644793
6f26dfb2683e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" About a minute ago Up About a minute 9000/tcp testrail-fpm-204644793
029e16b26201 mysql:5.7.22 "docker-entrypoint.s…" About a minute ago Up About a minute 3306/tcp testrail-mysql-204644793
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 testrail-web-204567103
04339229397e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 5 hours ago Up 5 hours 9000/tcp testrail-fpm-204567103
6ae0accab28d mysql:5.7.22 "docker-entrypoint.s…" 5 hours ago Up 5 hours 3306/tcp testrail-mysql-204567103
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 testrail-web-204553690
033b1f46afa9 registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 5 hours ago Up 5 hours 9000/tcp testrail-fpm-204553690
a8879c5ef941 mysql:5.7.22 "docker-entrypoint.s…" 5 hours ago Up 5 hours 3306/tcp testrail-mysql-204553690
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 testrail-web-204553539
ed6b17d911a5 registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 5 hours ago Up 5 hours 9000/tcp testrail-fpm-204553539
1a1eed057ea0 mysql:5.7.22 "docker-entrypoint.s…" 5 hours ago Up 5 hours 3306/tcp testrail-mysql-204553539Всички задачи са завършени успешно.
Артефактите на задачата съдържат логове на услугите и тестовете.
Изглежда всичко е наред, но има нюанс. Pipeline може да бъде принудително отменен по време на изпълнението на интеграционните тестове и в този случай стартираните контейнери няма да бъдат спрени. От време на време е необходимо да се почисти runner-ът. За съжаление, задачата за доработка в GitLab CE все още е в статус.
Но ние добавихме стартиране на задачата по график и никой не ни пречи да я стартираме ръчно.
Прехвърляме се в нашия проект -> CI/CD -> Графици и стартираме задачата. Почистване на runner-а.
В обобщение:
- Имаме един shell runner.
- Нямаме конфликти между задачите и средата.
- Имаме паралелно стартиране на задачи с интеграционни тестове.
- Интеграционните тестове могат да се стартират както локално, така и в контейнер.
- Логовете на услугите и тестовете се събират и прикачват към pipeline задачата.
- Има възможност за почистване на runner-а от стари docker образи.
Време за настройка — ~2 часа.
Ето, всъщност, това е всичко. Ще се радвам на обратна връзка.
Източник: habr.com
