Crearea de imagini Docker optimizate pentru o aplicație Spring Boot

Containerele au devenit mijlocul preferat de a împacheta o aplicație cu toate dependențele sale de software și sisteme de operare și apoi de a le livra în diferite medii.

Acest articol acoperă diferite moduri de a containeriza o aplicație Spring Boot:

  • crearea unei imagini Docker folosind un fișier Docker,
  • crearea unei imagini OCI din sursă folosind Cloud-Native Buildpack,
  • și optimizarea imaginii în timpul execuției prin separarea părților JAR în straturi diferite folosind instrumente cu mai multe niveluri.

 Cod simplu

Acest articol este însoțit de un exemplu de cod de lucru pe GitHub .

Terminologia containerelor

Vom începe cu terminologia containerului folosită în articol:

  • Imaginea containerului: fișier cu un anumit format. Vom converti aplicația noastră într-o imagine de container rulând instrumentul de compilare.
  • Recipient: o instanță executabilă a imaginii containerului.
  • Motor container: Procesul demon responsabil pentru rularea containerului.
  • Gazdă container: computerul gazdă pe care rulează motorul containerului.
  • Registrul containerelor: Locația generală folosită pentru publicarea și distribuirea imaginii containerului.
  • Standardul OCIInițiativa Open Container (OCI) este o structură de guvernanță ușoară, deschisă, formată în cadrul Fundației Linux. Specificația de imagine OCI definește standardele din industrie pentru imaginea containerului și formatele de rulare pentru a se asigura că toate motoarele de containere pot rula imagini de container create de orice instrument de construire.

Pentru a containeriza o aplicație, împachetăm aplicația noastră într-o imagine container și publicăm acea imagine într-un registru partajat. Durata de rulare a containerului preia această imagine din registry, o despachetează și rulează aplicația în interiorul acesteia.

Versiunea 2.3 a Spring Boot oferă pluginuri pentru crearea de imagini OCI.

Docher este implementarea containerului cel mai frecvent utilizată și folosim Docker în exemplele noastre, astfel încât toate referințele ulterioare ale containerului din acest articol se vor referi la Docker.

Construirea unei imagini de container în mod tradițional

Crearea imaginilor Docker pentru aplicațiile Spring Boot este foarte ușoară, adăugând câteva instrucțiuni în fișierul Docker.

Mai întâi creăm un fișier JAR executabil și, ca parte a instrucțiunilor fișierului Docker, copiem fișierul JAR executabil deasupra imaginii JRE de bază după aplicarea setărilor necesare.

Să creăm aplicația noastră Spring pe Spring Initializr cu dependențe weblombokи actuator. De asemenea, adăugăm un controler de odihnă pentru a oferi un API GETmetodă.

Crearea unui fișier Docker

Apoi containerizam această aplicație prin adăugare Dockerfile:

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

Fișierul nostru Docker conține o imagine de bază din adoptopenjdk, deasupra căruia copiem fișierul nostru JAR și apoi deschidem portul, 8080care va asculta cererile.

Construirea aplicației

Mai întâi trebuie să creați o aplicație folosind Maven sau Gradle. Aici folosim Maven:

mvn clean package

Aceasta creează un fișier JAR executabil pentru aplicație. Trebuie să convertim acest JAR executabil într-o imagine Docker pentru a rula pe motorul Docker.

Crearea unei imagini de container

Apoi punem acest fișier JAR executabil în imaginea Docker executând comanda docker builddin directorul rădăcină al proiectului care conține fișierul Dockerfile creat mai devreme:

docker build  -t usersignup:v1 .

Putem vedea imaginea noastră în listă folosind comanda:

docker images 

Ieșirea comenzii de mai sus include imaginea noastră usersignupîmpreună cu imaginea de bază, adoptopenjdkspecificat în fișierul nostru Docker.

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

Vizualizați straturi în interiorul unei imagini de container

Să ne uităm la teancul de straturi din interiorul imaginii. Noi vom folosi instrument  picaj pentru a vizualiza aceste straturi:

dive usersignup:v1

Iată o parte din rezultatul comenzii Dive: 

Crearea de imagini Docker optimizate pentru o aplicație Spring Boot

După cum putem vedea, stratul de aplicație reprezintă o parte semnificativă din dimensiunea imaginii. Dorim să reducem dimensiunea acestui strat în secțiunile următoare, ca parte a optimizării noastre.

Crearea unei imagini de container folosind Buildpack

Pachete de asamblare (Pachete de construcție) este un termen general utilizat de diverse oferte Platform as a Service (PAAS) pentru a crea o imagine container din codul sursă. A fost lansat de Heroku în 2011 și de atunci a fost adoptat de Cloud Foundry, Google App Engine, Gitlab, Knative și alte câteva.

Crearea de imagini Docker optimizate pentru o aplicație Spring Boot

Avantajul pachetelor cloud build

Unul dintre principalele beneficii ale utilizării Buildpack pentru a crea imagini este că Modificările configurației imaginii pot fi gestionate central (builder) și propagate la toate aplicațiile folosind builder.

Pachetele de compilare au fost strâns cuplate la platformă. Cloud-Native Buildpacks oferă standardizare între platforme prin sprijinirea formatului de imagine OCI, care asigură că imaginea poate fi rulată de motorul Docker.

Folosind pluginul Spring Boot

Pluginul Spring Boot construiește imagini OCI din sursă folosind Buildpack. Imaginile sunt create folosind bootBuildImagesarcini (Gradle) sau spring-boot:build-imageținte (Maven) și instalarea locală a Docker.

Putem personaliza numele imaginii necesare pentru a trimite în registrul Docker specificând numele în 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>

Să folosim Maven pentru a face asta build-imageobiective pentru crearea unei aplicații și crearea unei imagini container. Nu folosim niciun Dockerfile în acest moment.

mvn spring-boot:build-image

Rezultatul va fi cam așa:

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

Din rezultat vedem că paketo Cloud-Native buildpackfolosit pentru a crea o imagine OCI de lucru. Ca și înainte, putem vedea imaginea listată ca imagine Docker, rulând comanda:

docker images 

Concluzie:

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

Crearea unei imagini container folosind Jib

Jib este un plugin de creare de imagini de la Google care oferă o metodă alternativă pentru crearea unei imagini container din codul sursă.

Configurare jib-maven-pluginîn pom.xml:

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

Apoi, rulăm pluginul Jib folosind comanda Maven pentru a construi aplicația și a crea o imagine de container. Ca și înainte, nu folosim niciun fișier Docker aici:

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

După executarea comenzii Maven de mai sus, obținem următoarea ieșire:

[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

Rezultatul arată că imaginea containerului a fost creată și plasată în registru.

Motivații și tehnici pentru crearea de imagini optimizate

Avem două motive principale pentru optimizare:

  • productivitate: Într-un sistem de orchestrare a containerului, o imagine a containerului este preluată din registrul de imagini către gazda care rulează motorul containerului. Acest proces se numește planificare. Extragerea imaginilor mari din registry are ca rezultat timpi lungi de programare în sistemele de orchestrare a containerelor și timpi lungi de construire în conductele CI.
  • Безопасность: Imaginile mai mari au și o zonă mai mare pentru vulnerabilități.

O imagine Docker constă dintr-un teanc de straturi, fiecare dintre acestea reprezentând o instrucțiune în fișierul nostru Docker. Fiecare strat reprezintă o deltă a modificărilor din stratul de bază. Când extragem o imagine Docker din registry, aceasta este extrasă în straturi și stocată în cache pe gazdă.

Utilizări Spring Boot „BORCAN pentru grăsime” în ca format implicit de ambalare. Când ne uităm la JAR-ul gros, vedem că aplicația reprezintă o parte foarte mică din întregul JAR. Aceasta este partea care se schimbă cel mai des. Restul constă din dependențele Spring Framework.

Formula de optimizare se concentrează pe izolarea aplicației la un nivel separat de dependențele Spring Framework.

Stratul de dependență, care formează cea mai mare parte a fișierului JAR gros, este descărcat o singură dată și stocat în cache pe sistemul gazdă.

Doar un strat subțire al aplicației este extras în timpul actualizărilor aplicației și al programării containerelor. așa cum se arată în această diagramă:

Crearea de imagini Docker optimizate pentru o aplicație Spring Boot

În secțiunile următoare, vom analiza cum să creați aceste imagini optimizate pentru o aplicație Spring Boot.

Crearea unei imagini de container optimizate pentru o aplicație Spring Boot folosind Buildpack

Spring Boot 2.3 acceptă stratificarea prin extragerea unor părți dintr-un fișier JAR gros în straturi separate. Funcția de stratificare este dezactivată în mod implicit și trebuie să fie activată în mod explicit folosind pluginul Spring Boot Maven:

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

Vom folosi această configurație pentru a construi mai întâi imaginea containerului nostru cu Buildpack și apoi cu Docker în secțiunile următoare.

Hai să lansăm build-imageȚinta Maven pentru crearea imaginii containerului:

mvn spring-boot:build-image

Dacă rulăm Dive pentru a vedea straturile din imaginea rezultată, putem vedea că stratul de aplicație (subliniat cu roșu) este mult mai mic în intervalul de kiloocteți în comparație cu ceea ce am obținut folosind formatul JAR gros:

Crearea de imagini Docker optimizate pentru o aplicație Spring Boot

Crearea unei imagini de container optimizate pentru o aplicație Spring Boot folosind Docker

În loc să folosim un plugin Maven sau Gradle, putem crea și o imagine Docker JAR stratificată cu un fișier Docker.

Când folosim Docker, trebuie să facem doi pași suplimentari pentru a extrage straturile și a le copia în imaginea finală.

Conținutul JAR rezultat după construirea folosind Maven cu stratificarea activată va arăta astfel:

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

Ieșirea arată un JAR suplimentar numit spring-boot-jarmode-layertoolsи layersfle.idxfişier. Acest fișier JAR suplimentar oferă capabilități de procesare stratificată, așa cum este descris în secțiunea următoare.

Extragerea dependențelor de pe straturi individuale

Pentru a vizualiza și extrage straturi din JAR-ul nostru stratificat, folosim proprietatea sistemului -Djarmode=layertoolspentru început spring-boot-jarmode-layertoolsJAR în loc de aplicare:

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

Rularea acestei comenzi produce rezultate care conțin opțiunile de comandă disponibile:

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

Ieșirea arată comenzile listextractи helpс helpfi implicit. Să rulăm comanda cu listopțiune:

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

Vedem o listă de dependențe care pot fi adăugate ca straturi.

Straturi implicite:

Numele stratului

Conținut

dependencies

orice dependență a cărei versiune nu conține SNAPSHOT

spring-boot-loader

Clasele de încărcare JAR

snapshot-dependencies

orice dependență a cărei versiune conține SNAPSHOT

application

clase de aplicații și resurse

Straturile sunt definite în layers.idxfișierul în ordinea în care ar trebui să fie adăugat la imaginea Docker. Aceste straturi sunt stocate în cache în gazdă după prima recuperare, deoarece nu se schimbă. Doar stratul de aplicație actualizat este descărcat pe gazdă, ceea ce este mai rapid datorită dimensiunii reduse .

Construirea unei imagini cu dependențe extrase în straturi separate

Vom construi imaginea finală în două etape folosind o metodă numită asamblare în mai multe etape . În primul pas vom extrage dependențele și în al doilea pas vom copia dependențele extrase în imaginea finală.

Să modificăm fișierul Docker pentru o construcție în mai multe etape:

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

Salvăm această configurație într-un fișier separat - Dockerfile2.

Construim imaginea Docker folosind comanda:

docker build -f Dockerfile2 -t usersignup:v1 .

După rularea acestei comenzi, obținem următoarea ieșire:

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

Putem vedea că o imagine Docker este creată cu un ID de imagine și apoi etichetată.

În cele din urmă, rulăm comanda Dive ca înainte pentru a inspecta straturile din interiorul imaginii Docker generate. Putem furniza un ID de imagine sau o etichetă ca intrare la comanda Dive:

dive userssignup:v1

După cum puteți vedea în rezultat, stratul care conține aplicația are acum doar 11 KB, iar dependențele sunt stocate în cache în straturi separate. 

Crearea de imagini Docker optimizate pentru o aplicație Spring Boot

Extragerea dependențelor interne pe straturi individuale

Putem reduce și mai mult dimensiunea nivelului de aplicație prin extragerea oricăreia dintre dependențele noastre personalizate într-un nivel separat, în loc să le împachetăm cu aplicația declarându-le în ymlfișier similar numit 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/"

În acest dosar layers.idxam adăugat o dependență personalizată numită, io.myorgcare conțin dependențe de organizație preluate dintr-un depozit partajat.

Producție

În acest articol, ne-am uitat la utilizarea Cloud-Native Buildpacks pentru a construi o imagine de container direct din codul sursă. Aceasta este o alternativă la utilizarea Docker pentru a crea o imagine de container în mod obișnuit: mai întâi creând un fișier JAR executabil gros și apoi împachetați-l într-o imagine de container prin specificarea instrucțiunilor în fișierul Docker.

De asemenea, ne-am uitat la optimizarea containerului nostru prin activarea unei funcții de stratificare care trage dependențele în straturi separate care sunt stocate în cache pe gazdă și un strat subțire al aplicației este încărcat la momentul programării în motoarele de execuție ale containerului.

Puteți găsi tot codul sursă folosit în articol la Github .

Referință de comandă

Iată o scurtă descriere a comenzilor pe care le-am folosit în acest articol.

Curățarea contextului:

docker system prune -a

Crearea unei imagini de container folosind un fișier Docker:

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

Construim imaginea containerului din codul sursă (fără Dockerfile):

mvn spring-boot:build-image

Vizualizați straturile de dependență. Înainte de a construi fișierul JAR al aplicației, asigurați-vă că funcția de stratificare este activată în spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Extragerea straturilor de dependență. Înainte de a construi fișierul JAR al aplicației, asigurați-vă că funcția de stratificare este activată în spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Vizualizați o listă de imagini ale containerului

docker images

Vedeți din stânga în interiorul imaginii containerului (asigurați-vă că instrumentul de scufundare este instalat):

dive <image ID or image tag>

Sursa: www.habr.com