Opbygning af optimerede Docker-billeder til en Spring Boot-applikation

Containere er blevet det foretrukne middel til at pakke en applikation med alle dens software- og operativsystemafhængigheder og derefter levere dem til forskellige miljøer.

Denne artikel dækker forskellige måder at beholde en Spring Boot-applikation på:

  • opbygning af et docker-billede ved hjælp af en dockerfil,
  • opbygning af et OCI-billede fra kilde ved hjælp af Cloud-Native Buildpack,
  • og billedoptimering under kørsel ved at adskille JAR-dele i forskellige niveauer ved hjælp af lagdelte værktøjer.

 Kode eksempel

Denne artikel er ledsaget af et eksempel på en arbejdskode på GitHub .

Container terminologi

Vi starter med containerterminologien, der bruges gennem artiklen:

  • Containerbillede: en fil i et bestemt format. Vi konverterer vores applikation til et containerbillede ved at køre byggeværktøjet.
  • container: En eksekverbar forekomst af containerbilledet.
  • Container motor: Dæmonprocessen, der er ansvarlig for at køre containeren.
  • Container vært: Værtsmaskinen, som containermotoren kører på.
  • Containerregistrering: Den generelle placering, der bruges til at publicere og distribuere containerbilledet.
  • OCI standardOpen Container Initiative (OCI) er en let, open source-administrationsramme dannet af Linux Foundation. OCI-billedspecifikationen definerer industristandarder for containerbilledformater og kørselstiden for at sikre, at alle containermotorer kan køre containerbilleder, der er oprettet af ethvert byggeværktøj.

For at containerisere en applikation pakker vi vores applikation ind i et containerbillede og offentliggør dette billede til det offentlige register. Containerruntiden henter dette billede fra registreringsdatabasen, pakker det ud og kører programmet inde i det.

Version 2.3 af Spring Boot giver plugins til opbygning af OCI-billeder.

Docker er den mest brugte containerimplementering, og vi bruger Docker i vores eksempler, så alle efterfølgende containerreferencer i denne artikel vil referere til Docker.

Opbygning af et containerbillede på traditionel vis

Det er meget nemt at bygge Docker-billeder til Spring Boot-applikationer ved at tilføje nogle få instruktioner til din Dockerfile.

Vi opretter først en eksekverbar JAR, og som en del af Dockerfile-instruktionerne kopierer vi den eksekverbare JAR oven på basis-JRE-billedet efter at have anvendt de nødvendige tilpasninger.

Lad os oprette vores forårsapplikation på Spring Initializr med afhængigheder weblombokи actuator. Vi tilføjer også en hvile-controller til at give en API med GETmetode.

Oprettelse af en Dockerfile

Vi placerer derefter denne applikation i en beholder ved at tilføje Dockerfile:

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

Vores Dockerfile indeholder et basisbillede, fra adoptopenjdk, hvorpå vi kopierer vores JAR-fil og åbner derefter porten, 8080som vil lytte efter anmodninger.

Applikationsmontage

Først skal du oprette en applikation ved hjælp af Maven eller Gradle. Her bruger vi Maven:

mvn clean package

Dette opretter en eksekverbar JAR-fil til applikationen. Vi skal konvertere denne eksekverbare JAR til et Docker-billede for at køre på Docker-motoren.

Opret et containerbillede

Vi sætter derefter denne JAR-eksekverbare i Docker-billedet ved at køre kommandoen docker buildfra rodmappen på projektet, der indeholder den tidligere oprettede Dockerfil:

docker build  -t usersignup:v1 .

Vi kan se vores billede på listen med kommandoen:

docker images 

Outputtet af ovenstående kommando inkluderer vores billede usersignupsammen med basisbilledet, adoptopenjdkspecificeret i vores Dockerfile.

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

Se lag inde i et containerbillede

Lad os se på stakken af ​​lag inde i billedet. Vi vil bruge инструмент  dykke, for at se disse lag:

dive usersignup:v1

Her er en del af outputtet fra Dive-kommandoen: 

Opbygning af optimerede Docker-billeder til en Spring Boot-applikation

Som vi kan se, udgør applikationslaget en betydelig del af billedstørrelsen. Vi ønsker at reducere størrelsen af ​​dette lag i de følgende afsnit som en del af vores optimering.

Opbygning af et containerbillede med Buildpack

Samle pakker (Buildpacks) er et generisk udtryk, der bruges af forskellige Platform as a Service-tilbud (PAAS) til at skabe et containerbillede fra kildekoden. Det blev lanceret af Heroku i 2011 og er siden blevet adopteret af Cloud Foundry, Google App Engine, Gitlab, Knative og et par andre.

Opbygning af optimerede Docker-billeder til en Spring Boot-applikation

Fordel ved Cloud Build-pakker

En af de vigtigste fordele ved at bruge Buildpack til at bygge billeder er, at billedkonfigurationsændringer kan administreres centralt (builder) og udbredes til alle applikationer ved hjælp af builder.

Byggepakkerne var tæt knyttet til platformen. Cloud-Native Buildpacks giver standardisering på tværs af platforme ved at understøtte OCI-billedformatet, som sikrer, at billedet kan køres af Docker-motoren.

Brug af Spring Boot Plugin

Spring Boot-plugin'et bygger OCI-billeder fra kilden ved hjælp af Buildpack. Billeder er lavet vha bootBuildImageopgaver (Gradle) el spring-boot:build-imagetarget (Maven) og lokal Docker-installation.

Vi kan tilpasse navnet på det billede, vi skal skubbe til Docker-registret, ved at angive navnet i 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>

Lad os bruge Maven til at udføre build-imagemål for oprettelse af en applikation og oprettelse af et containerbillede. Vi bruger i øjeblikket ingen Dockerfiler.

mvn spring-boot:build-image

Resultatet bliver noget som dette:

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

Ud fra outputtet ser vi det paketo Cloud-Native buildpackbruges til at skabe et fungerende OCI-billede. Som før kan vi se billedet opført som et Docker-billede ved at køre kommandoen:

docker images 

Konklusion:

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

Oprettelse af et containerbillede med Jib

Jib er et billedforfatterplugin fra Google, der giver en alternativ metode til at skabe et containerbillede fra kilden.

Sætte op jib-maven-plugini pom.xml:

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

Dernæst kører vi Jib-plugin'et ved hjælp af Maven-kommandoen til at bygge applikationen og oprette containerbilledet. Som før bruger vi ingen Dockerfiler her:

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

Efter at have udført ovenstående Maven-kommando får vi følgende output:

[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

Outputtet viser, at containerbilledet er blevet oprettet og placeret i registreringsdatabasen.

Motivationer og metoder til at skabe optimerede billeder

Vi har to hovedårsager til at optimere:

  • Ydelse: I et containerorkestreringssystem trækkes containerbilledet fra billedregistret til værten, der kører containermotoren. Denne proces kaldes planlægning. At trække store billeder fra registreringsdatabasen resulterer i lange planlægningstider i containerorkestreringssystemer og lange byggetider i CI-pipelines.
  • Безопасность: store billeder har også et stort område for sårbarheder.

Et Docker-billede består af en stak af lag, der hver repræsenterer et udsagn i vores Dockerfile. Hvert lag repræsenterer deltaet af ændringer i det underliggende lag. Når vi trækker et Docker-billede fra registreringsdatabasen, trækkes det i lag og cachelagres på værten.

Spring Boot bruger "fat JAR" i som standard emballageformat. Når vi ser på en fed JAR, ser vi, at applikationen er en meget lille del af hele JAR. Det er den del, der ændrer sig mest. Resten består af Spring Framework-afhængigheder.

Optimeringsformlen er centreret omkring at isolere applikationen på et separat niveau fra Spring Framework-afhængigheder.

Afhængighedslaget, der udgør hovedparten af ​​den tykke JAR-fil, downloades kun én gang og cachelagres på værtssystemet.

Kun et tyndt lag af appen trækkes under appopdateringer og containerplanlægning, som vist i dette diagram:

Opbygning af optimerede Docker-billeder til en Spring Boot-applikation

I de følgende afsnit vil vi se på, hvordan du opretter disse optimerede billeder til en Spring Boot-applikation.

Opbygning af et optimeret containerbillede til en Spring Boot-applikation med Buildpack

Spring Boot 2.3 understøtter lagdeling ved at udtrække dele af en tyk JAR-fil i separate lag. Lagdelingsfunktionen er deaktiveret som standard og skal udtrykkeligt aktiveres ved hjælp af Spring Boot Maven-plugin:

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

Vi vil bruge denne konfiguration til at bygge vores containerbillede først med Buildpack og derefter med Docker i de følgende afsnit.

Lad os løbe build-imageMaven mål for at oprette et containerbillede:

mvn spring-boot:build-image

Hvis vi kører Dive for at se lagene i det resulterende billede, kan vi se, at applikationslaget (cirklet med rødt) er meget mindre i kilobyteområdet sammenlignet med det, vi fik ved at bruge det tykke JAR-format:

Opbygning af optimerede Docker-billeder til en Spring Boot-applikation

Opbygning af et optimeret containerbillede til en Spring Boot-applikation med Docker

I stedet for at bruge et Maven- eller Gradle-plugin, kan vi også oprette et lagdelt Docker JAR-billede med en Docker-fil.

Når vi bruger Docker, skal vi tage to ekstra trin for at udtrække lagene og kopiere dem til det endelige billede.

Indholdet af den resulterende JAR efter bygning med Maven med lagdeling aktiveret vil se sådan ud:

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

Udgangen viser en ekstra JAR navngivet spring-boot-jarmode-layertoolsи layersfle.idxfil. Denne ekstra JAR-fil giver mulighed for lagdeling, som beskrevet i næste afsnit.

Uddrag afhængigheder på separate lag

For at se og udtrække lag fra vores lagdelte JAR bruger vi systemegenskaben -Djarmode=layertoolstil start spring-boot-jarmode-layertoolsJAR i stedet for applikation:

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

Kørsel af denne kommando producerer et output, der indeholder de tilgængelige kommandomuligheder:

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

Udgangen viser kommandoerne listextractи helpс helpvære standard. Lad os køre kommandoen med listmulighed:

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

Vi ser en liste over afhængigheder, der kan tilføjes som lag.

Lag som standard:

Lagnavn

Indhold

dependencies

enhver afhængighed, hvis version ikke indeholder SNAPSHOT

spring-boot-loader

JAR-læsserklasser

snapshot-dependencies

enhver afhængighed, hvis version indeholder SNAPSHOT

application

applikationsklasser og ressourcer

Lag er defineret i layers.idxfilen i den rækkefølge, de skal tilføjes til Docker-billedet. Disse lag cachelagres på værten efter den første hentning, fordi de ikke ændres. Kun det opdaterede applikationslag downloades til værten, hvilket er hurtigere på grund af den reducerede størrelse .

Opbygning af et billede med afhængigheder udtrukket i separate lag

Vi vil bygge det endelige billede i to trin ved hjælp af en metode kaldet montering i flere trin . I det første trin vil vi udtrække afhængighederne, og i det andet trin vil vi kopiere de udpakkede afhængigheder til det endelige.

Lad os ændre vores Dockerfile til en multi-stage build:

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

Vi gemmer denne konfiguration i en separat fil - Dockerfile2.

Vi bygger Docker-billedet ved hjælp af kommandoen:

docker build -f Dockerfile2 -t usersignup:v1 .

Efter at have udført denne kommando, får vi følgende output:

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

Vi kan se, at Docker-billedet er oprettet med et billed-id og derefter tagget.

Til sidst kører vi Dive-kommandoen som før for at tjekke lagene inde i det genererede Docker-billede. Vi kan give et billed-id eller tag som input til Dive-kommandoen:

dive userssignup:v1

Som du kan se fra outputtet, er laget, der indeholder applikationen, nu kun 11 KB, og afhængighederne er cachelagret i separate lag. 

Opbygning af optimerede Docker-billeder til en Spring Boot-applikation

Uddrag interne afhængigheder på separate lag

Vi kan yderligere reducere størrelsen af ​​applikationslaget ved at udtrække enhver af vores tilpassede afhængigheder i et separat lag i stedet for at pakke dem sammen med applikationen ved at erklære dem i ymllignende fil navngivet 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/"

I denne fil layers.idxvi har tilføjet en tilpasset afhængighed ved navn, io.myorgindeholdende organisationsafhængigheder hentet fra det delte lager.

Output

I denne artikel så vi på at bruge Cloud-Native Buildpacks til at bygge et containerbillede direkte fra kilden. Dette er et alternativ til at bruge Docker til at skabe et containerbillede på den sædvanlige måde: først oprettes en tyk eksekverbar JAR-fil og pakkes derefter ind i et containerbillede ved at specificere instruktionerne i Dockerfilen.

Vi så også på at optimere vores container ved at inkludere en lagdelingsfunktion, der udtrækker afhængigheder i separate lag, der er cachelagret på værten, og et tyndt applikationslag indlæses på tidspunktet for planlægning i containerens udførelsesmotorer.

Du kan finde al kildekoden brugt i artiklen på Github .

Kommandoreference

Her er en oversigt over de kommandoer, vi brugte i denne artikel for en hurtig reference.

Kontekstrydning:

docker system prune -a

Opbygning af et containerbillede med en Dockerfile:

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

Byg containerbillede fra kilden (uden Dockerfile):

mvn spring-boot:build-image

Se afhængighedslag. Før du bygger applikationsjar-filen, skal du sørge for, at lagdelingsfunktionen er aktiveret i spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Udtræk afhængighedslag. Før du bygger applikationsjar-filen, skal du sørge for, at lagdelingsfunktionen er aktiveret i spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Visning af en liste over containerbilleder

docker images

Se til venstre inde i containerbilledet (sørg for, at dykkerværktøjet er installeret):

dive <image ID or image tag>

Kilde: www.habr.com