Контейнеры стали предпочтительным средством упаковки приложения со всеми зависимостями программного обеспечения и операционной системы, а затем доставки их в различные среды.
В этой статье рассматриваются различные способы контейнеризации приложения Spring Boot:
создание образа Docker с помощью файла Docker,
создание образа OCI из исходного кода с помощью Cloud-Native Buildpack,
и оптимизация образа во время выполнения путем разделения частей JAR на разные уровни с помощью многоуровневых инструментов.
Пример кода
Эта статья сопровождается примером рабочего кода на GitHub .
Терминология контейнеров
Мы начнем с терминологии контейнеров, используемой в статье:
Образ контейнера (Container image): файл определенного формата. Мы конвертируем наше приложение в образ контейнера, запустив инструмент сборки.
Контейнер: исполняемый экземпляр образа контейнера.
Движок контейнера (Container engine): процесс-демон, отвечающий за запуск контейнера.
Хост контейнера (Container host): хост-компьютер, на котором работает механизм контейнера.
Реестр контейнеров (Container registry): общее расположение, используемое для публикации и распространения образа контейнера.
Стандарт OCI: Open Container Initiative (OCI) - это облегченная открытая структура управления, сформированная в рамках Linux Foundation. Спецификация образов OCI определяет отраслевые стандарты для форматов образов контейнеров и среды выполнения, чтобы гарантировать, что все механизмы контейнеров могут запускать образы контейнеров, созданные любым инструментом сборки.
Чтобы поместить приложение в контейнер, мы заключаем наше приложение в образ контейнера и публикуем этот образ в общий реестр. Среда выполнения контейнера извлекает этот образ из реестра, распаковывает его и запускает приложение внутри него.
Версия 2.3 Spring Boot предоставляет плагины для создания образов OCI.
Docker - наиболее часто используемая реализация контейнера, и мы используем Docker в наших примерах, поэтому все последующие ссылки на контейнер в этой статье будут означать Docker.
Построение образа контейнера традиционным способом
Создавать образы Docker для приложений Spring Boot очень легко, добавив несколько инструкций в файл Docker.
Сначала мы создаем исполняемый файл JAR и, как часть инструкций файла Docker, копируем исполняемый файл JAR поверх базового образа JRE после применения необходимых настроек.
Давайте создадим наше приложение Spring на Spring Initializr с зависимостями web
, lombok
и actuator
. Мы также добавляем rest контроллер, чтобы предоставить API с GET
методом.
Создание файла Docker
Затем мы помещаем это приложение в контейнер, добавляя Dockerfile
:
FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/application.jar"]
Наш файл Docker содержит базовый образ, из adoptopenjdk
, поверх которого мы копируем наш файл JAR, а затем открываем порт, 8080
который будет прослушивать запросы.
Сборка приложения
Сначала нужно создать приложение с помощью Maven или Gradle. Здесь мы используем Maven:
mvn clean package
Это создает исполняемый JAR-файл приложения. Нам нужно преобразовать этот исполняемый JAR в образ Docker для работы в движке Docker.
Создание образа контейнера
Затем мы помещаем этот исполняемый файл JAR в образ Docker, выполнив команду docker build
из корневого каталога проекта, содержащего файл Docker, созданный ранее:
docker build -t usersignup:v1 .
Мы можем увидеть наш образ в списке с помощью команды:
docker images
Результат выполнения вышеуказанной команды включает в себя наш образ usersignup
вместе с базовым образом, adoptopenjdk
, указанным в нашем файле Docker.
REPOSITORY TAG SIZE
usersignup v1 249MB
adoptopenjdk 11-jre-hotspot 229MB
Просмотр слоев внутри образа контейнера
Давайте посмотрим на стопку слоев внутри образа. Мы будем использовать инструмент dive, чтобы просмотреть эти слои:
dive usersignup:v1
Вот часть результатов выполнения команды Dive:
Как мы видим, прикладной уровень составляет значительную часть размера образа. Мы хотим уменьшить размер этого слоя в следующих разделах в рамках нашей оптимизации.
Создание образа контейнера с помощью Buildpack
Сборочные пакеты (Buildpacks) - это общий термин, используемый различными предложениями «Платформа как услуга» (PAAS) для создания образа контейнера из исходного кода. Он был запущен Heroku в 2011 году и с тех пор был принят Cloud Foundry, Google App Engine, Gitlab, Knative и некоторыми другими.
Преимущество облачных сборочных пакетов
Одним из основных преимуществ использования Buildpack для создания образов является то, что изменениями конфигурации образа можно управлять централизованно (builder) и распространять на все приложения, использующие builder.
Сборочные пакеты были тесно связаны с платформой. Cloud-Native Buildpacks обеспечивают стандартизацию между платформами, поддерживая формат образа OCI, который гарантирует, что образ может запускаться движком Docker.
Использование плагина Spring Boot
Плагин Spring Boot создает образы OCI из исходного кода с помощью Buildpack. Образы создаются с использованием bootBuildImage
задачи (Gradle) или spring-boot:build-image
цели (Maven) и локальной установки Docker.
Мы можем настроить имя образа, необходимого для отправки в реестр Docker, указав имя в image tag
:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<name>docker.io/pratikdas/${project.artifactId}:v1</name>
</image>
</configuration>
</plugin>
Давайте воспользуемся Maven для выполнения build-image
цели по созданию приложения и созданию образа контейнера. Сейчас мы не используем никаких файлов Docker.
mvn spring-boot:build-image
Результат будет примерно таким:
[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Building image 'docker.io/pratikdas/usersignup:v1'
[INFO]
[INFO] > Pulling builder image 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
.
.
.. [creator] Adding label 'org.springframework.boot.version'
.. [creator] *** Images (c311fe74ec73):
.. [creator] docker.io/pratikdas/usersignup:v1
[INFO]
[INFO] Successfully built image 'docker.io/pratikdas/usersignup:v1'
Из выходных данных мы видим, что paketo Cloud-Native buildpack
используется для создания работающего образа OCI. Как и раньше, мы можем увидеть образ, указанный как образ Docker, выполнив команду:
docker images
Вывод:
REPOSITORY SIZE
paketobuildpacks/run 84.3MB
gcr.io/paketo-buildpacks/builder 652MB
pratikdas/usersignup 257MB
Создание образа контейнера с помощью Jib
Jib - это плагин для создания образов от Google, который предоставляет альтернативный метод создания образа контейнера из исходного кода.
Настраиваем jib-maven-plugin
в pom.xml:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.5.2</version>
</plugin>
Далее мы запускаем плагин Jib с помощью команды Maven, чтобы построить приложение и создать образ контейнера. Как и раньше, здесь мы не используем никаких файлов Docker:
mvn compile jib:build -Dimage=<docker registry name>/usersignup:v1
После выполнения указанной выше команды Maven мы получаем следующий вывод:
[INFO] Containerizing application to pratikdas/usersignup:v1...
.
.
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO]
[INFO] Built and pushed image as pratikdas/usersignup:v1
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete
Выходные данные показывают, что образ контейнера создан и помещен в реестр.
Мотивации и методы создания оптимизированных образов
У нас есть две основные причины для оптимизации:
Производительность: в системе оркестровки контейнеров образ контейнера извлекается из реестра образов на хост, на котором запущен механизм контейнера. Этот процесс называется планированием. Извлечение образов большого размера из реестра приводит к длительному времени планирования в системах оркестровки контейнеров и длительному времени сборки в конвейерах CI.
Безопасность: образа большого размера также имеют большую область для уязвимостей.
Образ Docker состоит из стека слоев, каждый из которых представляет инструкцию в нашем Dockerfile. Каждый слой представляет собой дельту изменений нижележащего слоя. Когда мы извлекаем образ Docker из реестра, он извлекается слоями и кэшируется на хосте.
Spring Boot использует «толстый JAR» в качестве формата упаковки по умолчанию. Когда мы просматриваем толстый JAR, мы видим, что приложение составляет очень маленькую часть всего JAR. Это часть, которая меняется чаще всего. Оставшаяся часть состоит из зависимостей Spring Framework.
Формула оптимизации сосредоточена вокруг изоляции приложения на отдельном уровне от зависимостей Spring Framework.
Слой зависимостей, формирующий основную часть толстого JAR-файла, загружается только один раз и кэшируется в хост-системе.
Только тонкий слой приложения вытягивается во время обновлений приложения и планирования контейнеров, как показано на этой диаграмме:
В следующих разделах мы рассмотрим, как создавать эти оптимизированные образы для приложения Spring Boot.
Создание оптимизированного образа контейнера для приложения Spring Boot с помощью Buildpack
Spring Boot 2.3 поддерживает многоуровневость путем извлечения частей толстого JAR-файла в отдельные слои. Функция наслоения по умолчанию отключена, и ее необходимо явно включить с помощью плагина Spring Boot Maven:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
Мы будем использовать эту конфигурацию для создания нашего образа контейнера сначала с помощью Buildpack, а затем с помощью Docker в следующих разделах.
Давайте запустим build-image
цель Maven для создания образа контейнера:
mvn spring-boot:build-image
Если мы запустим Dive, чтобы увидеть слои в результирующем образе, мы увидим, что уровень приложения (обведен красным) намного меньше в диапазоне килобайт по сравнению с тем, что мы получили с использованием толстого формата JAR:
Создание оптимизированного образа контейнера для приложения Spring Boot с помощью Docker
Вместо использования плагина Maven или Gradle мы также можем создать многоуровневый образ JAR Docker с файлом Docker.
Когда мы используем Docker, нам нужно выполнить два дополнительных шага для извлечения слоев и копирования их в окончательный образ.
Содержимое полученного JAR после сборки с помощью Maven с включенной функцией наслоения будет выглядеть следующим образом:
META-INF/
.
BOOT-INF/lib/
.
BOOT-INF/lib/spring-boot-jarmode-layertools-2.3.3.RELEASE.jar
BOOT-INF/classpath.idx
BOOT-INF/layers.idx
В выходных данных отображается дополнительный JAR с именем spring-boot-jarmode-layertools
и layersfle.idx
файл. Этот дополнительный JAR-файл предоставляет возможность многоуровневой обработки, как описано в следующем разделе.
Извлечение зависимостей на отдельных слоях
Чтобы просмотреть и извлечь слои из нашего многоуровневого JAR, мы используем системное свойство -Djarmode=layertools
для запуска spring-boot-jarmode-layertools
JAR вместо приложения:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar
Выполнение этой команды дает вывод, содержащий доступные параметры команды:
Usage:
java -Djarmode=layertools -jar usersignup-0.0.1-SNAPSHOT.jar
Available commands:
list List layers from the jar that can be extracted
extract Extracts layers from the jar for image creation
help Help about any command
Вывод показывает команды list
, extract
и help
с help
быть по умолчанию. Давайте запустим команду с list
опцией:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application
Мы видим список зависимостей, которые можно добавить как слои.
Слои по умолчанию:
Имя слоя | Содержание |
---|---|
| любая зависимость, версия которой не содержит SNAPSHOT |
| Классы загрузчика JAR |
| любая зависимость, версия которой содержит SNAPSHOT |
| классы приложений и ресурсы |
Слои определены в layers.idx
файле в том порядке, в котором они должны быть добавлены в образ Docker. Эти слои кэшируются в хосте после первого извлечения, поскольку они не меняются. На хост загружается только обновленный уровень приложения, что происходит быстрее из-за уменьшенного размера .
Построение образа с зависимостями, извлеченными в отдельные слои
Мы построим финальный образ в два этапа, используя метод, называемый многоэтапной сборкой . На первом этапе мы извлечем зависимости, а на втором этапе мы скопируем извлеченные зависимости в окончательный образ .
Давайте модифицируем наш файл Docker для многоэтапной сборки:
# the first stage of our build will extract the layers
FROM adoptopenjdk:14-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract
# the second stage of our build will copy the extracted layers
FROM adoptopenjdk:14-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
Сохраняем эту конфигурацию в отдельном файле - Dockerfile2
.
Собираем образ Docker с помощью команды:
docker build -f Dockerfile2 -t usersignup:v1 .
После выполнения этой команды мы получаем такой вывод:
Sending build context to Docker daemon 20.41MB
Step 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Pulling from library/adoptopenjdk
.
.
Successfully built a9ebf6970841
Successfully tagged userssignup:v1
Мы видим, что образ Docker создается с идентификатором образа, а затем тегируется.
Наконец, мы запускаем команду Dive, как и раньше, чтобы проверить слои внутри сгенерированного образа Docker. Мы можем указать идентификатор образа или тег в качестве входных данных для команды Dive:
dive userssignup:v1
Как видно из выходных данных, уровень, содержащий приложение, теперь занимает всего 11 КБ, а зависимости кэшируются в отдельных слоях.
Извлечение внутренних зависимостей на отдельных слоях
Мы можем дополнительно уменьшить размер уровня приложения, извлекая любые из наших пользовательских зависимостей в отдельный уровень вместо того, чтобы упаковывать их вместе с приложением, объявив их в yml
подобном файле с именем layers.idx
:
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "custom-dependencies":
- "io/myorg/"
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
В этом файле layers.idx
мы добавили настраиваемую зависимость с именем, io.myorg
содержащим зависимости организации, полученные из общего репозитория.
Вывод
В этой статье мы рассмотрели использование Cloud-Native Buildpacks для создания образа контейнера непосредственно из исходного кода. Это альтернатива использованию Docker для создания образа контейнера обычным способом: сначала создается толстый исполняемый файл JAR, а затем упаковывается его в образ контейнера, указав инструкции в файле Docker.
Мы также рассмотрели оптимизацию нашего контейнера, включив функцию наслоения, которая извлекает зависимости в отдельные уровни, которые кэшируются на хосте, а тонкий слой приложения загружается во время планирования в механизмах выполнения контейнера.
Вы можете найти весь исходный код, использованный в статье на Github .
Справочник команд
Вот краткое изложение команд, которые мы использовали в этой статье для быстрого ознакомления.
Очистка контекста:
docker system prune -a
Создание образа контейнера с помощью файла Docker:
docker build -f <Docker file name> -t <tag> .
Собираем образ контейнера из исходного кода (без Dockerfile):
mvn spring-boot:build-image
Просмотр слоев зависимостей. Перед сборкой JAR-файла приложения убедитесь, что функция наслоения включена в spring-boot-maven-plugin:
java -Djarmode=layertools -jar application.jar list
Извлечение слоев зависимостей. Перед сборкой JAR-файла приложения убедитесь, что функция наслоения включена в spring-boot-maven-plugin:
java -Djarmode=layertools -jar application.jar extract
Просмотр списка образов контейнеров
docker images
Просмотр слев внутри образа контейнера (убедитесь, что установлен инструмент для погружения):
dive <image ID or image tag>
zeldigas
Интересно насколько встроенные плагины бута справляются с правильным переиспользованием слоев в случае CI системы и большого числа агентов. До версии 2.3 с её инструментами сборки образов надо было приложить некоторые усилия для этого. Чуть больше года назад писал про это