Контейнеры стали предпочтительным средством упаковки приложения со всеми зависимостями программного обеспечения и операционной системы, а затем доставки их в различные среды.
В этой статье рассматриваются различные способы контейнеризации приложения 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-hotspotARG JAR_FILE=target/*.jarCOPY ${JAR_FILE} application.jarEXPOSE 8080ENTRYPOINT ["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 SIZEusersignup v1 249MBadoptopenjdk 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 SIZEpaketobuildpacks/run 84.3MBgcr.io/paketo-buildpacks/builder 652MBpratikdas/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.jarBOOT-INF/classpath.idxBOOT-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.jarAvailable 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
dependenciesspring-boot-loadersnapshot-dependenciesapplication
Мы видим список зависимостей, которые можно добавить как слои.
Слои по умолчанию:
Имя слоя |
Содержание |
---|---|
|
любая зависимость, версия которой не содержит SNAPSHOT |
|
Классы загрузчика JAR |
|
любая зависимость, версия которой содержит SNAPSHOT |
|
классы приложений и ресурсы |
Слои определены вlayers.idx
файле в том порядке, в
котором они должны быть добавлены в образ Docker.Эти слои
кэшируются в хосте после первого извлечения, поскольку они не
меняются.На хост загружается только обновленный уровень
приложения, что происходит быстрее из-за уменьшенного
размера.
Построение образа с зависимостями, извлеченными в отдельные слои
Мы построим финальный образ в два этапа, используя метод, называемыймногоэтапной сборкой.На первом этапе мы извлечем зависимости, а на втором этапе мы скопируем извлеченные зависимости в окончательный образ .
Давайте модифицируем наш файл Docker для многоэтапной сборки:
# the first stage of our build will extract the layersFROM adoptopenjdk:14-jre-hotspot as builderWORKDIR applicationARG JAR_FILE=target/*.jarCOPY ${JAR_FILE} application.jarRUN java -Djarmode=layertools -jar application.jar extract# the second stage of our build will copy the extracted layersFROM adoptopenjdk:14-jre-hotspotWORKDIR applicationCOPY --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.41MBStep 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder14-jre-hotspot: Pulling from library/adoptopenjdk..Successfully built a9ebf6970841Successfully 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>