Рубрики
Без рубрики

Руководство автостопщика по контейнеризации (Пружинный ботинок) Java-приложения

Контейнеризация приложения на основе “скриптового” языка проста. Добавьте источники, сделайте это… С тегами docker, container, java, spring boot.

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

FROM python:3

ADD requirements.txt .            # 1
RUN pip install                   # 2

ADD script.py .                   # 3

CMD ["python", "./script.py"]     # 4
  1. Скопируйте описание зависимостей
  2. Загрузите зависимости
  3. Скопируйте основной сценарий
  4. Запустите сценарий

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

Пример приложения

Чтобы описать эти альтернативы, нам нужен пример приложения. Мы будем использовать Spring Boot, который предлагает конечную точку REST и хранит данные в Hazelcast. Он построен с использованием Maven, с существующей оболочкой .

Конечная точка REST работает следующим образом:

curl -X PUT http://localhost:8080/John
{"who":"John","when":64244336297226}
curl http://localhost:8080/
[{"who":"John","when":64244336297226}]

По сравнению с языками сценариев приложения Java имеют два основных отличия:

  1. Они требуют дополнительного шага компиляции, который преобразует исходный код Java в байт-код
  2. Модуль развертывания, как правило, является самоисполняемым кувшин

Наивный подход

В качестве первого шага можно было бы создать приложение вне Docker, а затем добавить JAR к изображению.

./mvnw clean package -DskipTests
# docker build -t spring-in-docker:0.5 .

FROM adoptopenjdk/openjdk11:alpine-jre

COPY target/spring-in-docker-0.5.jar spring-in-docker.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "spring-in-docker.jar"]

Следующим логическим шагом является создание приложения внутри Dockerfile :

# docker build -t spring-in-docker:1.0 .

FROM adoptopenjdk/openjdk11:alpine-slim

COPY .mvn .mvn
COPY mvnw .
COPY pom.xml .
COPY src src
RUN ./mvnw package -DskipTests

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "target/spring-in-docker-1.0.jar"]

Этот способ имеет несколько недостатков:

  • В итоговое изображение встроен JDK . Во-первых, это увеличивает размер конечного изображения по сравнению с JRE . Что еще более важно, это позволяет образу компилировать Java-код: это может стать серьезной дырой в безопасности при производстве.
  • Версия в Maven POM должна быть вручную синхронизирована с версией изображения.
  • Для JAR есть один слой OCI.

Исполняемый файл dive выделяет последнюю точку:

Layers ├─────────────────────────────────────────────────────────────────────────────
Size     Command
5.6 MB   FROM 31609b718dd2bed
 14 MB   apk add --no-cache tzdata --virtual .build-deps curl binutils zstd     && GL
 17 kB   #(nop) COPY multi:5542ba69976bc682acd7b679c22d8a0277609ba9f5b611fd518f87f209
235 MB   set -eux;     apk add --no-cache --virtual .fetch-deps curl;     ARCH="$(apk
 56 kB   #(nop) COPY dir:20c328136da94aa01b2b6fd62c88ef506a15b545aefeb1a1e13473572aee
 10 kB   #(nop) COPY file:08c603013feae81d794c29c4c1f489cc58a32bd593154cc5e40c6afa522
1.8 kB   #(nop) COPY file:1bb01c4e5b60aae391d2efc563ead23a959701863adcf408540f33b7e40
 54 kB   #(nop) COPY dir:d2bd6e2521e5d990b16efb81ae8823e23ed2de826e833b331718b2211d6a
108 MB   ./mvnw package -DskipTests


│ ● Current Layer Contents ├─────────────────────────────────────────────────────────
Permission     UID:GID     Size  Filetree
drwx------       0:0      80 MB  ├─⊕ root
drwxr-xr-x       0:0      28 MB  └── target
drwxr-xr-x       0:0     5.8 kB      ├─⊕ classes
drwxr-xr-x       0:0        0 B      ├─⊕ generated-sources
drwxr-xr-x       0:0       64 B      ├─⊕ maven-archiver
drwxr-xr-x       0:0      364 B      ├─⊕ maven-status
-rw-r--r--       0:0      28 MB      ├── spring-in-docker-1.0.jar
-rw-r--r--       0:0     5.4 kB      └── spring-in-docker-1.0.jar.original

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

Многоступенчатые сборки на помощь

Многоступенчатые сборки Docker позволяют объединять в цепочку несколько этапов сборки, при этом последующие этапы цепочки повторно используют артефакты, созданные на предыдущих этапах. Таким образом, мы можем использовать JDK для компиляции и JRE для выполнения:

# docker build -t spring-in-docker:1.1 .

FROM adoptopenjdk/openjdk11:alpine-slim as build                         # 1

COPY .mvn .mvn
COPY mvnw .
COPY pom.xml .
COPY src src
RUN ./mvnw package -DskipTests

FROM adoptopenjdk/openjdk11:alpine-jre                                   # 2

COPY --from=build target/spring-in-docker-1.1.jar spring-in-docker.jar   # 3

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "spring-in-docker.jar"]
  1. На этапе сборки используется JDK
  2. Шаг выполнения использует JRE
  3. Скопируйте JAR, созданный на предыдущем шаге build

Многоступенчатые сборки создают по одному изображению на каждом шаге. Все изображения, кроме последних, не помечены.

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

# docker build -t spring-in-docker:1.2 .

FROM adoptopenjdk/openjdk11:alpine-slim as build

COPY .mvn .mvn                                                           # 1
COPY mvnw .                                                              # 1
COPY pom.xml .                                                           # 1
RUN ./mvnw dependency:go-offline                                          # 2

COPY src src                                                             # 3
RUN ./mvnw package -DskipTests

FROM adoptopenjdk/openjdk11:alpine-jre

COPY --from=build target/spring-in-docker-1.2.jar spring-in-docker.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "spring-in-docker.jar"]
  1. Скопируйте все необходимые файлы для загрузки зависимостей
  2. Загрузите зависимости – они будут частью выделенного слоя
  3. Теперь скопируйте исходные файлы; это добавит еще один слой

Давайте погрузимся в построение образа:

Layers ├────────────────────────────────────────────────────────────────────────────
  Size  Command
5.6 MB  FROM 31609b718dd2bed
 14 MB  apk add --no-cache tzdata --virtual .build-deps curl binutils zstd     && GL
 17 kB  #(nop) COPY multi:5542ba69976bc682acd7b679c22d8a0277609ba9f5b611fd518f87f209
235 MB  set -eux;     apk add --no-cache --virtual .fetch-deps curl;     ARCH="$(apk
 56 kB  #(nop) COPY dir:20c328136da94aa01b2b6fd62c88ef506a15b545aefeb1a1e13473572aee
 10 kB  #(nop) COPY file:08c603013feae81d794c29c4c1f489cc58a32bd593154cc5e40c6afa522
1.8 kB  #(nop) COPY file:f191db2f3a7fe2e434025c321ad8106112373b1aa0fa99f1a76c884bf61
100 MB  ./mvnw dependency:go-offline
 54 kB  #(nop) COPY dir:d2bd6e2521e5d990b16efb81ae8823e23ed2de826e833b331718b2211d6a # 1
 28 MB  ./mvnw package -DskipTests
  1. Уровень зависимостей

С последним Dockerfile нам удалось решить две проблемы: проблему безопасности, исходящую от JDK, и проблему многоуровневости. Тем не менее, нам нужно установить версию вручную, когда мы создаем изображение. Нет никакой синхронизации между стихами и изображением.

Более того, многоступенчатые сборки несовместимы с skaffold . Если вы привыкли автоматически запускать развертывания в (локальном) кластере Kubernetes при изменении исходного кода, забудьте о них.

Кливер

Jib – это плагин Maven (также доступный для Gradle), предоставляемый Google, который элегантно решает вышеуказанные проблемы.

Концепция, лежащая в основе Jib, проста, но умна. Java позволяет запускать JARS, но также и стандартные классы Java. За пределами мира контейнеров JAR является хорошим средством развертывания. Однако в мире контейнеров JAR – это просто дополнительная оболочка, поскольку контейнер является единицей развертывания.

Плагины Jib подключаются к системе сборки для компиляции исходных текстов Java, копирования соответствующих ресурсов в слои и создания образа, который запускает приложение в “развернутом” (не JAR) формате.


  com.google.cloud.tools
  jib-maven-plugin
  2.5.2
  
    
      ${project.artifactId}:${project.version} 
    
  

  1. Автоматическая синхронизация версии изображения с версией POM

Задание предлагает две цели: build для загрузки образа в репозиторий Docker и docker Build для сборки в Docker daemon . Давайте создадим изображение локально.

mvn compile com.google.cloud.tools:jib-maven-plugin:2.6.0

dive выводит следующее:

Layers ├────────────────────────────────────────────────────────────────────────────
  Size  Command
1.8 MB  FROM 7cfeac17984f4f4
 15 MB  bazel build ...
1.9 MB  bazel build ...
8.4 MB  bazel build ...
170 MB  bazel build ...
 16 MB  jib-maven-plugin:2.5.2
 12 MB  jib-maven-plugin:2.5.2
   1 B  jib-maven-plugin:2.5.2
5.8 kB  jib-maven-plugin:2.5.2

Кливер создает 4 слоя, от самого старого до самого последнего:

  1. Зависимости
  2. Зависимости моментальных снимков
  3. Ресурсы
  4. Скомпилированный код

Второй уровень обрабатывает случай SNAPSHOT зависимости: это зависимости, содержимое которых может изменяться, несмотря на тот же номер версии. Это может иметь место во время разработки. Вы не должны развертывать зависимости моментальных снимков в рабочей среде.

Командная строка контейнера Docker похожа на:

java -cp /app/resources:/app/classes:/app/libs/* ch.frankel.blog.springindocker.SpringInDockerApplication

Кроме того, изменение родительского изображения созданного изображения является простым:


  com.google.cloud.tools
  jib-maven-plugin
  2.5.2
  
    
      adoptopenjdk/openjdk11:alpine-jre 
    
    
      ${project.artifactId}:${project.version}
    
  

  1. Измените родительское изображение на Alpine

Кливер, по-видимому, является лучшей альтернативой. Но давайте продолжим изучать другие варианты.

Пружинный ботинок многослойная БАНКА

С версией 2.3 Spring Boot позволяет создавать JAR с выделенной структурой папок. Вы можете сопоставить эти папки со слоями в Dockerfile . По умолчанию это:

  1. Зависимости
  2. Зависимости моментальных снимков
  3. Время выполнения весенней загрузки
  4. Ресурсы и скомпилированный код

Можно настроить эти папки с помощью определенного layers.xml файл.

Вот файл многоступенчатой сборки, в котором показано, как создать приложение Spring Boot со слоями по умолчанию:

FROM adoptopenjdk/openjdk11:alpine-slim as builder

COPY .mvn .mvn
COPY mvnw .
COPY pom.xml .
RUN ./mvnw dependency:go-offline

COPY src src
RUN ./mvnw package -DskipTests                                            # 1

FROM adoptopenjdk/openjdk11:alpine-jre as layers

COPY --from=builder target/spring-in-docker-3.0.jar spring-in-docker.jar
RUN java -Djarmode=layertools -jar spring-in-docker.jar extract           # 2

FROM adoptopenjdk/openjdk11:alpine-jre

COPY --from=layers dependencies/ .                                        # 3
COPY --from=layers snapshot-dependencies/ .                               # 3
COPY --from=layers spring-boot-loader/ .                                  # 3
COPY --from=layers application/ .                                         # 3

ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
  1. Создайте стандартный самоисполняемый JAR
  2. Извлеките структуру папок
  3. Скопируйте каждую папку в слой

Этот подход имеет все недостатки Dockerfile , описанные выше: нет интеграции с scaffold и нет синхронизации с версией POM.

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

Встроенные в Облако пакеты сборки

Встроенные в облако пакеты сборки исходят из пакетов сборки Heroku. Heroku – одна из первых платформ облачного хостинга. Он также предлагает репозитории Git. Чтобы развернуть на Heroku, вам просто нужно отправить исходный код в удаленный репозиторий Heroku.

Платформа понимает, как создать исполняемый файл из исходных текстов. Он проверяет наличие некоторых файлов в качестве подсказок. Например, если репозиторий содержит pom.xml файл в корне, он активирует Maven buildpack; если он содержит package.json , он активирует Node.js один; и т.д.

CNBs – это обновление пакетов Heroku buildpacks, предназначенных для контейнеров OCI. Heroku и VMware Tanzu – компания, стоящая за Spring Boot, возглавляют проект. Он размещен по адресу CNCF .

Чтобы использовать buildpack, просто вызовите команду pack со ссылкой на builder и тегом изображения для сборки. Он создаст приложение и унаследует родительский образ по умолчанию. Например, вот командная строка для создания примера приложения:

pack build --builder gcr.io/paketo-buildpacks/builder:base-platform-api-0.3 spring-in-docker:4.0

Он запускает пакеты сборки, которые применяются к проекту:

===> DETECTING
[detector] 6 of 17 buildpacks participating
[detector] paketo-buildpacks/bellsoft-liberica 4.0.0
[detector] paketo-buildpacks/maven             3.1.1
[detector] paketo-buildpacks/executable-jar    3.1.1
[detector] paketo-buildpacks/apache-tomcat     2.3.0
[detector] paketo-buildpacks/dist-zip          2.2.0
[detector] paketo-buildpacks/spring-boot       3.2.1

[builder] Paketo BellSoft Liberica Buildpack 4.0.0
[builder]   https://github.com/paketo-buildpacks/bellsoft-liberica
[builder]   Build Configuration:
[builder]     $BP_JVM_VERSION              11.*            the Java version             # 1
[builder]   Launch Configuration:
[builder]     $BPL_JVM_HEAD_ROOM           0               the headroom in memory calculation
[builder]     $BPL_JVM_LOADED_CLASS_COUNT  35% of classes  the number of loaded classes in memory calculation
[builder]     $BPL_JVM_THREAD_COUNT        250             the number of threads in memory calculation
[builder]     $JAVA_TOOL_OPTIONS                           the JVM launch flags
  1. Определите версию JVM. Buildpack загружает правильный JDK.

Я столкнулся с несколькими недостатками:

  • base-platform-api-0.3 не является |/неизменяемым . Сборка работала ранее; на момент написания этой статьи она завершилась ошибкой Go. Загрузка JDK происходит при каждом запуске, несмотря на то, что он был загружен в предыдущих запусках.
  • Мне было нелегко изменить родительский образ.

Кроме того, как и в случае с Dockerfile , вам необходимо вручную пометить версию.

Плагин Spring Boot plugin

С Spring Boot нет необходимости вызывать внешнюю команду. Плагин предлагает build-image target, который выполняет то же самое, что и pack , вызывая соответствующий buildpack.

Давайте запустим его:

./mvnw spring-boot:build-image
[INFO]  > Running creator
[INFO]     [creator]     ===> DETECTING
[INFO]     [creator]     5 of 17 buildpacks participating
[INFO]     [creator]     paketo-buildpacks/bellsoft-liberica 4.0.0
[INFO]     [creator]     paketo-buildpacks/executable-jar    3.1.1
[INFO]     [creator]     paketo-buildpacks/apache-tomcat     2.3.0
[INFO]     [creator]     paketo-buildpacks/dist-zip          2.2.0
[INFO]     [creator]     paketo-buildpacks/spring-boot       3.2.1

Этот вариант имеет массу преимуществ:

  • Версия изображения автоматически считывается из версии POM.
  • Повторные сборки без изменений выполняются быстро.
  • С параметром конфигурации сборки конечный образ имеет 4 слоя загрузки Spring, описанные выше.

Более того, некоторые пакеты сборки позволяют легко настраивать процесс сборки. Например, чтобы сделать конечный артефакт собственным исполняемым файлом, достаточно просто добавить необходимую переменную среды:


  org.springframework.boot
  spring-boot-maven-plugin
  
    
      true
    
    
      ${project.artifactId}:${project.version}
      
        true
      
    
  

Самая большая проблема заключается в том, что изменить родительский образ непросто.

Резюмировать

Вот окончательные изображения с их соответствующим размером. Я также пометил изображения, используемые в многоступенчатых сборках.

REPOSITORY                TAG          IMAGE ID       CREATED         SIZE
spring-in-docker          0.5          ca380d4677f9   3 days ago      177MB
spring-in-docker          1.0          f16667a974f4   3 days ago      363MB
spring-in-docker/build    1.1          2f2a59f49486   3 days ago      363MB
spring-in-docker          1.1          45ae57fab5ae   3 days ago      177MB
spring-in-docker/build    1.2          b94b6a80a437   3 days ago      383MB
spring-in-docker          1.2          cbddb2300b1a   3 days ago      177MB
spring-in-docker          2.0          fb7d8501623a   50 years ago    225MB
spring-in-docker          2.1          c3b60a214da2   50 years ago    177MB
spring-in-docker/build    3.0-b        1eea78545af2   2 days ago      206MB
spring-in-docker/build    3.0-a        52c180e9f3d1   2 days ago      383MB
spring-in-docker          3.0          9e2240a4fc00   2 days ago      177MB
spring-in-docker          5.0          4cbda769276f   40 years ago    264MB
spring-in-docker          5.5          4ac2d37253ee   40 years ago    184MB
Многоступенчатый Инструкция Нет
Кливер Да Конфигурация
Buildpack Детская коляска Да Нет
Плагин Spring Boot Maven Менее гибкая, чем многоступенчатые сборки, Мощная параметризация в зависимости от пакета сборки Конфигурация Да

Чтобы идти дальше:

Первоначально опубликовано на Фанат Java 11 октября th , 2020

Оригинал: “https://dev.to/nfrankel/a-hitchhiker-s-guide-to-containerizing-spring-boot-java-apps-3mb8”