Geoptimaliseerde Docker-images bouwen voor een Spring Boot-applicatie

Containers zijn het voorkeursmiddel geworden om een ​​applicatie met al zijn software- en besturingssysteemafhankelijkheden te verpakken en deze vervolgens aan verschillende omgevingen te leveren.

In dit artikel worden verschillende manieren besproken om een ​​Spring Boot-applicatie in containers te plaatsen:

  • een docker-image bouwen met behulp van een dockerfile,
  • een OCI-image bouwen vanaf de bron met behulp van Cloud-Native Buildpack,
  • en runtime-beeldoptimalisatie door delen van de JAR in verschillende lagen te scheiden met behulp van tools met meerdere niveaus.

 Codevoorbeeld

Dit artikel gaat vergezeld van een werkend codevoorbeeld op GitHub .

Containerterminologie

We beginnen met de containerterminologie die in het hele artikel wordt gebruikt:

  • Containerafbeelding: een bestand met een specifiek formaat. We converteren onze applicatie naar een containerimage door de build-tool uit te voeren.
  • Bak: een uitvoerbaar exemplaar van de containerimage.
  • Containermotor: Het daemonproces dat verantwoordelijk is voor het uitvoeren van de container.
  • Containerhost: De hostmachine waarop de containermotor draait.
  • Containerregister: De algemene locatie die wordt gebruikt om de containerimage te publiceren en te distribueren.
  • OCI-standaardOpen Container-initiatief (OCI) is een lichtgewicht, open-source managementframework gevormd door de Linux Foundation. De OCI Image Specification definieert industriestandaarden voor containerimageformaten en de runtime om ervoor te zorgen dat alle containerengines containerimages kunnen uitvoeren die door elke buildtool zijn gemaakt.

Om een ​​applicatie in een container te plaatsen, verpakken we onze applicatie in een containerimage en publiceren we die image naar het openbare register. De containerruntime haalt deze afbeelding op uit het register, pakt deze uit en voert de toepassing erin uit.

Versie 2.3 van Spring Boot biedt plug-ins voor het bouwen van OCI-images.

havenarbeider is de meest gebruikte containerimplementatie en we gebruiken Docker in onze voorbeelden, dus alle volgende containerverwijzingen in dit artikel verwijzen naar Docker.

Op traditionele wijze een containerimage bouwen

Het maken van Docker-images voor Spring Boot-applicaties is heel eenvoudig door een paar instructies aan het Docker-bestand toe te voegen.

We maken eerst een uitvoerbare JAR en kopiëren, als onderdeel van de Dockerfile-instructies, de uitvoerbare JAR bovenop de basis-JRE-image nadat we de nodige aanpassingen hebben aangebracht.

Laten we onze Spring-applicatie maken Lente Initializr met afhankelijkheden weblombokи actuator. We voegen ook een rustcontroller toe om een ​​API van te voorzien GETmethode.

Een Dockerbestand maken

Vervolgens plaatsen we deze applicatie in een container door het toe te voegen Dockerfile:

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

Onze Dockerfile bevat een basisimage, van adoptopenjdk, waarop we ons JAR-bestand kopiëren en vervolgens de poort openen, 8080die naar verzoeken luistert.

Applicatie montage

Eerst moet u een applicatie maken met Maven of Gradle. Hier gebruiken we Maven:

mvn clean package

Hiermee wordt een uitvoerbaar JAR-bestand voor de toepassing gemaakt. We moeten deze uitvoerbare JAR converteren naar een Docker-image om op de Docker-engine te draaien.

Maak een containerimage

Vervolgens plaatsen we dit uitvoerbare JAR-bestand in de Docker-image door de opdracht uit te voeren docker buildvanuit de hoofdmap van het project met daarin het Dockerfile dat eerder is gemaakt:

docker build  -t usersignup:v1 .

We kunnen onze afbeelding in de lijst zien met het commando:

docker images 

De uitvoer van de bovenstaande opdracht bevat onze afbeelding usersignupsamen met de basisafbeelding, adoptopenjdkgespecificeerd in ons Dockerbestand.

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

Bekijk lagen in een containerimage

Laten we eens kijken naar de stapel lagen in de afbeelding. We zullen gebruiken инструмент  duiken, om deze lagen te bekijken:

dive usersignup:v1

Hier is een deel van de uitvoer van het duikcommando: 

Geoptimaliseerde Docker-images bouwen voor een Spring Boot-applicatie

Zoals we kunnen zien, vormt de applicatielaag een aanzienlijk deel van de afbeeldingsgrootte. We willen de grootte van deze laag in de volgende secties verkleinen als onderdeel van onze optimalisatie.

Een containerimage bouwen met Buildpack

Montage pakketten (bouwpakketten) is een algemene term die door verschillende Platform as a Service (PAAS)-aanbiedingen wordt gebruikt om een ​​containerimage van de broncode te maken. Het werd gelanceerd door Heroku in 2011 en is sindsdien overgenomen door Cloud Foundry, Google App Engine, Gitlab, Knative en een paar anderen.

Geoptimaliseerde Docker-images bouwen voor een Spring Boot-applicatie

Voordeel van Cloud Build-pakketten

Een van de belangrijkste voordelen van het gebruik van Buildpack om afbeeldingen te bouwen is dat Wijzigingen in de afbeeldingsconfiguratie kunnen centraal worden beheerd (builder) en met behulp van de builder naar alle applicaties worden doorgegeven.

De bouwpakketten waren nauw verbonden met het platform. Cloud-Native Buildpacks bieden standaardisatie op verschillende platforms door het OCI-imageformaat te ondersteunen, wat ervoor zorgt dat de image door de Docker-engine kan worden uitgevoerd.

De Spring Boot-plug-in gebruiken

De Spring Boot-plug-in bouwt OCI-afbeeldingen vanaf de broncode met behulp van Buildpack. Afbeeldingen worden gemaakt met behulp van bootBuildImagetaken (Gradle) of spring-boot:build-imagetarget (Maven) en lokale Docker-installatie.

We kunnen de naam aanpassen van de afbeelding die we naar het Docker-register moeten pushen door de naam op te geven 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>

Laten we Maven gebruiken om uit te voeren build-imagedoelen voor het maken van een applicatie en het maken van een containerimage. We gebruiken momenteel geen Dockerfiles.

mvn spring-boot:build-image

Het resultaat zal ongeveer zo zijn:

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

Uit de output zien we dat paketo Cloud-Native buildpackgebruikt om een ​​werkende OCI-image te maken. Net als voorheen kunnen we de afbeelding zien als een Docker-afbeelding door de opdracht uit te voeren:

docker images 

Conclusie:

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

Een containerimage maken met Jib

Jib is een plug-in voor het maken van afbeeldingen van Google die een alternatieve methode biedt voor het maken van een containerafbeelding vanaf de bron.

Opzetten jib-maven-pluginin pom.xml:

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

Vervolgens voeren we de Jib-plug-in uit met behulp van de Maven-opdracht om de applicatie te bouwen en de containerimage te maken. Net als voorheen gebruiken we hier geen Dockerfiles:

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

Na het uitvoeren van de bovenstaande Maven-opdracht krijgen we de volgende uitvoer:

[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

Uit de uitvoer blijkt dat de containerimage is gemaakt en in het register is geplaatst.

Motivaties en technieken voor het maken van geoptimaliseerde afbeeldingen

We hebben twee belangrijke redenen om te optimaliseren:

  • Производительность: In een containerorkestratiesysteem wordt een containerimage uit het imageregister gehaald naar de host waarop de containerengine draait. Dit proces heet planning. Het ophalen van grote afbeeldingen uit het register resulteert in lange planningstijden in containerorkestratiesystemen en lange bouwtijden in CI-pijplijnen.
  • veiligheid: grote afbeeldingen hebben ook een groot gebied voor kwetsbaarheden.

Een Docker-afbeelding bestaat uit een stapel lagen, die elk een statement in ons Dockerbestand vertegenwoordigen. Elke laag vertegenwoordigt de delta van veranderingen in de onderliggende laag. Wanneer we een Docker-image uit het register halen, wordt deze in lagen opgehaald en op de host in de cache opgeslagen.

Spring Boot-gebruik "dikke JAR" in als het standaardverpakkingsformaat. Als we naar een dikke JAR kijken, zien we dat de applicatie een heel klein onderdeel is van de hele JAR. Dit is het onderdeel dat het meest verandert. De rest bestaat uit Spring Framework-afhankelijkheden.

De optimalisatieformule is erop gericht de applicatie op een afzonderlijk niveau te isoleren van de Spring Framework-afhankelijkheden.

De afhankelijkheidslaag die het grootste deel van het dikke JAR-bestand vormt, wordt slechts één keer gedownload en in de cache op het hostsysteem opgeslagen.

Tijdens app-updates en containerplanning wordt slechts een dunne laag van de app getrokken, zoals weergegeven in dit diagram:

Geoptimaliseerde Docker-images bouwen voor een Spring Boot-applicatie

In de volgende secties bekijken we hoe u deze geoptimaliseerde afbeeldingen kunt maken voor een Spring Boot-toepassing.

Een geoptimaliseerde containerimage bouwen voor een Spring Boot-applicatie met Buildpack

Spring Boot 2.3 ondersteunt gelaagdheid door delen van een dik JAR-bestand in afzonderlijke lagen te extraheren. De gelaagdheidsfunctie is standaard uitgeschakeld en moet expliciet worden ingeschakeld met behulp van de Spring Boot Maven-plug-in:

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

We zullen deze configuratie gebruiken om onze containerimage eerst met Buildpack en vervolgens met Docker te bouwen in de volgende secties.

Laten we rennen build-imageMaven-doel om een ​​containerimage te maken:

mvn spring-boot:build-image

Als we Dive uitvoeren om de lagen in de resulterende afbeelding te bekijken, kunnen we zien dat de applicatielaag (rood omcirkeld) veel kleiner is in kilobytes vergeleken met wat we kregen met het dikke JAR-formaat:

Geoptimaliseerde Docker-images bouwen voor een Spring Boot-applicatie

Een geoptimaliseerde containerimage bouwen voor een Spring Boot-applicatie met Docker

In plaats van een Maven- of Gradle-plug-in te gebruiken, kunnen we ook een gelaagde Docker JAR-afbeelding maken met een Docker-bestand.

Wanneer we Docker gebruiken, moeten we twee extra stappen ondernemen om de lagen te extraheren en naar de uiteindelijke afbeelding te kopiëren.

De inhoud van de resulterende JAR na het bouwen met Maven met ingeschakelde lagen zal er als volgt uitzien:

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

De uitvoer toont een extra JAR met de naam spring-boot-jarmode-layertoolsи layersfle.idxbestand. Dit extra JAR-bestand biedt gelaagde verwerkingsmogelijkheden, zoals beschreven in de volgende sectie.

Afhankelijkheden op afzonderlijke lagen extraheren

Om lagen uit onze gelaagde JAR te bekijken en te extraheren, gebruiken we de systeemeigenschap -Djarmode=layertoolsom te beginnen spring-boot-jarmode-layertoolsJAR in plaats van toepassing:

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

Het uitvoeren van deze opdracht levert een uitvoer op met de beschikbare opdrachtopties:

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

De uitvoer toont de opdrachten listextractи helpс helpde standaard zijn. Laten we de opdracht uitvoeren met listkeuze:

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

We zien een lijst met afhankelijkheden die als lagen kunnen worden toegevoegd.

Standaard lagen:

Naam van laag

Inhoud

dependencies

elke afhankelijkheid waarvan de versie geen SNAPSHOT bevat

spring-boot-loader

JAR-laderklassen

snapshot-dependencies

elke afhankelijkheid waarvan de versie SNAPSHOT bevat

application

toepassingsklassen en bronnen

Lagen worden gedefinieerd in layers.idxbestand in de volgorde waarin ze aan de Docker-image moeten worden toegevoegd. Deze lagen worden na de eerste keer ophalen op de host in de cache opgeslagen, omdat ze niet veranderen. Alleen de bijgewerkte applicatielaag wordt gedownload naar de host, wat sneller is vanwege de kleinere omvang .

Een afbeelding bouwen met afhankelijkheden die in afzonderlijke lagen zijn geëxtraheerd

We zullen het uiteindelijke beeld in twee stappen bouwen met behulp van een methode genaamd meertrapsmontage . In de eerste stap extraheren we de afhankelijkheden en in de tweede stap kopiëren we de geëxtraheerde afhankelijkheden naar het uiteindelijke .

Laten we ons Dockerbestand aanpassen voor een build in meerdere fasen:

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

We slaan deze configuratie op in een apart bestand - Dockerfile2.

We bouwen de Docker-image met behulp van de opdracht:

docker build -f Dockerfile2 -t usersignup:v1 .

Na het uitvoeren van deze opdracht krijgen we de volgende uitvoer:

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

We kunnen zien dat de Docker-afbeelding is gemaakt met een afbeeldings-ID en vervolgens is getagd.

Ten slotte voeren we de opdracht Dive uit zoals voorheen om de lagen in de gegenereerde Docker-afbeelding te bekijken. We kunnen een afbeeldings-ID of tag opgeven als invoer voor de duikopdracht:

dive userssignup:v1

Zoals u in de uitvoer kunt zien, is de laag die de applicatie bevat nu slechts 11 KB groot en worden afhankelijkheden in afzonderlijke lagen in de cache opgeslagen. 

Geoptimaliseerde Docker-images bouwen voor een Spring Boot-applicatie

Extraheer interne afhankelijkheden op afzonderlijke lagen

We kunnen de grootte van de applicatielaag verder verkleinen door al onze aangepaste afhankelijkheden naar een aparte laag te extraheren in plaats van ze in de applicatie te verpakken door ze in te declareren ymlvergelijkbaar bestand met de naam 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/"

In dit bestand layers.idxwe hebben een aangepaste afhankelijkheid toegevoegd met de naam, io.myorgmet organisatie-afhankelijkheden die zijn opgehaald uit de gedeelde repository.

Uitgang

In dit artikel hebben we gekeken naar het gebruik van Cloud-Native Buildpacks om rechtstreeks vanuit de bron een containerimage te bouwen. Dit is een alternatief voor het gebruik van Docker om op de gebruikelijke manier een containerimage te maken: eerst wordt een dik uitvoerbaar JAR-bestand gemaakt en vervolgens verpakt in een containerimage door de instructies in het Dockerbestand op te geven.

We hebben ook gekeken naar het optimaliseren van onze container door een gelaagdheidsfunctie op te nemen die afhankelijkheden extraheert in afzonderlijke lagen die in de cache op de host worden opgeslagen en een dunne applicatielaag wordt geladen op het geplande tijdstip in de uitvoeringsengines van de container.

Je kunt alle broncode die in het artikel wordt gebruikt vinden op GitHub .

Commandoreferentie

Hier volgt een samenvatting van de opdrachten die we in dit artikel hebben gebruikt, zodat u deze snel kunt raadplegen.

Context opheldering:

docker system prune -a

Een containerimage bouwen met een Dockerfile:

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

Bouw een containerimage vanaf de bron (zonder Dockerfile):

mvn spring-boot:build-image

Bekijk afhankelijkheidslagen. Voordat u het JAR-bestand van de toepassing bouwt, moet u ervoor zorgen dat de gelaagdheidsfunctie is ingeschakeld in de spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Afhankelijkheidslagen extraheren. Voordat u het jar-bestand van de toepassing bouwt, moet u ervoor zorgen dat de gelaagdheidsfunctie is ingeschakeld in de spring-boot-maven-plug-in:

 java -Djarmode=layertools -jar application.jar extract

Een lijst met containerimages bekijken

docker images

Bekijk links in de containerafbeelding (zorg ervoor dat de duiktool is geïnstalleerd):

dive <image ID or image tag>

Bron: www.habr.com