Створення оптимізованих образів Docker для програми Spring Boot

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

У цій статті розглядаються різні способи контейнеризації програми Spring Boot:

  • створення образу Docker за допомогою файлу Docker,
  • створення образу OCI з вихідного коду за допомогою Cloud-Native Buildpack,
  • та оптимізація зображення під час виконання шляхом поділу частин JAR на різні рівні за допомогою багаторівневих інструментів.

 приклад коду

Ця стаття супроводжується прикладом робочого коду на GitHub .

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

Ми почнемо з термінології контейнерів, що використовується у статті:

  • Образ контейнера (Container image): файл певного формату Ми конвертуємо нашу програму в образ контейнера, запустивши інструмент складання.
  • контейнер: екземпляр образу контейнера.
  • Двигун контейнера (Container engine): процес-демон, що відповідає за запуск контейнера
  • Контейнерний хост (Container host): хост-комп'ютер, на якому працює механізм контейнера
  • Реєстр контейнерів (Container registry): загальне розташування, що використовується для публікації та розповсюдження образу контейнера.
  • Стандарт OCIІніціатива відкритих контейнерів (OCI) — це відкрита полегшена структура управління, сформована в рамках Linux Foundation. Специфікація образів OCI визначає галузеві стандарти для форматів образів контейнерів та середовища виконання, щоб гарантувати, що всі механізми контейнерів можуть запускати образи контейнерів, створені будь-яким інструментом збирання.

Щоб помістити додаток у контейнер, ми укладаємо наш додаток у образ контейнера та публікуємо цей образ у загальний реєстр. Середовище виконання контейнера витягує цей образ із реєстру, розпаковує його та запускає додаток усередині нього.

Версія 2.3 Spring Boot надає плагіни для створення образів OCI.

Docker — найчастіше використовувана реалізація контейнера, і ми використовуємо Docker у прикладах, тому всі наступні посилання контейнер у цій статті означатимуть Docker.

Побудова образу контейнера традиційним способом

Створювати образи Docker для додатків Spring Boot дуже легко, додавши кілька інструкцій файлу Docker.

Спочатку ми створюємо виконуваний файл JAR і, як частину інструкцій файлу Docker, копіюємо файл JAR, що виконується, поверх базового образу JRE після застосування необхідних налаштувань.

Давайте створимо наш додаток Spring на Весняний ініціалізатор із залежностями 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: 

Створення оптимізованих образів Docker для програми Spring Boot

Як бачимо, прикладний рівень становить значну частину розміру зображення. Ми хочемо зменшити розмір цього шару у наступних розділах у рамках нашої оптимізації.

Створення образу контейнера за допомогою Buildpack

Складальні пакети (Buildpacks) — це загальний термін, який використовують різні пропозиції «Платформа як послуга» (PAAS) для створення образу контейнера з вихідного коду. Він був запущений Heroku в 2011 році і з тих пір був прийнятий Cloud Foundry, Google App Engine, Gitlab, Knative та деякими іншими.

Створення оптимізованих образів Docker для програми Spring Boot

Перевага хмарних складальних пакетів

Однією з основних переваг використання 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-файлу, завантажується лише один раз і кешується в хост-системі.

Тільки тонкий шар програми витягується під час оновлень програми та планування контейнерів, як показано на цій діаграмі:

Створення оптимізованих образів Docker для програми Spring Boot

У наступних розділах ми розглянемо, як створювати ці оптимізовані образи для 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:

Створення оптимізованих образів Docker для програми Spring Boot

Створення оптимізованого образу контейнера для програми 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 КБ, а залежності кешуються в окремих шарах. 

Створення оптимізованих образів Docker для програми Spring Boot

Вилучення внутрішніх залежностей на окремих шарах

Ми можемо додатково зменшити розмір рівня програми, витягуючи будь-які з наших залежностей користувача в окремий рівень замість того, щоб упаковувати їх разом з додатком, оголосивши їх в 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>

Джерело: habr.com