Отладка Spring-микросервиса в контейнере
Когда речь заходит о микросервисах, на ум обычно приходят контейнеры. Разумеется, встречаются микросервисные архитектуры, в которых компоненты запускаются без контейнеров. На мой взгляд, сопровождение таких систем получается намного сложнее, так как требует более глубоких знаний в администрировании Linux, скриптинге и различных инструментах автоматизации. В то же время, порог вхождения (дисклеймер: подразумевается именно минимально необходимый набор знаний для начала работы с инструментом) для вещей вроде docker-compose существенно ниже, и работать с ними могут даже начинающие разработчики.
Иногда для оперативной локализации ошибки проще всего воспользоваться отладчиком. Я думаю, каждый разработчик так или иначе применял подход DDD (DDD - шут. Debug Driven Development) при локальной разработке или в поисках бага на удаленном стенде. Но что делать, если удаленное приложение в контейнере? В этой заметке я бы хотел поделиться Dockerfile-ом, к которому пришел в свое время, решая проблему отладки контейнеризированного приложения.
За основу был взят Dockerfile из официального руководства Spring. Я опущу подробности про multi-stage build и нюансы распаковки JAR-ника, так как они подробно описаны в оригинальной статье, и сразу перейду к вариантам запуска.
Так может выглядеть Dockerfile для простого запуска springboot-приложения в докере:
# Builder, выполняющий подготовительные манипуляции над JAR-файлом.
FROM eclipse-temurin:18-jdk-focal as builder
WORKDIR application
# Аргумент сборки для передачи JAR-файла.
ARG JAR_FILE=build/libs/application.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
# Результирующий образ, из которого в последствии будет запускаться контейнер.
FROM eclipse-temurin:18-jdk-focal
ENV PORT=8080
WORKDIR application
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
EXPOSE ${PORT:-8080}
# Команда которая запускает приложение на выбранном порту.
ENTRYPOINT else java -Dserver.port=$PORT org.springframework.boot.loader.JarLauncher
Чтобы запустить наше приложение в режиме отладки, придется модифицировать команду запуска:
java -Dserver.port=$PORT -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:[PORT] org.springframework.boot.loader.JarLauncher
Для того чтобы образ был удобен для использования как в режиме отладки, так и в условно боевом режиме, он должен предоставлять возможность выбирать режим запуска извне. Также было бы удобно, если бы мы могли указывать порт отладчика. Для этого мы можем добавить переменные окружения по аналогии с переменной PORT. Пусть это будут следующие переменные:
ENV DEBUG_PORT=8000
ENV DEBUG=false
Теперь мы можем переписать нашу команду таким образом, чтобы желаемый режим учитывался при запуске:
if ($DEBUG = "true");
then
java -Dserver.port=$PORT -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:$DEBUG_PORT org.springframework.boot.loader.JarLauncher;
else
java -Dserver.port=$PORT org.springframework.boot.loader.JarLauncher;
fi
Получившийся Dockerfile имеет следующий вид:
# Builder, выполняющий подготовительные манипуляции над JAR-файлом.
FROM eclipse-temurin:18-jdk-focal as builder
WORKDIR application
# Аргумент сборки для передачи JAR-файла.
ARG JAR_FILE=build/libs/application.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
FROM eclipse-temurin:18-jdk-focal
ENV PORT=8080
# Переменная, задающая режим запуска.
ENV DEBUG=false
# Переменная, задающая порт отладчика.
ENV DEBUG_PORT=8000
# Результирующий образ, из которого в последствии будет запускаться контейнер.
WORKDIR application
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
EXPOSE ${PORT:-8080}
# Oneliner с выбором режима запуска.
ENTRYPOINT if ($DEBUG = "true"); then java -Dserver.port=$PORT -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=0.0.0.0:$DEBUG_PORT org.springframework.boot.loader.JarLauncher; else java -server -Dserver.port=$PORT org.springframework.boot.loader.JarLauncher; fi
Теперь при запуске контейнера, мы можем выбрать режим запуска и при необходимости переопределить порт отладчика.
Предположим, что у нас есть образ собранный из этого dockerfile-а, пусть он называестя debugable-spring-container. Напишем для него docker-compose файл:
version: "3.9"
services:
service:
image: debugable-spring-container
environment:
DEBUG: true
DEBUG_PORT: 1234
ports:
- "8000:1234"
После запуска, отладка будет доступна по адресу localhost:8000.
Вот таким простым образом можно отлаживать Spring-boot приложения, запущенные в контейнере.
А что вы думаете по поводу развертывания микросервисов?
Какие плюсы и минусы у контейнеров по сравнению с развертыванием непосредственно в ОС?
Пожалуйста, напишите в комментариях.