Створення оптимізованих образів 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 на Весняний ініціалізатор із залежностями web, lombokи actuator. Ми також додаємо rest контролер, щоб надати API з GETметодом.
Створення файлу Docker
Потім ми поміщаємо цю програму в контейнер, додаючи Dockerfile:
Наш файл 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:
Давайте скористаємося Maven для виконання build-imageцілі зі створення програми та створення образу контейнера. Зараз ми не використовуємо жодних файлів Docker.
З вихідних даних ми бачимо, що paketo Cloud-Native buildpackвикористовується для створення працюючого образу OCI. Як і раніше, ми можемо побачити образ, вказаний як образ Docker, виконавши команду:
Далі ми запускаємо Jib плагін за допомогою команди Maven, щоб побудувати додаток і створити образ контейнера. Як і раніше, тут ми не використовуємо жодних файлів Docker:
Після виконання вказаної команди 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:
Ми будемо використовувати цю конфігурацію для створення нашого образу контейнера спочатку за допомогою Buildpack, а потім за допомогою Docker у наступних розділах.
Давайте запустимо build-imageмета Maven для створення образу контейнера:
mvn spring-boot:build-image
Якщо ми запустимо Dive, щоб побачити шари в результуючому зображенні, ми побачимо, що рівень програми (обведений червоним) набагато менший у діапазоні кілобайт порівняно з тим, що ми отримали з використанням товстого формату JAR:
Створення оптимізованого образу контейнера для програми Spring Boot за допомогою Docker
Замість використання плагіна Maven або Gradle ми можемо створити багаторівневий образ JAR Docker з файлом Docker.
Коли ми використовуємо Docker, нам потрібно виконати два додаткові кроки для отримання шарів і копіювання їх в остаточний образ.
Вміст отриманого JAR після складання за допомогою Maven із включеною функцією нашарування виглядатиме таким чином:
У вихідних даних відображається додатковий JAR з ім'ям spring-boot-jarmode-layertoolsи layersfle.idxфайл. Цей додатковий JAR файл надає можливість багаторівневої обробки, як описано в наступному розділі.
Вилучення залежностей на окремих шарах
Щоб переглянути та витягти шари з нашого багаторівневого JAR, ми використовуємо системну властивість -Djarmode=layertoolsдля запуску spring-boot-jarmode-layertoolsJAR замість програми:
Виконання цієї команди дає висновок, що містить доступні параметри команди:
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
будь-яка залежність, версія якої не містить 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:
У цьому файлі 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: