Dockerfile java spring boot

Учимся разворачивать микросервисы. Часть 1. Spring Boot и Docker

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

Изначально я разбил создание проекта на несколько шагов:

  1. Создать два сервиса — ‘бекенд’ (backend) и ‘шлюз’ (gateway), упаковать их в docker-образы и настроить их совместную работу
    Ключевые слова: Java 11, Spring Boot, Docker, image optimization
  2. Разработка Kubernetes конфигурации и деплой системы в Google Kubernetes Engine
    Ключевые слова: Kubernetes, GKE, resource management, autoscaling, secrets
  3. Создание чарта с помощью Helm 3 для более эффективного управления кластером
    Ключевые слова: Helm 3, chart deployment
  4. Настройка Jenkins и пайплайна для автоматической доставки кода в кластер
    Ключевые слова: Jenkins configuration, plugins, separate configs repository

Каждому шагу я планирую посвятить отдельную статью.

Направленность этого цикла статей заключается не в том, как написать микросервисы, а как заставить их работать в единой системе. Хоть все эти вещи обычно лежат за пределами ответственности разработчика, думаю, что все равно полезно быть знакомым с ними хотя бы на 20% (которые, как известно, дают 80% результата). Некоторые безусловно важные темы, такие как обеспечение безопасности, будут оставлены за скобками этого проекта, так как автор в этом мало что понимает система создается исключительно для личного пользования. Я буду рад любым мнениям и конструктивной критике.

Создание микросервисов

Сервисы были написаны на Java 11 с использованием Spring Boot. Межсервисное взаимодействие организовано с использованием REST. Проект будет включать в себя минимальное количество тестов (чтобы потом было, что тестировать в Jenkins). Исходный код сервисов доступен на GitHub: бекенд и шлюз.

Чтобы иметь возможность проверить состояние каждого из сервисов, в их зависимости был добавлен Spring Actuator. Он создаст эндпойнт /actuator/health и будет возвращать 200 статус, если сервис готов принимать траффик, или 504 в случае проблем. В данном случае это довольно фиктивная проверка, так как сервисы очень просты, и при каком-то форсмажоре они скорее станут полностью недоступны, чем сохранят частичную работоспособность. Но в реальных системах Actuator может помочь диагностировать проблему до того, как об нее начнут биться пользователи. Например, при возникновении проблем с доступом к БД, мы сможем автоматически на это среагировать, прекратив обрабатывать запросы сломанным экземпляром сервиса.

Читайте также:  Python увеличение числа на единицу
Сервис Backend

Сервис бекенда будет просто считать и отдавать количество принятых запросов.

@RestController public class RequestsCounterController < private final AtomicLong counter = new AtomicLong(); @GetMapping("/requests") public Long getRequestsCount() < return counter.incrementAndGet(); >>
@WebMvcTest(RequestsCounterController.class) public class RequestsCounterControllerTests < @Autowired private MockMvc mockMvc; @Test public void firstRequest_one() throws Exception < mockMvc.perform(get("/requests")) .andExpect(status().isOk()) .andExpect(MockMvcResultMatchers.content().string("1")); >>
Сервис Gateway

Шлюз будет переадресовывать запрос сервису бекенда, дополняя его следующей информацией:

  • id шлюза. Он нужен, чтобы можно было по ответу сервера отличить один экземпляр шлюза от другого
  • Некий «секрет», который будет играть роль очень важного пароля (№ ключа шифрования важной куки)

Конфигурация в application.properties:

backend.url=http://localhost:8081 instance.id=$ secret="default-secret"

Адаптер для связи с бекендом:

@Service public class BackendAdapter < private static final String REQUESTS_ENDPOINT = "/requests"; private final RestTemplate restTemplate; @Value("$") private String backendUrl; public BackendAdapter(RestTemplateBuilder builder) < restTemplate = builder.build(); >public String getRequests() < ResponseEntityresponse = restTemplate.getForEntity( backendUrl + REQUESTS_ENDPOINT, String.class); return response.getBody(); > >
@RestController @RequiredArgsConstructor public class EndpointController < private final BackendAdapter backendAdapter; @Value("$") private int instanceId; @Value("$") private String secret; @GetMapping("/") public String getRequestsCount() < return String.format("Number of requests %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret); >>
Запуск:
./mvnw package -DskipTests java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jar
./mvnw package -DskipTests java -jar target/microservices-gateway-1.0.0.jar
$ curl http://localhost:8080/ Number of requests 1 (gateway 38560358, secret "default-secret")

Все работает. Внимательный читатель отметит, что нам ничего не мешает обратиться к бекенду напрямую в обход шлюза (http://localhost:8081/requests). Чтоб это исправить, сервисы должны быть объединены в одну сеть, а наружу «торчать» должен только шлюз.
Также оба сервиса делят одну файловую систему, плодят потоки и в один момент могут начать мешать друг другу. Было бы неплохо изолировать наши микросервисы. Этого можно достичь с помощью разнесения приложений по разным машинам (много денег, сложно), использования виртуальных машин (ресурсоемко, долгий запуск) или же с помощью контейнеризации. Ожидаемо выбираем третий вариант и Docker как инструмент для контейнеризации.

Читайте также:  Css before pseudo elements

Docker

Если вкратце, то докер создает изолированные контейнеры, по одному на приложение. Чтобы использовать докер, требуется написать Dockerfile — инструкцию по сборке и запуску приложения. Далее можно будет собрать образ, загрузить его в реестр образов (№ DockerHub) и в одну команду развернуть свой микросервис в любой докеризированной среде.

Dockerfile

Одна из важнейшей характеристик образа — это его размер. Компактный образ быстрее скачается с удаленного репозитория, займет меньше места, и ваш сервис быстрее стартует. Любой образ строится на основании базового образа, и рекомендуется выбирать наиболее минималистичный вариант. Хорошим вариантом является Alpine — полноценный дистрибутив Linux с минимумом пакетов.

Для начала попробуем написать Dockerfile для сервиса backend «в лоб» (сразу скажу, что это плохой способ, не делайте так):

FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine ADD . /src WORKDIR /src RUN ./mvnw package -DskipTests EXPOSE 8080 ENTRYPOINT ["java","-jar","target/microservices-backend-1.0.0.jar"]

Здесь мы используем базовый образ на основе Alpine с уже установленным JDK для сборки нашего проекта. Командой ADD мы добавляем в образ текущую директорию src, отмечаем ее рабочей (WORKDIR) и запускаем сборку. Команда EXPOSE 8080 сигнализирует докеру, что приложение в контейнере будет использовать его порт 8080 (это не сделает приложение доступным извне, но позволит обратиться к приложению, например, из другого контейнера в той же сети докера).

Чтобы упаковать сервис в образ надо выполнить команду из корня проекта:

docker image build . -t msvc-backend:1.0.0

В результате получаем образ размером в 456 Мбайт (из них базовый образ JDK 340 занял Мбайт). И все притом, что классов в нашем проекте по пальцем пересчитать. Чтобы уменьшить размер нашего образа:

  • Используем многошаговую сборку. На первом шаге соберем проект, на втором установим JRE, а третим шагом скопируем все это в новый чистый Alpine образ. Итого в финальном образе окажутся только необходимые компоненты.
  • Воспользуемся модуляризацией java. Начиная с Java 9, можно с помощью инструмента jlink создать JRE только из нужных модулей

Для любознательных, вот хорошая статья про подходы уменьшения размеров образа https://habr.com/ru/company/ruvds/blog/485650/.

FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine as builder ADD . /src WORKDIR /src RUN ./mvnw package -DskipTests FROM alpine:3.10.3 as packager RUN apk --no-cache add openjdk11-jdk openjdk11-jmods ENV JAVA_MINIMAL="/opt/java-minimal" RUN /usr/lib/jvm/java-11-openjdk/bin/jlink \ --verbose \ --add-modules \ java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument \ --compress 2 --strip-debug --no-header-files --no-man-pages \ --release-info="add:IMPLEMENTOR=radistao:IMPLEMENTOR_VERSION=radistao_JRE" \ --output "$JAVA_MINIMAL" FROM alpine:3.10.3 LABEL maintainer="Anton Shelenkov anshelen@yandex.ru" ENV JAVA_HOME=/opt/java-minimal ENV PATH="$PATH:$JAVA_HOME/bin" COPY --from=packager "$JAVA_HOME" "$JAVA_HOME" COPY --from=builder /src/target/microservices-backend-*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","/app.jar"]

Пересоздаем образ, и он в итоге похудел в 6 раз, составив 77 МБайт. Неплохо. После, готовый образ можно загрузить в реестр образов, чтобы он был доступен для скачивания из интернета.

Читайте также:  Php use class variable in function

Аналогично создадим образ для сервиса gateway и загрузим его в реестр под именем и тегом msvc-gateway:1.0.0.

Совместный запуск сервисов в Docker

Для начала наши сервисы должны быть в одной сети. В докере существует несколько типов сетей, и мы используем самый примитивный из них — bridge, позволяющий объединять в сеть контейнеры, запущенные на одном хосте. Создадим сеть следующей командой:

docker network create msvc-network

Далее запустим контейнер бекенда под именем ‘backend’ с образом msvc-backend:1.0.0:

docker run -dit --name backend --network msvc-network msvc-backend:1.0.0

Стоит отметить, что bridge-сеть предоставляет из коробки service discovery для контейнеров по их именам. То есть сервис бекенда будет доступен внутри сети докера по адресу http://backend:8080.

docker run -dit -p 80:8080 --env secret=my-real-secret --env BACKEND_URL=http://backend:8080/ --name gateway --network msvc-network msvc-gateway:1.0.0

В этой команде мы указываем, что мы пробрасываем 80 порт нашего хоста на 8080 порт контейнера. Опции env мы используем для установки переменных среды, которые автоматически будут вычитаны спрингом и переопределят свойства из application.properties.

После запуска вызываем http://localhost/ и убеждаемся, что все работает, как и в прошлом случае.

Заключение

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

  • Плохая отказоустойчивость — у нас все работает на одном сервере
  • Плохая масштабируемость — при увеличении нагрузки было бы неплохо автоматически разворачивать дополнительные экземпляры сервисов и балансировать нагрузку между ними
  • Сложность запуска — нам понадобилось ввести как минимум 3 команды, причем с определенными параметрами (это только для 2 сервисов)

Для устранения вышеперечисленных проблем существует ряд решений, таких как Docker Swarm, Nomad, Kubernetes или OpenShift. Если вся система будет написана на Java можно посмотреть в сторону Spring Cloud (хорошая статья).

В следующей части я расскажу про то, как я настраивал Kubernetes и деплоил проект в Google Kubernetes Engine.

Источник

Оцените статью