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

В этой статье рассматриваются различные способы контейнеризации приложения Spring Boot:

  • создание образа Docker с помощью файла Docker,

  • создание образа OCI из исходного кода с помощью Cloud-Native Buildpack,

  • и оптимизация образа во время выполнения путем разделения частей JAR на разные уровни с помощью многоуровневых инструментов.

 Пример кода

Эта статья сопровождается примером рабочего кода на GitHub .

Терминология контейнеров

Мы начнем с терминологии контейнеров, используемой в статье:

  • Образ контейнера (Container image): файл определенного формата. Мы конвертируем наше приложение в образ контейнера, запустив инструмент сборки.

  • Контейнер: исполняемый экземпляр образа контейнера.

  • Движок контейнера (Container engine): процесс-демон, отвечающий за запуск контейнера.

  • Хост контейнера (Container host): хост-компьютер, на котором работает механизм контейнера.

  • Реестр контейнеров (Container registry): общее расположение, используемое для публикации и распространения образа контейнера.

  • Стандарт OCIOpen 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 с зависимостями weblombokи 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-layertoolsJAR вместо приложения:

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

Вывод показывает команды listextractи helpс helpбыть по умолчанию. Давайте запустим команду с listопцией:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application

Мы видим список зависимостей, которые можно добавить как слои.

Слои по умолчанию:

Имя слоя

Содержание

dependencies

любая зависимость, версия которой не содержит SNAPSHOT

spring-boot-loader

Классы загрузчика JAR

snapshot-dependencies

любая зависимость, версия которой содержит SNAPSHOT

application

классы приложений и ресурсы

Слои определены в 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>