Клёвые фичи в Docker Compose — профили и шаблоны

от автора

Сейчас мы расскажем вам историю. Историю о том, как мы разработали API и решили написать на него E2E-тесты. Тесты были простыми, описывали и проверяли функциональность API, но оказались мудрёные в плане запуска. Но обо всём по порядку.

В этой статье рассмотрим решение к которому пришли на примере простой Docker Compose конфигурации.

Ручной запуск тестов

Мы искали удобный инструмент для написания E2E-тестов для API. Практически сразу нам встретился инструмент Karate. Полистали документацию и сначала решили запустить тесты вручную такой командой:

java -jar karate.jar . 

Но выяснилось, что для этого надо установить Karate и Java-рантайм для него, а потом написать об этом инструкцию для трех операционок: Windows, Linux и macOS.

Используем Docker Compose

Чтобы избежать установки множества инструментов (а еще и определенных версий!), решили запускать тесты в Docker, где все зависимости описаны в Dockerfile, и при сборке контейнера они устанавливаются в сам контейнер автоматически. Ниже приводим пример нашего Dockerfile для запуска Karate-тестов. Вдохновлялись официальной документацией. Для запуска тестов в Docker мы решили использовать Docker Compose, так как он позволяет нам поднять сразу несколько сервисов одной командой. В нашем случае это API, база данных и контейнер с тестами:

Для запуска всех сервисов написали docker-compose.yml файл.

version: '3.8'  services:   db:     image: postgres:13     container_name: 'db'     environment:       POSTGRES_USER: postgres       POSTGRES_PASSWORD: postgres       POSTGRES_DB: postgres     ports:       - 5432:5432    api:     container_name: 'api'     build:       context: .       dockerfile: Api/Dockerfile # Путь до докерфайла, из которого собирается                                  # образ API     ports:       - 5000:80     depends_on: # При запуске API миграции применяются к базе данных автоматически,                 # поэтому API запускается, только когда база данных уже запущена       - db    karate_tests:     container_name: 'karate_tests'     build:       dockerfile: KarateDockerfile # Путь до докерфайла, из которого собирается                                    # образ Karate (скачивается karate.jar файл,                                    # а Java уже существует внутри контейнера)       context: .     depends_on:       - api # Если API не будет запущен, тесты упадут, поэтому ждём запуска API     command: ["/karate"] # Запускаем тесты из папки /karate     volumes:       - .:/karate # Монтируем папку с тестами в папку /karate     environment:       API_URL: 'http://api' 

KarateDockerfile

FROM openjdk:11-jre-slim  RUN apt-get update && apt-get install -y curl  RUN apt-get install -y unzip  RUN curl -o /karate.jar -L 'https://github.com/intuit/karate/releases/download/v1.3.0/karate-1.3.0.jar'  ENTRYPOINT ["java", "-jar", "/karate.jar"] 

Два файла Docker Compose

Выглядит неплохо и удобно, но не всей команде нужны все контейнеры. Разработчику, например, нужна только база данных, так как API он запускает у себя в IDE.
Решили разделить Docker Compose файл на два: docker-compose.yml — остался без изменений и docker-compose-db.yml — содержащий только контейнер с базой данных:

docker-compose-db.yml

version: '3.8'  services:   db:     image: postgres:13     restart: always     container_name: 'db'     environment:       POSTGRES_USER: postgres       POSTGRES_PASSWORD: postgres       POSTGRES_DB: postgres     ports:       - 11237:5432 

Запуск тестов в пайплайне

Удобная конфигурация готова, а значит можно запустить тесты в пайплайне на создание мерж-реквеста в основную ветку. Для этих целей мы использовали файл docker-compose.yml, так как в нём есть всё необходимое для запуска тестов.
Написали пайплайн для GitHub, в котором запустили все контейнеры, в том числе и с самими тестами.

run: | LOGS=$(docker-compose --file docker-compose.yml up --abort-on-container-exit) # запишем все логи в переменную,                                                                                # чтобы в дальнейшем их разобрать и проверить,                                                                               # прошли тесты или нет # проверим, что нет упавших тестов if [ "$FAILED" -gt 0 ]; then   echo "Failed tests found! Failing the pipeline..."   exit 1 fi # проверим, что тесты в целом прошли, чтобы избежать ложного успешного завершения пайплайна if [ "$PASSED" -eq 0 ]; then   echo "No tests passed! Failing the pipeline..."   exit 1 fi 

Получилось немного костыльно, но пока не нашли лучшего способа обрушить пайплайн при упавших Karate-тестах.
Фактически, мы можем использовать два файла при сборке контейнеров, указывая их через флаг —file:

docker-compose --file docker-compose.yml --file docker-compose-db.yml up 

Но это решение не самая лучшая идея. Между файлами можно запутаться, и придется каждый раз проверять, что один и тот же сервис не присутствует в нескольких файлах сразу. А ещё файлов будет много — столько же, сколько конфигураций запуска. Мы определили как минимум:

  • local-environment — запускается база данных и API;

  • db-only — запускается только база данных для взаимодействия с ней сервиса, запущенного в IDE;

  • e2e-local-environment — запускаются API, база данных и контейнер с Karate-тестами, которые тестируют API, запущенный в Docker;

  • e2e-production-environment — мы сторонники TDD(Test-Driven Development) и тестирования в проде. А потому хотим запускать тесты не только в фича-ветке до мержа в основную ветку, но и после деплоя в прод. Эта конфигурация запускает только karate_tests контейнер, который нацелен на прод.

Профили

Порылись в документации Docker Compose и обнаружили удобный инструмент — профили. Эта фича поможет разделить сервисы так, как это нужно, и при этом все они будут в одном файле. Тогда для запуска нужных сервисов понадобится указать в команде docker-compose up аргумент —profile с нужным именем профиля.

docker-compose --profile db-only up 

Мы вновь объединили все контейнеры в один файл docker-compose.yml и распределили между ними профили, чтобы выбирать для запуска только те контейнеры, которые нужны сейчас.

version: '3.8'  services:   db:     image: postgres:13     container_name: 'db'     profiles: ['db-only', 'e2e-local-environment', 'local-environment'] # только при запуске с этими профилями сервис будет запущен     environment:       POSTGRES_USER: postgres       POSTGRES_PASSWORD: postgres       POSTGRES_DB: postgres     ports:       - 5432:5432    api:     container_name: 'api'     profiles: ['e2e-local-environment', 'local-environment']     build:       context: .       dockerfile: Api/Dockerfile     ports:       - 5000:5000     depends_on:       - db    karate_tests:     container_name: 'karate_tests'     profiles: ['e2e-local-environment']     build:       dockerfile: KarateDockerfile       context: .     depends_on:       - api     command: ['/karate']     volumes:       - .:/karate     environment:       API_URL: 'http://api' 

Шаблоны

Теперь нам нужно запускать те же самые E2E-тесты, но уже по задеплоенному сервису. Для этого нужно заменить адрес в API_URL на адрес API в проде.
Мы создали второй karate_tests сервис, где переменная окружения API_URL имеет уже другое значение.

version: '3.8'  services:   # остальные сервисы (API, database)    local_karate_tests:     container_name: 'local_karate_tests'     profiles: ['e2e-local-environment']     build:       dockerfile: KarateDockerfile       context: .     depends_on:       - api     command: ['/karate']     volumes:       - .:/karate     environment:       API_URL: 'http://api'    production_karate_tests:     container_name: 'production_karate_tests'     profiles: ['e2e-production-environment']     build:       dockerfile: KarateDockerfile       context: .     depends_on:       - api     command: ['/karate']     volumes:       - .:/karate     environment:       API_URL: 'https://my-deployed-service.com' 

Но получается так, что эти два сервиса, запускающие Karate-тесты, практически полностью дублируют друг друга. У них меняется только API_URL.
Эту проблему мы решили используя фичу Docker Compose — extends блок, который позволяет сервису наследоваться от какого-то другого сервиса и переопределить лишь часть конфигурации, которая отличается между наследуемым сервисом и наследником.
Мы создали шаблон base_karate_tests в файле docker-compose.yml. Он содержит в себе те данные, которые у контейнеров не меняются: KarateDockerfile, из которого собирается образ, команда для запуска и volume.
Теперь применим этот шаблон к сервисам с помощью блока extends таким образом:

version: '3.8'  services:   # остальные сервисы (API, database)    base_karate_tests:     build:       dockerfile: KarateDockerfile       context: .     command: ['/karate']     volumes:       - .:/karate      local_karate_tests:     container_name: 'local_karate_tests'     profiles: ['e2e-local-environment']     extends:       service: base_karate_tests     environment:       API_URL: 'http://api'    production_karate_tests:     container_name: 'production_karate_tests'     profiles: ['e2e-local-environment']     extends:       service: base_karate_tests     environment:       API_URL: 'https://my-deployed-service.com' 

Один шаблон не занимает много места. Однако, если у нас появится система, в которой шаблонов и наследуемых сервисов будет больше, сам шаблон можно вынести в отдельный файл и ссылаться на него. Для этого создадим файл templates.yml.

version: '3.8'  services:   base_karate_tests:     build:       dockerfile: KarateDockerfile       context: .     command: ['karate', '/karate']     volumes:       - .:/karate 

Опишем здесь все нужные шаблоны, а в docker-compose.yml будем использовать параметр file, помимо service, чтобы применить шаблон, но уже из другого файла.

extends:   file: templates.yml   service: base_karate_tests 

Для маленькой системы в этом нет необходимости, но для более сложной автоматизации может быть самое то.

Итоги

Используя Docker Compose мы смогли успешно и с удовольствием запустить E2E-тесты API и в пайплайне мерж-реквеста перед закрытием фичи и после деплоя этой фичи в прод.

При запуске в Docker Compose мы прошли следующий путь эволюции решения:

  • Отдельные файлы для запуска конкретных сервисов, где манифест дублируется;

  • Привязка сервисов к профилям, в которых они должны запускаться;

  • Вынесение дублирующегося кода сервисов в шаблоны.

Итого имеем компактный Docker Compose файл, из которого мы можем запустить только нужные сервисы одной командой и всего лишь с одним аргументом профиля. Также это решение нормально расширяется при добавлении новых режимов запуска.

Если вы сталкивались с подобными проблемами, расскажите нам, как вы их решили?

Авторы: Колесникова Анна, Шинкарев Александр
Вычитка и фидбек: Ядрышникова Мария, Черных Виктор, Сипатов Максим, Магденко Юлия
Оформление: Маргарита Шур


ссылка на оригинал статьи https://habr.com/ru/articles/830002/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *