Pull to refresh

GitLab Shell Runner. Конкурентный запуск тестируемых сервисов при помощи Docker Compose

IT systems testingTDDWeb services testingDevOpsMicroservices


Данная статья будет интересна как тестировщикам, так и разработчикам, но рассчитана в большей степени на автоматизаторов, которые столкнулись с проблемой настройки GitLab CI/CD для проведения интеграционного тестирования в условиях недостаточности инфраструктурных ресурсов и/или отсутствия платформы оркестрации контейнеров. Я расскажу, как настроить развертывание тестируемых окружений при помощи docker compose на одном единственном GitLab shell раннере и так, чтобы при развертывании нескольких окружений запускаемые сервисы друг другу не мешали.



Содержание




Предпосылки


  1. В моей практике частенько случалось "лечить" интеграционное тестирование на проектах. И зачастую первой и самой значительной проблемой является CI pipeline, в котором интеграционное тестирование разрабатываемого сервиса(ов) проводится в dev/stage окружении. Это вызывало не мало проблем:


    • Из-за дефектов в том или ином сервисе в процессе интеграционного тестирования тестовый контур может быть испорчен битыми данными. Бывали случаи, когда отправка запроса с битым JSON-форматом вешал сервис, что приводило стенд полностью в нерабочее состояние.
    • Замедлением работы тестового контура с ростом тестовых данных. Думаю, описывать пример с очисткой/откатом БД не имеет смысла. В своей практике я не встречал проекта, где эта процедура проходила бы гладко.
    • Риск нарушить работоспособность тестового контура при тестировании общих настроек системы. Например, user/group/password/application policy.
    • Тестовые данные от автотестов мешают жить ручным тестировщикам.

    Кто-то скажет, что хорошие автотесты должны чистить данные после себя. У меня есть аргументы против:


    • Динамические стенды весьма удобны в использовании.
    • Не каждый объект можно удалить из системы через API. Например вызов на удаление объекта не реализован, так как противоречит бизнес логике.
    • При создании объекта через API может создаваться огромное количество метаданных, которые удалить проблематично.
    • Если тесты имеют зависимость между собой, то процесс очистки данных после выполнения тестов превращается в головную боль.
    • Дополнительные (и, на мой взгляд, не оправданные) вызовы к API.
    • И главный аргумент: когда тестовые данные начинают чистить прямо из БД. Это превращается в настоящий PK/FK цирк! От разработчиков слышно: «Я только табличку добавил/удалил/переименовал, почему 100500 интеграционных тестов попадало?»

    По моему мнению, самое оптимальное решение — это динамическое окружение.


  2. Много кто использует docker-compose для запуска тестового окружения, но мало кто использует docker-compose при проведении интеграционного тестирования в CI/CD. И тут я не беру в расчет kubernetes, swarm и другие платформы оркестрации контейнеров. Не в каждой компании они есть. Хорошо бы было, если бы docker-compose.yml был универсальный.
  3. Если даже у нас есть свой QA раннер, как нам сделать так, чтобы сервисы запускаемые через docker-compose не мешали друг-другу?
  4. Как собирать логи тестируемых сервисов?
  5. Как чистить раннер?

У меня есть собственный 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

Проверяем CI запуск
$ 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. Ваши коментарии оказались очень ценными в контектсе публикации.


К содержанию

Tags:automationautomation testinggitlab-cigitlab cigitlab runnergitlab-runnerdocker composedocker-composeconcurrent
Hubs: IT systems testing TDD Web services testing DevOps Microservices
Rating +15
Views 14.8k Add to bookmarks 118
Comments
Comments 20

Popular right now

Backend Developer (Python/GO)
from 180,000 ₽AgentAppRemote job
DevOps-инженер
from 220,000 ₽Российский квантовый центрМоскваRemote job
DevOps инженер
from 2,000 to 4,000 $TаксДомRemote job
Python-разработчик
from 60,000 ₽JetStyleЕкатеринбург
QA Engineer (remote/relocation)
from 3,000 €ExnessМоскваRemote job

Top of the last 24 hours