7 October 2024

Лучший способ создания нескольких окружений для Spring Boot приложения с помощью Docker Compose

В новой статье от команды Amplicode, которая создаёт инструменты разработки web-приложений, я расскажу, как можно создать несколько Docker Compose файлов для разных нужд. Например, для продакшена и разработки, и при этом не утонуть в копипасте.

Статья также доступна в видеоформате для вашего удобства!

Итак, сегодня мы хотим получить два Docker Compose файла. Один будет полезен для продакшена и будет содержать три сервиса: наше Spring Boot приложение, PostgreSQL и Kafka, а второй будет использоваться во время разработки и точно так же будет включать в себя PostgreSQL и Kafka, а также инструменты для работы с ними, а именно pgAdmin и Kafka UI.

Docker Compose для продакшена!?

Отметим, что касательно использования Docker Compose в продакшене однозначного мнения нет. Одни считают, что Docker Compose не подходит для продакшена, и лучше использовать продвинутые системы оркестрации, такие как Kubernetes. Другие считают, что это вполне допустимо при определенных условиях. В общем, как часто бывает в программировании, ответ зависит от контекста.

Способы решения поставленной задачи

Возвращаясь к поставленной задаче: как вы можете заметить, Kafka и PostgreSQL должны присутствовать в обоих Docker Compose файлах, и есть несколько способов решить эту задачу.

Использование одного Docker Compose файла с профилями

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

#многие свойства сервисов не указаны для простоты восприятия 
services: 
  spring-petclinic: 
    image: spring-petclinic:latest 
    profiles: 
      - prod 
  postgres: 
    image: postgres:16.3 
    profiles: 
      - prod 
      - dev 
  kafka: 
    image: confluentinc/cp-kafka:7.6.1 
    profiles: 
      - prod 
      - dev 
  kafkaui: 
    image: provectuslabs/kafka-ui:v0.7.2 
    profiles: 
      - dev 
  pgadmin: 
    image: dpage/pgadmin4:8.12.0 
    profiles: 
      - dev 

Однако, этот вариант немного ограничен с точки зрения использования. Например, я не смогу указать для разных профилей, что для них должны использоваться разные порты или скрипты инициализации.

Создание отдельных Docker Compose файлов для каждого окружения

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

Использование include и extends для переиспользования сервисов

Наконец, есть третий способ, про который, возможно, не все знают. Этот способ подразумевает использование конструкций include и extends в Docker Compose для переиспользования сервисов.

Конструкция include позволяет включить один Docker Compose файл в другой, по сути предоставляя аналог запуска сразу нескольких Docker Compose файлов из терминала.

Файл services.yaml :

#многие свойства сервисов не указаны для простоты восприятия 

services: 
  postgres: 
    image: postgres:16.3 
  kafka: 
    image: confluentinc/cp-kafka:7.6.1 

Файл app-compose.yaml :

 #многие свойства сервисов не указаны для простоты восприятия 

include: 
 - services.yaml 

services: 
 spring-petclinic: 
   image: spring-petclinic:latest

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

И этот подход, в том числе, поддерживается Amplicode с точки зрения визуального отображения:

Но если вам нужно произвести тонкую настройку сервиса под свои нужды, то здесь больше подойдет ключевое слово extends .

С его помощью, точно так же, как и с помощью include , можно включить сервисы из другого Docker Compose файла в текущий. При этом у нас появляется возможность конфигурировать его свойства.

Файл services.yaml :

#многие свойства сервисов не указаны для простоты восприятия 
services: 
 postgres: 
   image: postgres:16.3 
 kafka: 
   image: confluentinc/cp-kafka:7.6.1

Файл app-compose.yaml :

 services: 
  spring-petclinic: 
    image: spring-petclinic:latest 
  postgres: 
    extends: 
      service: postgres 
      file: services.yaml 

  kafka: 
    extends: 
      service: kafka 
      file: services.yaml 

    #так как это расширение указанного сервиса, 
    #мы можем доконфигурировать его свойства  
    ports: 
      - "9092:9092" 

В данной ситуации вариант с extends подойдет лучше всего. Используя этот вариант, можно избежать дублирования кода и при этом сохранить гибкость конфигурирования. Кстати, схожий подход использует и JHipster, генератор Spring Boot приложений. Отличный доклад про этот генератор сделал Илья Кучмин на последнем JPoint. Рекомендую его посмотреть, доклад получился очень интересный:

Решим задачу с extends и include

По легенде мы занимаемся доработкой существующего приложения, в котором уже был реализован следующий Docker Compose файл:

services: 
  spring-petclinic: 
    image: spring-petclinic:latest 
    build: 
      context: . 
      args: 
        DOCKER_BUILDKIT: 1 
    restart: always 
    ports: 
      - "8080:8080" 
    environment: 
      POSTGRES_HOST: postgres 
      POSTGRES_DB: spring-petclinic 
      POSTGRES_USER: root 
      POSTGRES_PASSWORD: root 
      KAFKA_BOOTSTRAP_SERVERS: kafka:29092 
    healthcheck: 
      test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1 
      interval: 30s 
      timeout: 5s 
      start_period: 30s 
      retries: 5 
    depends_on: 
      - postgres 
  postgres: 
    image: postgres:16.3 
    restart: always 
    ports: 
      - "5432:5432" 
    volumes: 
      - postgres_data:/var/lib/postgresql/data 
    environment: 
      POSTGRES_USER: root 
      POSTGRES_PASSWORD: root 
      POSTGRES_DB: spring-petclinic 
    healthcheck: 
      test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB 
      interval: 10s 
      timeout: 5s 
      start_period: 10s 
      retries: 5 
  kafka: 
    image: confluentinc/cp-kafka:7.6.1 
    restart: always 
    ports: 
      - "29092:29092" 
      - "9092:9092" 
    volumes: 
      - kafka_data:/var/lib/kafka/data 
    environment: 
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT 
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT 
      KAFKA_NODE_ID: 1 
      CLUSTER_ID: 8GyRIS62T8aMSkDJs-AH5Q 
      KAFKA_PROCESS_ROLES: controller,broker 
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER 
      KAFKA_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://0.0.0.0:9092,CONTROLLER://kafka:9093 
    healthcheck: 
      test: kafka-topics --bootstrap-server localhost:9092 --list 
      interval: 10s 
      timeout: 5s 
      start_period: 30s 
      retries: 5 
 
volumes: 
  postgres_data: 
  kafka_data:

Docker Compose файл для переиспользуемых сервисов

По сути, этот файл мы можем использовать для продакшена, с небольшими модификациями. Давайте их выполним. Сначала переименуем его в compose.prod.yaml :

Далее, скопируем сервисы Kafka и PostgreSQL в новый Docker Compose файл с сервисами. Именно этот файл мы и будем в дальнейшем переиспользовать в других docker compose файлах, предназначенных для разных целей (продакшена, разработки, тестового окружения и т. д.). Для этого в панели Amplicode Explorer веберем Docker → New → Docker Compose File и зададим ему название services.yaml .

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

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

А в случае с продакшеном я бы вообще не хотел, чтобы какой‑либо сервис, помимо Spring Boot приложения, открывал для внешнего подключения свои порты.

Для Kafka удалю только порты, переменные окружения оставлю, так как они будут одинаковые для двух окружений.

Файл services.yaml готов:

services: 
  postgres: 
    image: postgres:16.3 
    restart: always 
    volumes: 
      - postgres_data:/var/lib/postgresql/data 
    healthcheck: 
      test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB 
      interval: 10s 
      timeout: 5s 
      start_period: 10s 
      retries: 5 
  kafka: 
    image: confluentinc/cp-kafka:7.6.1 
    restart: always 
    volumes: 
      - kafka_data:/var/lib/kafka/data 
    environment: 
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,CONTROLLER:PLAINTEXT 
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT 
      KAFKA_NODE_ID: 1 
      CLUSTER_ID: zNOJ9oWQQWCJqtCat68MLQ 
      KAFKA_PROCESS_ROLES: controller,broker 
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER 
      KAFKA_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://0.0.0.0:9092,CONTROLLER://kafka:9093 
    healthcheck: 
      test: kafka-topics --bootstrap-server localhost:9092 --list 
      interval: 10s 
      timeout: 5s 
      start_period: 30s 
      retries: 5 
volumes: 
  kafka_data: 
  postgres_data:

Docker Compose файл для продакшена

Теперь мне нужно вернуть файл для продакшена в то состояние, в котором он был. А именно, добавить сервисы для PostgreSQL и Kafka. Для этого вызову меню Generate и в нем найду действие Extend Existing Service от Amplicode:

Выберу сервис, имя оставлю без изменений.

В результате сервис в файле compose.prod.yaml будет выглядеть следующим образом:

Для корректной работы приложения сразу после запуска мне нужны некоторые таблицы в базе данных и записи в них. Я могу проинициализировать базу, указав путь к директории, в которой лежат скрипты инициализации. Чтобы сделать это не покидая IDE и не ошибиться с написанием пути можно воспользоваться панелью Amplicode Designer и секцией Init scripts. Используя ее, укажу путь до директории со скриптами в строке Source:

Так как указывать чувствительную информацию в открытом виде в Docker Compose файлах – это довольно грубое нарушение общепринятых политик безопасности, давайте воспользуемся .env файлом. Начнём набирать env_file и Amplicode предложит нам code completion не только для атрибута сервиса:

Но и для названия файла, расположенного в проекте, с расширением .env :

Что самое удобное, так это то, что Amplicode также отобразит все данные из него в соответствующих секциях в панели Amplicode Designer:

Также не забудем удалить переменные окружения из сервиса с нашим Spring Boot приложением:

И укажем .env файл:

Остается добавить Kafka, для которой никаких дополнительных телодвижений тут делать не придется. Аналогичным образом вызовем меню Generate и расширим сервис Kafka:

В итоге файл compose.prod.yaml теперь выглядит следующим образом:

services: 
  spring-petclinic: 
    image: spring-petclinic:latest 
    build: 
      context: . 
      args: 
        DOCKER_BUILDKIT: 1 
    restart: always 
    ports: 
      - "8080:8080" 
    env_file: 
      - prod.env 
    healthcheck: 
      test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1 
      interval: 30s 
      timeout: 5s 
      start_period: 30s 
      retries: 5 
    depends_on: 
      - postgres 
  postgres: 
    extends: 
      service: postgres 
      file: services.yaml 
    env_file: 
      - prod.env 
    volumes: 
      - ./src/main/resources/db/postgres:/docker-entrypoint-initdb.d:ro 
  kafka: 
    extends: 
      service: kafka 
      file: services.yaml 
volumes: 
  postgres_data: 
  kafka_data:

Docker Compose файл для разработки

Теперь провернём аналогичные действия и для Docker Compose файла, который понадобиться нам для разработки.

Сначала расширим сервисы PostgreSQL и Kafka описанные в файле services.yaml . В итоге у нас получится файл compose.dev.yaml со следующим содержимым:

services: 
  postgres: 
    extends: 
      service: postgres 
      file: services.yaml 
    ports: 
      - "5432:5432" 
    volumes: 
      - ./src/main/resources/db/postgres:/docker-entrypoint-initdb.d:ro 
    environment: 
      POSTGRES_DB: postgres 
      POSTGRES_USER: root 
      POSTGRES_PASSWORD: root 
  kafka: 
    extends: 
      service: kafka 
      file: services.yaml   
volumes: 
  postgres_data: 
  kafka_data:

Отмечу, что для PostgreSQL я открыл порт 5432 для внешнего подключения, а для Kafka порт 9092 . Если бы я этого не сделал, то я бы не смог достучаться до базы данных и брокера сообщений извне Docker сети. А, следовательно, если бы я запустил приложение в режиме отладки, подключиться к PostgreSQL или Kafka оно бы не смогло.

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

Что интересно, для PostgreSQL он даже автоматически настроит подключение в pgAdmin, так что мне ничего не нужно будет настраивать после того, как я его запущу и подключусь. Можно будет просто сразу начать пользоваться.

Жмём ОК и pgAdmin сервис готов:

Повторю то же самое и для Kafka UI.

Вот и все:

В итоге, после всех наших манипуляций файл compose.dev.yaml выглядит следующим образом:

services: 
  postgres: 
    extends: 
      service: postgres 
      file: services.yaml 
    ports: 
      - "5432:5432" 
    volumes: 
      - ./src/main/resources/db/postgres:/docker-entrypoint-initdb.d:ro 
    environment: 
      POSTGRES_DB: postgres 
      POSTGRES_USER: root 
      POSTGRES_PASSWORD: root 
  kafka: 
    extends: 
      service: kafka 
      file: services.yaml 
  pgadmin: 
    image: dpage/pgadmin4:8.12.0 
    restart: "no" 
    ports: 
      - "5050:80" 
    volumes: 
      - pgadmin_data:/var/lib/pgadmin 
      - ./docker/pgadmin/servers.json:/pgadmin4/servers.json 
      - ./docker/pgadmin/pgpass:/pgadmin4/pgpass 
    environment: 
      PGADMIN_DEFAULT_EMAIL: admin@admin.com 
      PGADMIN_DEFAULT_PASSWORD: root 
      PGADMIN_CONFIG_SERVER_MODE: "False" 
      PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED: "False" 
    healthcheck: 
      test: wget --no-verbose --tries=1 --spider http://localhost:80/misc/ping || exit -1 
      interval: 10s 
      timeout: 5s 
      start_period: 10s 
      retries: 5 
    entrypoint: /bin/sh -c "chmod 600 /pgadmin4/pgpass; /entrypoint.sh;" 
  kafkaui: 
    image: provectuslabs/kafka-ui:v0.7.2 
    restart: "no" 
    ports: 
      - "8989:8080" 
    environment: 
      DYNAMIC_CONFIG_ENABLED: "true" 
      KAFKA_CLUSTERS_0_NAME: 8GyRIS62T8aMSkDJs-AH5Q 
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 
    healthcheck: 
      test: wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit -1 
      interval: 10s 
      timeout: 5s 
      start_period: 60s 
      retries: 5 
volumes: 
  postgres_data: 
  kafka_data: 
  pgadmin_data: 

Заключение

Теперь любой разработчик без каких-либо проблем сможет в пару кликов запустить все необходимые для разработки сервисы. И у него не будет никаких проблем, связанных с локальным окружением.

Сегодня мы узнали про два очень полезных ключевых слова Docker Compose — include и extends — а также научились ими пользоваться для решения конкретных задач.

Подписывайтесь на наши Telegram и YouTube, чтобы не пропустить новые материалы про Amplicode, Spring и связанные с ним технологии!