Tworzenie zoptymalizowanych obrazów platformy Docker dla aplikacji Spring Boot

Kontenery stały się preferowanym sposobem pakowania aplikacji ze wszystkimi zależnościami oprogramowania i systemu operacyjnego, a następnie dostarczania ich do różnych środowisk.

W tym artykule opisano różne sposoby konteneryzacji aplikacji Spring Boot:

  • utworzenie obrazu Dockera przy użyciu pliku Docker,
  • tworzenie obrazu OCI ze źródła przy użyciu Cloud-Native Buildpack,
  • oraz optymalizację obrazu w czasie wykonywania poprzez rozdzielenie części pliku JAR na różne warstwy przy użyciu narzędzi wielowarstwowych.

 Przykład kodu

Do artykułu dołączony jest przykład działającego kodu na GitHub .

Terminologia kontenerowa

Zaczniemy od terminologii kontenerowej użytej w artykule:

  • Obraz kontenera: plik o określonym formacie. Przekonwertujemy naszą aplikację na obraz kontenera, uruchamiając narzędzie do kompilacji.
  • pojemnik: Wykonywalna instancja obrazu kontenera.
  • Silnik kontenerowy: Proces demona odpowiedzialny za uruchomienie kontenera.
  • Host kontenerowy: Komputer hosta, na którym działa silnik kontenera.
  • Rejestr kontenerów: Ogólna lokalizacja używana do publikowania i rozpowszechniania obrazu kontenera.
  • Norma OCIInicjatywa Open Container (OCI) to lekka, otwarta struktura zarządzania utworzona w ramach Linux Foundation. Specyfikacja obrazu OCI definiuje standardy branżowe dla formatów obrazów kontenerów i środowiska wykonawczego, aby zapewnić, że wszystkie silniki kontenerów będą mogły uruchamiać obrazy kontenerów utworzone za pomocą dowolnego narzędzia do kompilacji.

Aby konteneryzować aplikację, otaczamy ją obrazem kontenera i publikujemy ten obraz we wspólnym rejestrze. Środowisko wykonawcze kontenera pobiera ten obraz z rejestru, rozpakowuje go i uruchamia w nim aplikację.

Wersja 2.3 Spring Boot zawiera wtyczki do tworzenia obrazów OCI.

Doker to najczęściej używana implementacja kontenera, a w naszych przykładach używamy Dockera, więc wszystkie kolejne odniesienia do kontenerów w tym artykule będą odnosić się do Dockera.

Budowanie obrazu kontenera w tradycyjny sposób

Tworzenie obrazów Dockera dla aplikacji Spring Boot jest bardzo proste poprzez dodanie kilku instrukcji do pliku Docker.

Najpierw tworzymy wykonywalny plik JAR i w ramach instrukcji pliku Docker kopiujemy wykonywalny plik JAR na podstawowy obraz JRE po zastosowaniu niezbędnych ustawień.

Stwórzmy naszą aplikację Spring na Wiosna Initializr z zależnościami weblombokи actuator. Dodajemy także kontroler odpoczynku, za pomocą którego można udostępniać interfejs API GETmetoda.

Tworzenie pliku Dockerfile

Następnie konteneryzujemy tę aplikację, dodając Dockerfile:

FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/application.jar"]

Nasz plik Docker zawiera obraz podstawowy z adoptopenjdk, na który kopiujemy nasz plik JAR i następnie otwieramy port, 8080który będzie nasłuchiwał próśb.

Budowanie aplikacji

Najpierw musisz stworzyć aplikację za pomocą Mavena lub Gradle. Tutaj używamy Mavena:

mvn clean package

Spowoduje to utworzenie wykonywalnego pliku JAR dla aplikacji. Musimy przekonwertować ten wykonywalny plik JAR na obraz Dockera, aby działał w silniku Dockera.

Tworzenie obrazu kontenera

Następnie umieszczamy ten wykonywalny plik JAR w obrazie Dockera, uruchamiając polecenie docker buildz katalogu głównego projektu zawierającego utworzony wcześniej plik Dockerfile:

docker build  -t usersignup:v1 .

Nasz obraz możemy zobaczyć na liście za pomocą polecenia:

docker images 

Dane wyjściowe powyższego polecenia zawierają nasz obraz usersignupwraz z obrazem bazowym, adoptopenjdkokreślone w naszym pliku Docker.

REPOSITORY          TAG                 SIZE
usersignup          v1                  249MB
adoptopenjdk        11-jre-hotspot      229MB

Wyświetl warstwy wewnątrz obrazu kontenera

Przyjrzyjmy się stosowi warstw wewnątrz obrazu. Użyjemy инструмент  nurkować aby wyświetlić te warstwy:

dive usersignup:v1

Oto część wyników polecenia Dive: 

Tworzenie zoptymalizowanych obrazów platformy Docker dla aplikacji Spring Boot

Jak widać, warstwa aplikacji stanowi znaczną część rozmiaru obrazu. W ramach naszej optymalizacji chcemy zmniejszyć rozmiar tej warstwy w kolejnych sekcjach.

Tworzenie obrazu kontenera za pomocą Buildpack

Pakiety montażowe (Pakiety konstrukcyjne) to ogólny termin używany w różnych ofertach Platform as a Service (PAAS) do tworzenia obrazu kontenera z kodu źródłowego. Został uruchomiony przez Heroku w 2011 roku i od tego czasu został przyjęty przez Cloud Foundry, Google App Engine, Gitlab, Knative i kilka innych.

Tworzenie zoptymalizowanych obrazów platformy Docker dla aplikacji Spring Boot

Zaleta pakietów do budowania w chmurze

Jedną z głównych zalet używania Buildpack do tworzenia obrazów jest to Zmianami konfiguracji obrazu można zarządzać centralnie (konstruktor) i propagować je do wszystkich aplikacji za pomocą kreatora.

Pakiety kompilacji były ściśle powiązane z platformą. Pakiety kompilacji natywne dla chmury zapewniają standaryzację na różnych platformach, obsługując format obrazu OCI, co gwarantuje, że obraz może być uruchamiany przez silnik Docker.

Korzystanie z wtyczki Spring Boot

Wtyczka Spring Boot buduje obrazy OCI ze źródła za pomocą Buildpack. Obrazy tworzone są za pomocą bootBuildImagezadania (Gradle) lub spring-boot:build-imagetargets (Maven) i lokalna instalacja Dockera.

Możemy dostosować nazwę obrazu potrzebnego do przekazania do rejestru Dockera, podając nazwę w 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>

Użyjmy do tego Mavena build-imagecele tworzenia aplikacji i tworzenia obrazu kontenera. W tej chwili nie używamy żadnych plików Dockerfile.

mvn spring-boot:build-image

Wynik będzie mniej więcej taki:

[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'

Z danych wyjściowych to widzimy paketo Cloud-Native buildpackużywany do tworzenia działającego obrazu OCI. Tak jak poprzednio, możemy zobaczyć obraz wymieniony jako obraz Dockera, uruchamiając polecenie:

docker images 

Wnioski:

REPOSITORY                             SIZE
paketobuildpacks/run                  84.3MB
gcr.io/paketo-buildpacks/builder      652MB
pratikdas/usersignup                  257MB

Tworzenie obrazu kontenera za pomocą Jib

Jib to wtyczka Google do tworzenia obrazów, która zapewnia alternatywną metodę tworzenia obrazu kontenera z kodu źródłowego.

Konfiguracja jib-maven-pluginw pom.xml:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.5.2</version>
      </plugin>

Następnie uruchamiamy wtyczkę Jib za pomocą polecenia Maven w celu zbudowania aplikacji i utworzenia obrazu kontenera. Tak jak poprzednio, nie używamy tutaj żadnych plików Dockera:

mvn compile jib:build -Dimage=<docker registry name>/usersignup:v1

Po wykonaniu powyższego polecenia Maven otrzymujemy następujące dane wyjściowe:

[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

Dane wyjściowe pokazują, że obraz kontenera został utworzony i umieszczony w rejestrze.

Motywacje i techniki tworzenia zoptymalizowanych obrazów

Mamy dwa główne powody optymalizacji:

  • produktywność: W systemie orkiestracji kontenerów obraz kontenera jest pobierany z rejestru obrazów do hosta, na którym działa silnik kontenera. Proces ten nazywa się planowaniem. Wyciąganie dużych obrazów z rejestru skutkuje długimi czasami planowania w systemach orkiestracji kontenerów i długimi czasami kompilacji w potokach CI.
  • bezpieczeństwo: Większe obrazy mają również większy obszar dla luk w zabezpieczeniach.

Obraz Dockera składa się ze stosu warstw, z których każda reprezentuje instrukcję w naszym pliku Dockerfile. Każda warstwa reprezentuje deltę zmian w warstwie bazowej. Kiedy pobieramy obraz Dockera z rejestru, jest on pobierany warstwami i buforowany na hoście.

Używa Spring Boot „gruby słoik” w jako domyślny format opakowania. Kiedy patrzymy na gruby plik JAR, widzimy, że aplikacja stanowi bardzo małą część całego pliku JAR. To część, która zmienia się najczęściej. Pozostała część składa się z zależności Spring Framework.

Formuła optymalizacyjna koncentruje się na izolowaniu aplikacji na innym poziomie od zależności Spring Framework.

Warstwa zależności, która stanowi większość grubego pliku JAR, jest pobierana tylko raz i buforowana w systemie hosta.

Podczas aktualizacji aplikacji i planowania kontenerów pobierana jest tylko cienka warstwa aplikacji. jak pokazano na tym schemacie:

Tworzenie zoptymalizowanych obrazów platformy Docker dla aplikacji Spring Boot

W poniższych sekcjach przyjrzymy się, jak utworzyć zoptymalizowane obrazy dla aplikacji Spring Boot.

Tworzenie zoptymalizowanego obrazu kontenera dla aplikacji Spring Boot przy użyciu pakietu Buildpack

Spring Boot 2.3 obsługuje nakładanie warstw, wyodrębniając części grubego pliku JAR na osobne warstwy. Funkcja warstw jest domyślnie wyłączona i należy ją jawnie włączyć za pomocą wtyczki Spring Boot Maven:

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <layers>
      <enabled>true</enabled>
    </layers>
  </configuration> 
</plugin>

W poniższych sekcjach użyjemy tej konfiguracji do zbudowania obrazu kontenera najpierw za pomocą pakietu Buildpack, a następnie platformy Docker.

Uruchommy build-imageCel Mavena do tworzenia obrazu kontenera:

mvn spring-boot:build-image

Jeśli uruchomimy Dive, aby zobaczyć warstwy na powstałym obrazie, zobaczymy, że warstwa aplikacji (zaznaczona na czerwono) jest znacznie mniejsza w zakresie kilobajtów w porównaniu do tego, co otrzymaliśmy przy użyciu grubego formatu JAR:

Tworzenie zoptymalizowanych obrazów platformy Docker dla aplikacji Spring Boot

Tworzenie zoptymalizowanego obrazu kontenera dla aplikacji Spring Boot przy użyciu platformy Docker

Zamiast używać wtyczki Maven lub Gradle, możemy również utworzyć warstwowy obraz JAR Dockera z plikiem Docker.

Kiedy używamy Dockera, musimy wykonać dwa dodatkowe kroki, aby wyodrębnić warstwy i skopiować je do końcowego obrazu.

Zawartość wynikowego pliku JAR po zbudowaniu przy użyciu Mavena z włączonym warstwowaniem będzie wyglądać następująco:

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

Dane wyjściowe pokazują dodatkowy plik JAR o nazwie spring-boot-jarmode-layertoolsи layersfle.idxplik. Ten dodatkowy plik JAR zapewnia możliwości przetwarzania warstwowego, jak opisano w następnej sekcji.

Wyodrębnianie zależności na poszczególnych warstwach

Aby wyświetlić i wyodrębnić warstwy z naszego warstwowego pliku JAR, używamy właściwości systemowej -Djarmode=layertoolsNa początek spring-boot-jarmode-layertoolsJAR zamiast aplikacji:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar

Uruchomienie tego polecenia generuje dane wyjściowe zawierające dostępne opcje polecenia:

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

Dane wyjściowe pokazują polecenia listextractи helpс helpbyć domyślnym. Uruchommy polecenie za pomocą listopcja:

java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
dependencies
spring-boot-loader
snapshot-dependencies
application

Widzimy listę zależności, które można dodać jako warstwy.

Domyślne warstwy:

Nazwa warstwy

Zawartość

dependencies

dowolna zależność, której wersja nie zawiera SNAPSHOT

spring-boot-loader

Klasy modułu ładującego JAR

snapshot-dependencies

dowolna zależność, której wersja zawiera SNAPSHOT

application

klasy aplikacji i zasoby

Warstwy są zdefiniowane w layers.idxw kolejności, w jakiej powinny być dodawane do obrazu Dockera. Warstwy te są buforowane na hoście po pierwszym pobraniu, ponieważ się nie zmieniają. Na host pobierana jest tylko zaktualizowana warstwa aplikacji, która jest szybsza ze względu na zmniejszony rozmiar .

Budowanie obrazu z zależnościami wyodrębnionymi w osobnych warstwach

Ostateczny obraz zbudujemy w dwóch etapach, stosując metodę tzw montaż wieloetapowy . W pierwszym kroku wyodrębnimy zależności, a w drugim skopiujemy wyodrębnione zależności do końcowego obrazu.

Zmodyfikujmy nasz plik Dockerfile, aby uzyskać wieloetapową kompilację:

# 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"]

Zapisujemy tę konfigurację w osobnym pliku - Dockerfile2.

Budujemy obraz Dockera za pomocą polecenia:

docker build -f Dockerfile2 -t usersignup:v1 .

Po uruchomieniu tego polecenia otrzymamy następujące dane wyjściowe:

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

Widzimy, że obraz Dockera jest tworzony z identyfikatorem obrazu, a następnie tagowany.

Na koniec, jak poprzednio, uruchamiamy polecenie Dive, aby sprawdzić warstwy wewnątrz wygenerowanego obrazu Dockera. Możemy podać identyfikator obrazu lub znacznik jako dane wejściowe polecenia Dive:

dive userssignup:v1

Jak widać na wynikach, warstwa zawierająca aplikację ma teraz tylko 11 KB, a zależności są buforowane w oddzielnych warstwach. 

Tworzenie zoptymalizowanych obrazów platformy Docker dla aplikacji Spring Boot

Wyodrębnianie zależności wewnętrznych na poszczególnych warstwach

Możemy jeszcze bardziej zmniejszyć rozmiar warstwy aplikacji, wyodrębniając dowolne z naszych niestandardowych zależności do osobnej warstwy, zamiast pakować je w aplikację, deklarując je w ymlpodobny plik o nazwie 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/"

W tym pliku layers.idxdodaliśmy niestandardową zależność o nazwie, io.myorgzawierające zależności organizacyjne pobrane ze wspólnego repozytorium.

Wniosek

W tym artykule przyjrzeliśmy się używaniu pakietów kompilacji natywnych dla chmury do tworzenia obrazu kontenera bezpośrednio z kodu źródłowego. Jest to alternatywa dla używania Dockera do tworzenia obrazu kontenera w zwykły sposób: najpierw utwórz gruby wykonywalny plik JAR, a następnie spakuj go do obrazu kontenera, określając instrukcje w pliku Dockera.

Przyjrzeliśmy się także optymalizacji naszego kontenera, włączając funkcję warstw, która ściąga zależności do oddzielnych warstw buforowanych na hoście, a cienka warstwa aplikacji jest ładowana w czasie planowania w silnikach wykonawczych kontenera.

Cały kod źródłowy użyty w artykule można znaleźć pod adresem Github .

Odniesienie do poleceń

Oto krótki przegląd poleceń, których użyliśmy w tym artykule.

Czyszczenie kontekstu:

docker system prune -a

Tworzenie obrazu kontenera przy użyciu pliku Docker:

docker build -f <Docker file name> -t <tag> .

Budujemy obraz kontenera z kodu źródłowego (bez Dockerfile):

mvn spring-boot:build-image

Wyświetl warstwy zależności. Przed zbudowaniem pliku JAR aplikacji upewnij się, że funkcja warstw jest włączona we wtyczce Spring-boot-maven:

java -Djarmode=layertools -jar application.jar list

Wyodrębnianie warstw zależności. Przed zbudowaniem pliku JAR aplikacji upewnij się, że funkcja warstw jest włączona we wtyczce Spring-boot-maven:

 java -Djarmode=layertools -jar application.jar extract

Wyświetl listę obrazów kontenerów

docker images

Widok po lewej stronie obrazu kontenera (upewnij się, że narzędzie do nurkowania jest zainstalowane):

dive <image ID or image tag>

Źródło: www.habr.com