Ten artykuł będzie interesujący zarówno dla testerów, jak i programistów, ale jest w większym stopniu skierowany do automatyków, którzy napotkali problem z konfiguracją GitLab CI/CD do przeprowadzania testów integracyjnych w warunkach niewystarczających zasobów infrastrukturalnych i/lub braku platformy orkiestracji kontenerów. Opowiem, jak skonfigurować wdrażanie testowych środowisk przy pomocy Docker Compose na jednym jedynym GitLab shell runnerze, aby podczas uruchamiania wielu środowisk uruchamiane usługi nawzajem sobie nie przeszkadzały.
Spis treści
Przesłanki
W mojej praktyce wielokrotnie zdarzało mi się 'leczyć' testy integracyjne w projektach. I często pierwszym oraz najważniejszym problemem jest CI pipeline, w którym testy integracyjne rozdevelopowanego usługi(e) prowadzone są w środowisku dev/stage. Powodowało to sporo problemów:
- Z powodu defektów w danej usłudze w trakcie testów integracyjnych, kontur testowy może zostać zniszczony uszkodzonymi danymi. Były przypadki, gdy wysłanie żądania z uszkodzonym formatem JSON zawieszało usługę, co prowadziło do całkowitego unieruchomienia standu.
- Spowolnienie pracy konturu testowego wraz ze wzrostem danych testowych. Myślę, że nie ma sensu opisywać przykładu z czyszczeniem/rollbackiem Bazy Danych; w swojej praktyce nie spotkałem projektu, w którym ta procedura przebiegałaby gładko.
- Ryzyko zakłócenia działania konturu testowego podczas testowania wspólnych ustawień systemu. Na przykład: user/group/password/application policy.
- Dane testowe od testów automatycznych przeszkadzają ręcznym testerom.
Niektórzy powiedzą, że dobre testy automatyczne powinny po sobie sprzątać. Mam argumenty przeciwko temu:
- Dynamiczne standy są bardzo wygodne w użyciu.
- Nie każdy obiekt można usunąć z systemu przez API. Na przykład wywołanie usunięcia obiektu nie zostało zrealizowane, ponieważ kłóci się z logiką biznesową.
- Przy tworzeniu obiektu przez API może zostać utworzona ogromna ilość metadanych, które są trudne do usunięcia.
- Jeśli testy są ze sobą powiązane, proces czyszczenia danych po wykonaniu testów staje się prawdziwą udręką.
- Dodatkowe (moim zdaniem, nieuzasadnione) wywołania do API.
- A główny argument: gdy dane testowe zaczynają być czyszczone bezpośrednio z bazy danych. To staje się prawdziwym cyrkiem PK/FK! Od programistów słychać: „Dodałem/usunąłem/przemianowałem tylko jedną tabelę, dlaczego 100500 testów integracyjnych padło?”
Moim zdaniem, optymalnym rozwiązaniem jest dynamiczne środowisko.
- Wiele osób używa docker-compose do uruchamiania środowiska testowego, ale mało kto go używa podczas przeprowadzania testów integracyjnych w CI/CD. Nie biorę tu pod uwagę kubernetes, swarm i inne platformy orkiestracji kontenerów. Nie w każdej firmie są obecne. Fajnie by było, gdyby docker-compose.yml był uniwersalny.
- Nawet jeśli mamy własny runner QA, jak możemy zrobić, aby usługi uruchamiane przez docker-compose nie przeszkadzały sobie nawzajem?
- Jak zbierać logi testowanych usług?
- Jak oczyścić runner?
Mam własnego runnera GitLab dla swoich projektów i z tymi pytaniami zmierzyłem się podczas rozwoju. do . A dokładniej podczas uruchamiania testów integracyjnych. Tutaj będziemy rozwiązywać te problemy na przykładach z tego projektu.
GitLab Shell Runner
Dla runnera polecam wirtualną maszynę z Linuxem z 4 vCPU, 4 GB RAM, 50 GB HDD.
W internecie jest bardzo dużo informacji na temat konfiguracji gitlab-runner, więc krótko:
- Zaloguj się na maszynę przez SSH.
Jeśli masz mniej niż 8 GB RAM, polecam , aby OOM killer nie zabijał naszych zadań z powodu braku pamięci RAM. Może się to zdarzyć, gdy jednocześnie uruchomionych jest więcej niż 5 zadań. Zgłoszenia będą przechodzić wolniej, ale stabilnie.
Przykład z OOM killer
Jeśli w logach zadania zobaczysz
bash: line 82: 26474 Killed, po prostu wykonaj na runnerzesudo 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:0kBJeśli obraz wygląda mniej więcej tak, to albo dodaj swap, albo dołóż RAM.
- Instalujemy , , , make.
- Dodajemy użytkownika
gitlab-runnerdo grupydockersudo groupadd docker sudo usermod -aG docker gitlab-runner - gitlab-runner.
Otwieramy do edycji
/etc/gitlab-runner/config.tomli dodajemyconcurrent=20 [[runners]] request_concurrency = 10To pozwoli na uruchamianie równoległych zadań na jednym runnerze. Więcej informacji znajdziesz tutaj .
Jeśli masz mocniejszą maszynę, na przykład 8 vCPU, 16 GB RAM, to te liczby można zwiększyć przynajmniej dwukrotnie. Ale wszystko zależy od tego, co dokładnie będzie uruchamiane na danym runnerze i w jakiej ilości.
To wystarczające.
Przygotowanie docker-compose.yml
Głównym zadaniem jest uniwersalny plik docker-compose.yml, który programiści/testerzy mogą używać zarówno lokalnie, jak i w pipeline CI.
Po pierwsze, nadajemy unikalne nazwy usługom dla CI. Jedną z unikalnych zmiennych w GitLab CI jest zmienna CI_JOB_ID. Jeśli zdefiniujesz container_name o wartości "service-${CI_JOB_ID:-local}", to w przypadku:
- jeśli
CI_JOB_IDniezdefiniowanej w zmiennych środowiskowych,
nazwa usługi będzieservice-local - jeśli
CI_JOB_IDzdefiniowanej w zmiennych środowiskowych (na przykład 123),
nazwa usługi będzieservice-123
Po drugie, tworzymy wspólną sieć dla uruchamianych usług. Daje nam to izolację na poziomie sieci przy uruchamianiu wielu środowisk testowych.
networks:
default:
external:
name: service-network-${CI_JOB_ID:-local}Właściwie, to pierwszy krok do sukcesu =)
Przykład mojego pliku docker-compose.yml z komentarzami
version: "3"
# Dla poprawnego działania web (php) i fmt,
# kontenery muszą mieć wspólną zawartość do wykonania.
# W naszym przypadku to katalog /var/www/testrail
volumes:
static-content:
# Izolujemy środowisko na poziomie sieciowym
networks:
default:
external:
name: testrail-network-${CI_JOB_ID:-local}
services:
db:
image: mysql:5.7.22
# Każdy container_name zawiera ${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}"
# Jeśli zmienne TR_HTTP_PORT lub TR_HTTPS_PORTS nie są zdefiniowane,
# to usługa uruchamia się na porcie 80 i 443 odpowiednio.
ports:
- ${TR_HTTP_PORT:-80}:80
- ${TR_HTTPS_PORT:-443}:443
volumes:
- static-content:/var/www/testrail
links:
- db
- fpm
networks:
- defaultPrzykład lokalnego uruchomienia
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 ... doneAle nie wszystko jest takie proste z uruchamianiem w CI.
Przygotowanie Makefile
Używam Makefile, ponieważ jest to bardzo wygodne zarówno do lokalnego zarządzania środowiskiem, jak i w CI. Następnie komentarze w linii.
# У меня в проектах все вспомогательные вещи лежат в директории `.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
Sprawdzamy
make docker-up
$ make docker-up
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml kill
Zabijanie testrail-web-local ... gotowe
Zabijanie testrail-fpm-local ... gotowe
Zabijanie testrail-mysql-local ... gotowe
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
Pobieranie db ... gotowe
Pobieranie migration ... gotowe
Pobieranie fpm ... gotowe
Pobieranie web ... gotowe
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Rekreacja testrail-mysql-local ... gotowe
Rekreacja testrail-fpm-local ... gotowe
Rekreacja testrail-migration-local ... gotowe
Rekreacja testrail-web-local ... gotowe
docker ps
ID KONTENERA PORTY NAZWY
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: nie można utworzyć katalogu ‘./logs’: Katalog istnieje
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
Przygotowanie .gitlab-ci.yml
Uruchomienie testów integracyjnych
Integracja:
etap: test
tagi:
- my-shell-runner
before_script:
# Logujemy się do rejestru
- docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
# Generujemy pseudounikalne TR_HTTP_PORT i TR_HTTPS_PORT
- export TR_HTTP_PORT=$(shuf -i10000-60000 -n1)
- export TR_HTTPS_PORT=$(shuf -i10000-60000 -n1)
# tworzymy katalog z identyfikatorem zadania
- mkdir ${CI_JOB_ID}
# kopiujemy do utworzonego katalogu nasz docker-compose.yml
# aby kontekst był inny dla każdego zadania
- cp .indirect/docker-compose.yml ${CI_JOB_ID}/docker-compose.yml
script:
# uruchamiamy nasze środowisko
- make docker-up
# uruchamiamy testy za pomocą jar (u mnie tak)
- java -jar itest.jar --http-port ${TR_HTTP_PORT} --https-port ${TR_HTTPS_PORT}
# lub w kontenerze
- docker run --network=testrail-network-${CI_JOB_ID:-local} --rm itest
after_script:
# zbieramy logi
- make docker-logs
# zatrzymujemy środowisko
- make docker-kill
artifacts:
# zapisujemy logi
when: always
paths:
- logs
expire_in: 30 dniW wyniku uruchomienia takiego zadania w artefaktach katalog logs będzie zawierał logi usług i testów. Co jest bardzo wygodne w przypadku wystąpienia błędów. Każdy test w równoległych operacjach zapisuje swój log, ale o tym opowiem osobno.

Czyszczenie runnera
Zadanie będzie uruchamiane tylko według harmonogramu.
etapy:
- czyszczenie
- budowanie
- testowanie
Czysty runner:
etap: czyszczenie
tylko:
- harmonogramy
tagi:
- my-shell-runner
skrypt:
- make docker-cleanNastępnie przechodzimy do naszego projektu w GitLab -> CI/CD -> Harmonogramy -> Nowy Harmonogram i dodajemy nowy harmonogram
Wynik
Uruchamiamy 4 zadania w GitLab CI
W logach ostatniego zadania z testami integracyjnymi widzimy kontenery od różnych zadań
IDENTYFIKATOR KONTENERA NAZWY
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-204553539Bardziej szczegółowy log
$ docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
OSTRZEŻENIE! Używanie --hasła za pomocą CLI jest niebezpieczne. Użyj --password-stdin.
OSTRZEŻENIE! Twoje hasło będzie przechowywane w niezaszyfrowanym formacie w /home/gitlab-runner/.docker/config.json.
Skonfiguruj pomocnika poświadczeń, aby wyeliminować to ostrzeżenie. Zobacz
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Logowanie zakończone sukcesem
$ 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
Błąd: Nie ma takiej sieci: testrail-network-204645172
docker network create testrail-network-${CI_JOB_ID:-local}
0a59552b4464b8ab484de6ae5054f3d5752902910bacb0a7b5eca698766d0331
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml pull
Pobieranie web ... zakończone
Pobieranie fpm ... zakończone
Pobieranie migration ... zakończone
Pobieranie db ... zakończone
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Tworzenie wolumenu "204645172_static-content" z domyślnym sterownikiem
Tworzenie testrail-mysql-204645172 ...
Tworzenie testrail-mysql-204645172 ... zakończone
Tworzenie testrail-migration-204645172 ... zakończone
Tworzenie testrail-fpm-204645172 ... zakończone
Tworzenie testrail-web-204645172 ... zakończone
docker ps
ID KONTENERA OBRAZ KOMENDA UTWORZONY STATUS PORTY NAZWY
c6b76f9135ed registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 13 sekund temu Działa 1 sekundę 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 sekund temu Działa 13 sekund 9000/tcp testrail-fpm-204645172
2cdab1edbf6a registry.gitlab.com/touchbit/image/testrail/migration:latest "docker-entrypoint.s…" 16 sekund temu Działa 13 sekund 3306/tcp, 33060/tcp testrail-migration-204645172
826aaf7c0a29 mysql:5.7.22 "docker-entrypoint.s…" 18 sekund temu Działa 16 sekund 3306/tcp testrail-mysql-204645172
6dbb3fae0322 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 36 sekund temu Działa 22 sekundy 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 sekund temu Działa 35 sekund 9000/tcp testrail-fpm-204645084
70fea72aa10d mysql:5.7.22 "docker-entrypoint.s…" 40 sekund temu Działa 37 sekund 3306/tcp testrail-mysql-204645084
d8aa24b2892d registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" Około minuty temu Działa 53 sekundy 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…" Około minuty temu Działa Około minutę 9000/tcp testrail-fpm-204644881
685d8023a3ec mysql:5.7.22 "docker-entrypoint.s…" Około minuty temu Działa Około minutę 3306/tcp testrail-mysql-204644881
1cdfc692003a registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" Około minuty temu Działa Około minutę 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…" Około minuty temu Działa Około minutę 9000/tcp testrail-fpm-204644793
029e16b26201 mysql:5.7.22 "docker-entrypoint.s…" Około minuty temu Działa Około minutę 3306/tcp testrail-mysql-204644793
c10443222ac6 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 5 godzin temu Działa 5 godzin 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 godzin temu Działa 5 godzin 9000/tcp testrail-fpm-204567103
6ae0accab28d mysql:5.7.22 "docker-entrypoint.s…" 5 godzin temu Działa 5 godzin 3306/tcp testrail-mysql-204567103
b66b60d79e43 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 5 godzin temu Działa 5 godzin 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 godzin temu Działa 5 godzin 9000/tcp testrail-fpm-204553690
a8879c5ef941 mysql:5.7.22 "docker-entrypoint.s…" 5 godzin temu Działa 5 godzin 3306/tcp testrail-mysql-204553690
069954ba6010 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 5 godzin temu Działa 5 godzin 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 godzin temu Działa 5 godzin 9000/tcp testrail-fpm-204553539
1a1eed057ea0 mysql:5.7.22 "docker-entrypoint.s…" 5 godzin temu Działa 5 godzin 3306/tcp testrail-mysql-204553539Wszystkie zadania zostały pomyślnie zakończone
Artefakty zadania zawierają logi usług i testów
Wygląda na to, że wszystko jest w porządku, ale jest pewien szczegół. Pipeline może być wymuszenie anulowany podczas wykonywania testów integracyjnych, a w takim przypadku uruchomione kontenery nie zostaną zatrzymane. Od czasu do czasu konieczne jest czyszczenie runnera. Niestety, zadanie do poprawy w GitLab CE wciąż jest w statusie
Ale mamy dodane uruchamianie zadania zgodnie z harmonogramem, a nikt nie zabrania nam uruchomić go ręcznie.
Przechodzimy do naszego projektu -> CI/CD -> Harmonogramy i uruchamiamy zadanie Czyszczenie runnera
Podsumowując:
- Mamy jeden shell runner.
- Nie ma konfliktów między zadaniami a środowiskiem.
- Mamy równoległe uruchamianie zadań z testami integracyjnymi.
- Można uruchamiać testy integracyjne zarówno lokalnie, jak i w kontenerze.
- Logi usług i testów są zbierane i dołączane do zadania pipeline.
- Istnieje możliwość oczyszczenia runnera ze starych obrazów dockerowych.
Czas konfiguracji — ~2 godziny.
To wszystko. Będę wdzięczny za feedback.
Źródło: habr.com
