Контейнеры стали предпочтительным средством упаковки приложения со всеми зависимостями программного обеспечения и операционной системы, а затем доставки их в различные среды. В этой статье рассматриваются различные способы контейнеризации приложения 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методом. Затем мы помещаем это приложение в контейнер, добавляя Dockerfile: Наш файл Docker содержит базовый образ, из adoptopenjdk, поверх которого мы копируем наш файл JAR, а затем открываем порт, 8080который будет прослушивать запросы. Сначала нужно создать приложение с помощью Maven или Gradle. Здесь мы используем Maven: Это создает исполняемый JAR-файл приложения. Нам нужно преобразовать этот исполняемый JAR в образ Docker для работы в движке Docker. Затем мы помещаем этот исполняемый файл JAR в образ Docker, выполнив команду docker buildиз корневого каталога проекта, содержащего файл Docker, созданный ранее: Мы можем увидеть наш образ в списке с помощью команды: Результат выполнения вышеуказанной команды включает в себя наш образ usersignupвместе с базовым образом, adoptopenjdk, указанным в нашем файле Docker. Давайте посмотрим на стопку слоев внутри образа. Мы будем использовать инструмент  dive, чтобы просмотреть эти слои: Вот часть результатов выполнения команды Dive:    Как мы видим, прикладной уровень составляет значительную часть размера образа. Мы хотим уменьшить размер этого слоя в следующих разделах в рамках нашей оптимизации. Сборочные пакеты (Buildpacks) - это общий термин, используемый различными предложениями «Платформа как услуга» (PAAS) для создания образа контейнера из исходного кода. Он был запущен Heroku в 2011 году и с тех пор был принят Cloud Foundry, Google App Engine, Gitlab, Knative и некоторыми другими. Одним из основных преимуществ использования Buildpack для создания образов является то, что изменениями конфигурации образа можно управлять централизованно (builder) и распространять на все приложения, использующие builder. Сборочные пакеты были тесно связаны с платформой. Cloud-Native Buildpacks обеспечивают стандартизацию между платформами, поддерживая формат образа OCI, который гарантирует, что образ может запускаться движком Docker. Плагин Spring Boot создает образы OCI из исходного кода с помощью Buildpack. Образы создаются с использованием bootBuildImageзадачи (Gradle) или spring-boot:build-imageцели (Maven) и локальной установки Docker. Мы можем настроить имя образа, необходимого для отправки в реестр Docker, указав имя в image tag: Давайте воспользуемся Maven для выполнения build-imageцели по созданию приложения и созданию образа контейнера. Сейчас мы не используем никаких файлов Docker. Результат будет примерно таким: Из выходных данных мы видим, что paketo Cloud-Native buildpackиспользуется для создания работающего образа OCI. Как и раньше, мы можем увидеть образ, указанный как образ Docker, выполнив команду: Вывод: Jib - это плагин для создания образов от Google, который предоставляет альтернативный метод создания образа контейнера из исходного кода. Настраиваем jib-maven-pluginв pom.xml: Далее мы запускаем плагин Jib с помощью команды Maven, чтобы построить приложение и создать образ контейнера. Как и раньше, здесь мы не используем никаких файлов Docker: После выполнения указанной выше команды Maven мы получаем следующий вывод: Выходные данные показывают, что образ контейнера создан и помещен в реестр. У нас есть две основные причины для оптимизации: Производительность: в системе оркестровки контейнеров образ контейнера извлекается из реестра образов на хост, на котором запущен механизм контейнера. Этот процесс называется планированием. Извлечение образов большого размера из реестра приводит к длительному времени планирования в системах оркестровки контейнеров и длительному времени сборки в конвейерах CI. Безопасность: образа большого размера также имеют большую область для уязвимостей. Образ Docker состоит из стека слоев, каждый из которых представляет инструкцию в нашем Dockerfile. Каждый слой представляет собой дельту изменений нижележащего слоя. Когда мы извлекаем образ Docker из реестра, он извлекается слоями и кэшируется на хосте. Spring Boot использует «толстый JAR» в качестве формата упаковки по умолчанию. Когда мы просматриваем толстый JAR, мы видим, что приложение составляет очень маленькую часть всего JAR. Это часть, которая меняется чаще всего. Оставшаяся часть состоит из зависимостей Spring Framework. Формула оптимизации сосредоточена вокруг изоляции приложения на отдельном уровне от зависимостей Spring Framework. Слой зависимостей, формирующий основную часть толстого JAR-файла, загружается только один раз и кэшируется в хост-системе. Только тонкий слой приложения вытягивается во время обновлений приложения и планирования контейнеров, как показано на этой диаграмме: В следующих разделах мы рассмотрим, как создавать эти оптимизированные образы для приложения Spring Boot. Spring Boot 2.3 поддерживает многоуровневость  путем извлечения частей толстого JAR-файла в отдельные слои. Функция наслоения по умолчанию отключена, и ее необходимо явно включить с помощью плагина Spring Boot Maven: Мы будем использовать эту конфигурацию для создания нашего образа контейнера сначала с помощью Buildpack, а затем с помощью Docker в следующих разделах. Давайте запустим build-imageцель Maven для создания образа контейнера: Если мы запустим Dive, чтобы увидеть слои в результирующем образе, мы увидим, что уровень приложения (обведен красным) намного меньше в диапазоне килобайт по сравнению с тем, что мы получили с использованием толстого формата JAR: Вместо использования плагина Maven или Gradle мы также можем создать многоуровневый образ JAR Docker с файлом Docker. Когда мы используем Docker, нам нужно выполнить два дополнительных шага для извлечения слоев и копирования их в окончательный образ. Содержимое полученного JAR после сборки с помощью Maven с включенной функцией наслоения будет выглядеть следующим образом: В выходных данных отображается дополнительный JAR с именем spring-boot-jarmode-layertoolsи layersfle.idxфайл. Этот дополнительный JAR-файл предоставляет возможность многоуровневой обработки, как описано в следующем разделе. Чтобы просмотреть и извлечь слои из нашего многоуровневого JAR, мы используем системное свойство -Djarmode=layertoolsдля запуска spring-boot-jarmode-layertoolsJAR вместо приложения: Выполнение этой команды дает вывод, содержащий доступные параметры команды: Вывод показывает команды list, extractи helpс helpбыть по умолчанию. Давайте запустим команду с listопцией: Мы видим список зависимостей, которые можно добавить как слои. Слои по умолчанию: Имя слоя Содержание dependencies любая зависимость, версия которой не содержит SNAPSHOT spring-boot-loader Классы загрузчика JAR snapshot-dependencies любая зависимость, версия которой содержит SNAPSHOT application классы приложений и ресурсы Слои определены в layers.idxфайле в том порядке, в котором они должны быть добавлены в образ Docker. Эти слои кэшируются в хосте после первого извлечения, поскольку они не меняются. На хост загружается только обновленный уровень приложения, что происходит быстрее из-за уменьшенного размера . Мы построим финальный образ в два этапа, используя метод, называемый многоэтапной сборкой . На первом этапе мы извлечем зависимости, а на втором этапе мы скопируем извлеченные зависимости в окончательный образ . Давайте модифицируем наш файл Docker для многоэтапной сборки: Сохраняем эту конфигурацию в отдельном файле - Dockerfile2. Собираем образ Docker с помощью команды: После выполнения этой команды мы получаем такой вывод: Мы видим, что образ Docker создается с идентификатором образа, а затем тегируется. Наконец, мы запускаем команду Dive, как и раньше, чтобы проверить слои внутри сгенерированного образа Docker. Мы можем указать идентификатор образа или тег в качестве входных данных для команды Dive: Как видно из выходных данных, уровень, содержащий приложение, теперь занимает всего 11 КБ, а зависимости кэшируются в отдельных слоях.  Мы можем дополнительно уменьшить размер уровня приложения, извлекая любые из наших пользовательских зависимостей в отдельный уровень вместо того, чтобы упаковывать их вместе с приложением, объявив их в ymlподобном файле с именем layers.idx: В этом файле layers.idxмы добавили настраиваемую зависимость с именем, io.myorgсодержащим зависимости организации, полученные из общего репозитория. В этой статье мы рассмотрели использование Cloud-Native Buildpacks для создания образа контейнера непосредственно из исходного кода. Это альтернатива использованию Docker для создания образа контейнера обычным способом: сначала создается толстый исполняемый файл JAR, а затем упаковывается его в образ контейнера, указав инструкции в файле Docker. Мы также рассмотрели оптимизацию нашего контейнера, включив функцию наслоения, которая извлекает зависимости в отдельные уровни, которые кэшируются на хосте, а тонкий слой приложения загружается во время планирования в механизмах выполнения контейнера. Вы можете найти весь исходный код, использованный в статье на Github . Вот краткое изложение команд, которые мы использовали в этой статье для быстрого ознакомления. Очистка контекста: Создание образа контейнера с помощью файла Docker: Собираем образ контейнера из исходного кода (без Dockerfile): Просмотр слоев зависимостей. Перед сборкой JAR-файла приложения убедитесь, что функция наслоения включена в spring-boot-maven-plugin: Извлечение слоев зависимостей. Перед сборкой JAR-файла приложения убедитесь, что функция наслоения включена в spring-boot-maven-plugin: Просмотр списка образов контейнеров Просмотр слев внутри образа контейнера (убедитесь, что установлен инструмент для погружения):