Kontenery stały się preferowanym środkiem pakowania aplikacji ze wszystkimi zależnościami oprogramowania i systemu operacyjnego, a następnie dostarczania ich do różnych środowisk.
W tym artykule omówiono różne metody konteneryzacji aplikacji Spring Boot:
- tworzenie obrazu Dockera przy użyciu pliku Docker,
- tworzenie obrazu OCI z kodu źródłowego za pomocą Cloud-Native Buildpack,
- oraz optymalizacja obrazu w czasie rzeczywistym poprzez dzielenie części JAR na różne warstwy z użyciem narzędzi wielowarstwowych.
Przykład kodu
Artykuł ten jest wspierany przykładem działającego kodu .
Terminologia kontenerów
Zaczniemy od terminologii kontenerów używanej w artykule:
- Obraz kontenera (Container image): plik określonego formatu. Przekształcamy naszą aplikację w obraz kontenera, uruchamiając narzędzie do budowy.
- Kontener: wykonywalna instancja obrazu kontenera.
- Silnik kontenera (Container engine): proces demon, odpowiedzialny za uruchamianie kontenera.
- Hosten kontenera (Container host): komputer gospodarza, na którym działa silnik kontenera.
- Rejestr kontenerów (Container registry): ogólna lokalizacja używana do publikacji i rozpowszechniania obrazu kontenera.
- Standard OCI: — to lekka, otwarta struktura zarządzania, utworzona w ramach Linux Foundation. Specyfikacja obrazów OCI określa branżowe standardy dla formatów obrazów kontenerów oraz środowisk uruchomieniowych, aby zapewnić, że wszystkie silniki kontenerów mogą uruchamiać obrazy kontenerów stworzone przez dowolne narzędzie do budowy.
Aby umieścić aplikację w kontenerze, otaczamy naszą aplikację obrazem kontenera i publikujemy ten obraz w ogólnym rejestrze. Środowisko uruchomieniowe kontenera pobiera ten obraz z rejestru, rozpakowuje go i uruchamia aplikację wewnątrz niego.
Wersja 2.3 Spring Boot oferuje wtyczki do tworzenia obrazów OCI.
— najczęściej używana implementacja kontenera, a w naszych przykładach używamy Dockera, dlatego wszystkie późniejsze odniesienia do kontenera w tym artykule będą odnosić się do Dockera.
Tworzenie obrazu kontenera tradycyjnym sposobem
Tworzenie obrazów Dockera dla aplikacji Spring Boot jest bardzo proste, wystarczy dodać kilka instrukcji do pliku Docker.
Na początku tworzymy plik wykonywalny JAR, a jako część instrukcji pliku Docker kopiujemy plik wykonywalny JAR na obraz bazowy JRE po zastosowaniu niezbędnych ustawień.
Stwórzmy naszą aplikację Spring na z zależnościami web, lomboki actuator. Dodamy również kontroler rest, aby udostępnić API z i login/hasło: admin/admin.metodą.
Tworzenie pliku Docker
Następnie umieszczamy tę aplikację w kontenerze, 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 bazowy, z adoptopenjdk, na który kopiujemy nasz plik JAR, a następnie otwieramy port, 8080który będzie nasłuchiwał zapytań.
Budowanie aplikacji
Na początku musimy stworzyć aplikację za pomocą Mavena lub Gradle. Tutaj używamy Mavena:
mvn clean packageTo tworzy wykonywalny plik JAR aplikacji. Musimy przekształcić ten plik JAR w obraz Dockera do pracy w silniku Docker.
Tworzenie obrazu kontenera
Następnie umieszczamy ten plik wykonywalny JAR w obrazie Docker, wykonując polecenie docker buildz katalogu głównego projektu, który zawiera wcześniej stworzony plik Docker:
docker build -t usersignup:v1 .Możemy zobaczyć nasz obraz na liście za pomocą polecenia:
docker images Wynik wykonania powyższego polecenia zawiera nasz obraz usersignuprazem z obrazem bazowym, adoptopenjdk, wskazanym w naszym pliku Docker.
REPOSITORY TAG SIZE
usersignup v1 249MB
adoptopenjdk 11-jre-hotspot 229MBPodgląd warstw wewnątrz obrazu kontenera
Zobaczmy stos warstw wewnątrz obrazu. Użyjemy aby zobaczyć te warstwy:
dive usersignup:v1Oto część wyników wykonania polecenia Dive:

Jak widać, poziom aplikacyjny stanowi znaczną część rozmiaru obrazu. Chcemy zmniejszyć rozmiar tej warstwy w następnych sekcjach w ramach naszej optymalizacji.
Tworzenie obrazu kontenera za pomocą Buildpack
() to ogólny termin używany w różnych propozycjach «Platforma jako usługa» (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 inne.

Zaleta korzystania z pakietów budujących w chmurze
Jedną z głównych zalet korzystania z Buildpack do tworzenia obrazów jest to, że Zmiany w konfiguracji obrazu można zarządzać centralnie (builder) i rozpowszechniać na wszystkie aplikacje, które korzystają z buildera.
Pakiety budowanie były ściśle związane z platformą. Cloud-Native Buildpacks zapewniają standaryzację między platformami, wspierając format obrazu OCI, który gwarantuje, że obraz może być uruchamiany przez silnik Docker.
Użycie wtyczki Spring Boot
Wtyczka Spring Boot tworzy obrazy OCI z kodu źródłowego za pomocą Buildpack. Obrazy są tworzony za pomocą bootBuildImagezadań (Gradle) lub spring-boot:build-imagecelów (Maven) oraz lokalnej instalacji Docker.
Możemy skonfigurować nazwę obrazu, który ma być wysłany do rejestru Docker, podając nazwę w image tag:
org.springframework.boot
spring-boot-maven-plugin
docker.io/pratikdas/${project.artifactId}:v1Zastosujmy Maven do wykonania build-imagecelu do zbudowania aplikacji i stworzenia obrazu kontenera. Obecnie nie korzystamy z żadnych plików Docker.
mvn spring-boot:build-imageWynik będzie mniej więcej taki:
[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Budowanie obrazu 'docker.io/pratikdas/usersignup:v1'
[INFO]
[INFO] > Pobieranie obrazu budującego 'gcr.io/paketo-buildpacks/builder:base-platform-api-0.3' 0%
.
.
.. [creator] Dodawanie etykiety 'org.springframework.boot.version'
.. [creator] *** Obrazy (c311fe74ec73):
.. [creator] docker.io/pratikdas/usersignup:v1
[INFO]
[INFO] Sukces: zbudowano obraz 'docker.io/pratikdas/usersignup:v1'Z wyjściowych danych widzimy, że paketo Cloud-Native buildpackjest używany do tworzenia działającego obrazu OCI. Jak wcześniej, możemy zobaczyć obraz wskazany jako obraz Docker, wykonując polecenie:
docker images Wynik:
REPOSITORY SIZE
paketobuildpacks/run 84.3MB
gcr.io/paketo-buildpacks/builder 652MB
pratikdas/usersignup 257MBTworzenie obrazu kontenera za pomocą Jib
Jib to wtyczka do tworzenia obrazów od Google, która zapewnia alternatywną metodę tworzenia obrazu kontenera z kodu źródłowego.
Konfigurujemy jib-maven-pluginw pom.xml:
com.google.cloud.tools
jib-maven-plugin
2.5.2Następnie uruchamiamy wtyczkę Jib za pomocą polecenia Maven, aby zbudować aplikację i stworzyć obraz kontenera. Jak wcześniej, tutaj nie używamy żadnych plików Docker:
mvn compile jib:build -Dimage=/usersignup:v1Po wykonaniu powyższego polecenia Maven otrzymujemy następujący wynik:
[INFO] Konteneryzacja aplikacji do pratikdas/usersignup:v1...
.
.
[INFO] Punkt wejścia kontenera ustawiony na [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO]
[INFO] Zbudowano i wypchnięto obraz jako pratikdas/usersignup:v1
[INFO] Wykonywanie zadań:
[INFO] [==============================] 100,0% ukończonoWyniki pokazują, że obraz kontenera został utworzony i umieszczony w rejestrze.
Motywacje i metody optymalizacji obrazów
Mamy dwa główne powody do optymalizacji:
- Wydajność: w systemie orkiestracji kontenerów obraz kontenera jest pobierany z rejestru obrazów na hoście, na którym działa mechanizm kontenera. Proces ten nazywa się planowaniem. Pobieranie dużych obrazów z rejestru prowadzi do długiego czasu planowania w systemach orkiestracji kontenerów i długiego czasu budowy w potokach CI.
- Bezpieczeństwo: duże obrazy mają także większy obszar do potencjalnych luk.
Obraz Docker składa się ze stosu warstw, z których każda reprezentuje instrukcję w naszym Dockerfile. Każda warstwa przedstawia deltę zmian niższej warstwy. Kiedy pobieramy obraz Docker z rejestru, jest on pobierany warstwami i buforowany na hoście.
Spring Boot korzysta z Formuła optymalizacji koncentruje się na izolowaniu aplikacji na osobnym poziomie od zależności Spring Framework.
Warstwa zależności, która tworzy główną część grubego pliku JAR, jest ładowana tylko raz i buforowana w systemie gospodarza.
Tylko cienka warstwa aplikacji jest pobierana podczas aktualizacji aplikacji i planowania kontenerów,
jak pokazano na tym diagramie: W kolejnych częściach przyjrzymy się, jak tworzyć te zoptymalizowane obrazy dla aplikacji Spring Boot.

Tworzenie zoptymalizowanego obrazu kontenera dla aplikacji Spring Boot za pomocą Buildpack
Spring Boot 2.3 wspiera wielowarstwowość poprzez wydzielenie części grubego pliku JAR w osobne warstwy. Funkcjonalność nakładania warstw jest domyślnie wyłączona i musi być wyraźnie włączona za pomocą wtyczki Spring Boot Maven:
Spring Boot 2.3 wspiera wiele warstw poprzez wydzielenie części grubego pliku JAR w osobne warstwy. Funkcja nakładania warstw domyślnie jest wyłączona i należy ją jawnie włączyć za pomocą wtyczki Spring Boot Maven:
org.springframework.boot
spring-boot-maven-plugin
trueBędziemy używać tej konfiguracji do tworzenia naszego obrazu kontenera, najpierw za pomocą Buildpack, a następnie za pomocą Dockera w następnych rozdziałach.
Uruchommy build-imagecel Maven do stworzenia obrazu kontenera:
mvn spring-boot:build-imageJeśli uruchomimy Dive, aby zobaczyć warstwy w otrzymanym obrazie, zobaczymy, że warstwa aplikacji (podkreślona na czerwono) jest znacznie mniejsza w zakresie kilobajtów w porównaniu do tego, co uzyskaliśmy przy użyciu grubego formatu JAR:

Tworzenie zoptymalizowanego obrazu kontenera dla aplikacji Spring Boot za pomocą Dockera
Zamiast używać wtyczki Maven lub Gradle, możemy również stworzyć wielowarstwowy obraz JAR Dockera z plikiem Docker.
Kiedy używamy Dockera, musimy wykonać dwa dodatkowe kroki, aby wydobyć warstwy i skopiować je do ostatecznego obrazu.
Zawartość otrzymanego JAR po zbudowaniu za pomocą Maven z włączoną funkcją warstwowania 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.idxW wyjściu wyświetla się dodatkowy JAR o nazwie spring-boot-jarmode-layertoolsi layersfle.idxplik. Ten dodatkowy plik JAR umożliwia przetwarzanie wielowarstwowe, jak opisano w następnym rozdziale.
Ekstrakcja zależności w oddzielnych warstwach
Aby przeglądać i wydobywać warstwy z naszego wielowarstwowego JAR, używamy właściwości systemu -Djarmode=layertoolsdo uruchomienia spring-boot-jarmode-layertoolsJAR zamiast aplikacji:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jarWykonanie tego polecenia zwraca wynik zawierający dostępne opcje poleceń:
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 commandWynik pokazuje polecenia list, extracti helpz helpbyć domyślnie. Uruchommy polecenie z listopcją:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar listdependencies
spring-boot-loader
snapshot-dependencies
applicationWidzimy listę zależności, które można dodać jako warstwy.
Warstwy domyślnie:
Nazwa warstwy
Spis treści
dependencies
dowolna zależność, której wersja nie zawiera SNAPSHOT
spring-boot-loader
Klasy ładowarki JAR
snapshot-dependencies
dowolna zależność, której wersja zawiera SNAPSHOT
application
klasy aplikacji i zasoby
Warstwy zdefiniowane w layers.idxpliku w takiej kolejności, w jakiej mają być dodawane do obrazu Docker. Te warstwy są buforowane na hoście po pierwszym pobraniu, ponieważ się nie zmieniają. Na hosta ładowana jest tylko zaktualizowana warstwa aplikacji, co przebiega szybciej z powodu zmniejszonego rozmiaru. .
Budowanie obrazu z zależnościami pobranymi w osobnych warstwach.
Zbudujemy finalny obraz w dwóch etapach, korzystając z metody zwanej W pierwszym etapie pobierzemy zależności, a w drugim etapie skopiujemy pobrane zależności do ostatecznego obrazu.
Dostosujmy nasz plik Docker do wielostopniowego budowania:
# 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"]Zachowujemy tę konfigurację w osobnym pliku — Dockerfile2.
Budujemy obraz Docker za pomocą polecenia:
docker build -f Dockerfile2 -t usersignup:v1 .Po wykonaniu tego polecenia otrzymujemy taki wynik:
Wysyłanie kontekstu budowy do demona Docker 20.41MB
Krok 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Pobieranie z library/adoptopenjdk
.
.
Pomyslnie zbudowano a9ebf6970841
Pomyslnie oznaczono userssignup:v1Widzimy, że obraz Docker jest tworzony z identyfikatorem obrazu, a następnie jest oznaczany.
Na koniec uruchamiamy polecenie Dive, jak wcześniej, aby sprawdzić warstwy wewnątrz wygenerowanego obrazu Docker. Możemy wskazać identyfikator obrazu lub tag jako dane wejściowe dla polecenia Dive:
dive userssignup:v1Jak widać z wyników, warstwa zawierająca aplikację teraz zajmuje tylko 11 KB, a zależności są buforowane w osobnych warstwach.

Pobieranie wewnętrznych zależności w osobnych warstwach.
Możemy dodatkowo zmniejszyć rozmiar warstwy aplikacji, pobierając wszelkie nasze zależności użytkownika w osobnej warstwie zamiast pakowania ich razem z aplikacją, deklarując je w ymlpodobnym pliku 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 dostosowaną zależność o nazwie, io.myorgzawierającą zależności organizacji pobrane z ogólnego repozytorium.
Wnioski
W tym artykule omówiliśmy wykorzystanie Cloud-Native Buildpacks do tworzenia obrazu kontenera bezpośrednio z kodu źródłowego. To alternatywa dla używania Dockera do tradycyjnego tworzenia obrazu kontenera: najpierw tworzony jest gruby plik wykonywalny JAR, a następnie pakowany do obrazu kontenera za pomocą instrukcji w pliku Docker.
Omawialiśmy również optymalizację naszego kontenera, włączając funkcję nakładania, która wydziela zależności do oddzielnych warstw, które są buforowane na hoście, a cienka warstwa aplikacji jest ładowana podczas planowania w mechanizmach wykonawczych kontenera.
Możesz znaleźć cały kod źródłowy użyty w artykule na .
Podręcznik poleceń
Oto krótki przegląd poleceń, które użyliśmy w tym artykule do szybkiego zapoznania się.
Oczyszczenie kontekstu:
docker system prune -aTworzenie obrazu kontenera za pomocą pliku Docker:
docker build -f -t .Budowanie obrazu kontenera z kodu źródłowego (bez Dockerfile):
mvn spring-boot:build-imagePodgląd warstw zależności. Przed budową pliku JAR aplikacji upewnij się, że funkcja nakładania jest włączona w spring-boot-maven-plugin:
java -Djarmode=layertools -jar application.jar listWydobywanie warstw zależności. Przed budową pliku JAR aplikacji upewnij się, że funkcja nakładania jest włączona w spring-boot-maven-plugin:
java -Djarmode=layertools -jar application.jar extractPodgląd listy obrazów kontenerów
docker imagesPodgląd warstw wewnątrz obrazu kontenera (upewnij się, że narzędzie do nurkowania jest zainstalowane):
diveŹródło: habr.com
