Geoptimaliseerde Docker-images maken voor een Spring Boot-applicatie

Containers zijn de voorkeursmethode geworden voor het verpakken van een applicatie met al zijn software- en besturingssysteemafhankelijkheden, en voor het vervolgens leveren aan verschillende omgevingen.

In dit artikel worden verschillende manieren besproken om een Spring Boot-applicatie te containeriseren:

  • het creëren van een Docker-image met behulp van een Docker-bestand,
  • het creëren van een OCI-image uit de broncode met behulp van Cloud-Native Buildpack,
  • en het optimaliseren van het image tijdens runtime door delen van de JAR te scheiden op verschillende niveaus met behulp van multistage tools.

 Voorbeeldcode

Dit artikel wordt ondersteund door een voorbeeld van werkende code op GitHub .

Container terminologie

We beginnen met de container terminologie die in dit artikel wordt gebruikt:

  • Container image: een bestand van een specifiek formaat. We converteren onze applicatie naar een container image door een bouwtool uit te voeren.
  • Container: een uitvoerende instantie van een container image.
  • Container engine: een daemonproces dat verantwoordelijk is voor het uitvoeren van de container.
  • Container host: de hostcomputer waarop de container engine draait.
  • Container registry: een algemene locatie die wordt gebruikt voor het publiceren en distribueren van container images.
  • OCI-standaard: Open Container Initiative (OCI) is een lichtgewicht open governance-structuur die is opgericht binnen de Linux Foundation. De OCI-image-specificatie definieert industriestandaarden voor container image-formaten en runtimes, zodat alle container engines container images kunnen uitvoeren die door elke bouwtool zijn gemaakt.

Om een applicatie in een container te plaatsen, verpakken we onze applicatie in een container image en publiceren we deze image in een openbare registry. De container runtime haalt deze image uit de registry, pakt deze uit en voert de applicatie binnenin uit.

Versie 2.3 van Spring Boot biedt plugins voor het creëren van OCI-images.

Docker de meest gebruikte containerimplementatie, en we gebruiken Docker in onze voorbeelden, dus alle verdere verwijzingen naar container in dit artikel zullen betrekking hebben op Docker.

Het bouwen van een container image op de traditionele manier

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

Eerst maken we een uitvoerbaar JAR-bestand en als onderdeel van de Docker-instructies kopiëren we het uitvoerbare JAR-bestand bovenop de basisafbeelding van JRE na het toepassen van de nodige configuraties.

Laten we onze Spring-applicatie maken op Spring Initializr met afhankelijkheden web, lomboken actuator. We voegen ook een REST-controller toe om een API te bieden met GETde methode.

Het maken van een Docker-bestand

Vervolgens plaatsen we deze applicatie in een container, waarbij we toevoegen Dockerfile:

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

Ons Docker-bestand bevat een basisafbeelding van adoptopenjdk, waarop we ons JAR-bestand kopieren en vervolgens de poort openen, 8080die verzoeken zal afluisteren.

De applicatie bouwen

Eerst moet de applicatie worden gemaakt met Maven of Gradle. Hier gebruiken we Maven:

mvn clean package

Dit creëert een uitvoerbaar JAR-bestand van de applicatie. We moeten dit uitvoerbare JAR omzetten naar een Docker-afbeelding om te draaien in de Docker-engine.

Een containerafbeelding maken

Daarna plaatsen we dit uitvoerbare JAR-bestand in de Docker-afbeelding door de opdracht te uitvoeren docker builduit de hoofdmap van het project, dat eerder het Docker-bestand bevatte:

docker build  -t usersignup:v1 .

We kunnen onze afbeelding in de lijst zien met behulp van de opdracht:

docker images 

Het resultaat van het uitvoeren van de bovenstaande opdracht omvat onze afbeelding usersignupsamen met de basisafbeelding, adoptopenjdk, zoals opgegeven in onze Docker-bestand.

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

De lagen binnen de containerafbeelding bekijken

Laten we kijken naar de stapel lagen binnen de afbeelding. We zullen gebruiken de tool  dive, om deze lagen te bekijken:

dive usersignup:v1

Hier is een deel van de resultaten van het uitvoeren van de Dive-opdracht: 

Geoptimaliseerde Docker-images maken voor een Spring Boot-applicatie

Zoals we zien, is het toepassingsniveau een aanzienlijk deel van de afbeeldingsgrootte. We willen de grootte van deze laag verminderen in de volgende secties in het kader van onze optimalisatie.

Een containerafbeelding maken met behulp van Buildpack

Buildpacks (Buildpacks) is een algemene term die wordt gebruikt door verschillende "Platform as a Service" (PAAS) aanbiedingen om een containerafbeelding te maken uit de broncode. Het werd gelanceerd door Heroku in 2011 en is sindsdien aangenomen door Cloud Foundry, Google App Engine, Gitlab, Knative en sommige anderen.

Geoptimaliseerde Docker-images maken voor een Spring Boot-applicatie

Het voordeel van cloud-buildpacks

Een van de belangrijkste voordelen van het gebruik van Buildpack voor het maken van afbeeldingen is dat De wijzigingen in de image-configuratie kunnen centraal worden beheerd (builder) en verspreid worden naar alle applicaties die gebruikmaken van de builder.

Buildpacks waren sterk verbonden met het platform. Cloud-Native Buildpacks zorgen voor standaardisatie tussen platforms door het OCI-imageformaat te ondersteunen, dat garandeert dat het image kan worden uitgevoerd door de Docker-engine.

Het gebruik van de Spring Boot-plugin

De Spring Boot-plugin creëert OCI-images uit de source code met behulp van Buildpack. Images worden gemaakt met behulp van bootBuildImagetaken (Gradle) of spring-boot:build-imagedoelen (Maven) en een lokale Docker-installatie.

We kunnen de naam van de image instellen die naar de Docker registry moet worden gepusht door de naam op te geven in image tag:

org.springframework.boot
  spring-boot-maven-plugin
  
    
      docker.io/pratikdas/${project.artifactId}:v1

Laten we Maven gebruiken om de build-imagedoel uit te voeren om de applicatie te bouwen en een containerimage te maken. We gebruiken momenteel geen Docker-bestanden.

mvn spring-boot:build-image

Het resultaat zal ongeveer als volgt 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 uitvoer kunnen we zien dat paketo Cloud-Native buildpackwordt gebruikt om een werkend OCI-image te creëren. Zoals voorheen kunnen we het image dat als Docker-image is opgegeven, zien door de opdracht uit te voeren:

docker images 

Uitvoer:

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

Een containerimage maken met Jib

Jib is een imagebouwplugin van Google die een alternatieve methode biedt om een containerimage uit de source code te maken.

We configureren jib-maven-pluginin pom.xml:

      com.google.cloud.tools
        jib-maven-plugin
        2.5.2

Vervolgens voeren we de Jib-plugin uit met de Maven-opdracht om de applicatie te bouwen en een containerimage te creëren. Zoals voorheen gebruiken we hier geen Docker-bestanden:

mvn compile jib:build -Dimage=/usersignup:v1

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

[INFO] Containerize de applicatie naar pratikdas/usersignup:v1...
.
.
[INFO] Container entrypoint ingesteld op [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO] 
[INFO] Afbeelding gebouwd en gepusht als pratikdas/usersignup:v1
[INFO] Uitvoeren van taken:
[INFO] [==============================] 100.0% voltooid

De uitvoer toont aan dat de containerafbeelding is gemaakt en in de registry is geplaatst.

Motivaties en methoden voor het creëren van geoptimaliseerde afbeeldingen

We hebben twee belangrijke redenen voor optimalisatie:

  • Prestaties: in het containersorceringssysteem wordt de containerafbeelding opgehaald uit de afbeeldingsregister op de host waar de containermechanisme wordt uitgevoerd. Dit proces wordt plannen genoemd. Het ophalen van grote afbeeldingsbestanden uit de registry leidt tot lange planningtijden in containersorceringssystemen en lange build-tijden in CI-pijplijnen.
  • Beveiliging: grote afbeeldingen hebben ook een grotere oppervlakte voor kwetsbaarheden.

Een Docker-afbeelding bestaat uit een stapel lagen, waarvan elke laag een instructie in ons Dockerfile vertegenwoordigt. Elke laag is een delta van de wijzigingen in de onderliggende laag. Wanneer we een Docker-afbeelding uit de registry ophalen, worden deze in lagen opgehaald en op de host gecached.

Spring Boot gebruikt "fat JAR" als standaard verpakkingsformaat. Wanneer we naar de fat JAR kijken, zien we dat de applicatie slechts een klein deel van de hele JAR uitmaakt. Dit is het gedeelte dat het vaakst verandert. De rest bestaat uit afhankelijkheden van het Spring Framework.

De optimalisatieformule richt zich op het isoleren van de applicatie op een afzonderlijk niveau van de afhankelijkheden van het Spring Framework.

De laag met afhankelijkheden, die het grootste deel van het fat JAR-bestand vormt, wordt slechts één keer geladen en gecached in het host-systeem.

Alleen de dunne laag van de applicatie wordt opgehaald tijdens applicatie-updates en containerplanning, zoals weergegeven in dit diagram:

Geoptimaliseerde Docker-images maken voor een Spring Boot-applicatie

In de volgende secties zullen we bekijken hoe deze geoptimaliseerde afbeeldingen voor Spring Boot-applicaties te maken.

Het maken van een geoptimaliseerde containerafbeelding voor een Spring Boot-applicatie met behulp van Buildpack

Spring Boot 2.3 ondersteunt multilayering door delen van het fat JAR-bestand in afzonderlijke lagen te extraheren. De layering functie is standaard uitgeschakeld en moet expliciet worden ingeschakeld met de Spring Boot Maven-plugin:

org.springframework.boot
  spring-boot-maven-plugin
  
    
      true

We zullen deze configuratie gebruiken om eerst ons containerbeeld te creëren met Buildpack, en daarna met Docker in de volgende secties.

Laten we de build-imageMaven-doelstelling voor het bouwen van een containerbeeld uitvoeren:

mvn spring-boot:build-image

Als we Dive uitvoeren om de lagen in het resulterende beeld te bekijken, zullen we zien dat de applicatielaag (omcirkeld in het rood) veel kleiner is in het kilobyte-bereik dan wat we hebben verkregen met het dikke JAR-formaat:

Geoptimaliseerde Docker-images maken voor een Spring Boot-applicatie

Een geoptimaliseerd containerbeeld voor een Spring Boot-applicatie creëren met Docker

In plaats van de Maven- of Gradle-plugin te gebruiken, kunnen we ook een gelaagd Docker JAR-beeld maken met een Docker-bestand.

Wanneer we Docker gebruiken, moeten we twee extra stappen uitvoeren om de lagen te extraheren en deze in het uiteindelijke beeld te kopiëren.

De inhoud van het resulterende JAR na het bouwen met Maven met de ingeschakelde gelaagdheid 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

In de uitvoer wordt een extra JAR met de naam weergegeven spring-boot-jarmode-layertoolsen layersfle.idxbestand. Dit extra JAR-bestand biedt de mogelijkheid tot gelaagde verwerking, zoals in de volgende sectie wordt beschreven.

Afhankelijkheden extraheren op afzonderlijke lagen

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

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

Het uitvoeren van deze opdracht geeft output 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 lijst, extracten helpmet helpte zijn. Laten we de opdracht uitvoeren met lijstde optie:

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.

Standaardlagen:

Laagnaam

Inhoud

dependencies

Elke afhankelijkheid waarvan de versie geen SNAPSHOT bevat

spring-boot-loader

JAR-laadclasses

snapshot-dependencies

Elke afhankelijkheid waarvan de versie een SNAPSHOT bevat

application

applicatieklassen en -bronnen

De lagen zijn gedefinieerd in layers.idxhet bestand in de volgorde waarin ze aan de Docker-image moeten worden toegevoegd. Deze lagen worden gecached op de host nadat ze voor het eerst zijn opgehaald, aangezien ze niet veranderen. Alleen de bijgewerkte laag van de applicatie wordt naar de host geüpload, wat sneller gebeurt door de verminderde grootte. .

Een image bouwen met afhankelijkheden, opgehaald in aparte lagen.

We zullen de uiteindelijke image in twee fasen bouwen, met een methode genaamd multi-stage build . In de eerste fase halen we de afhankelijkheden op, en in de tweede fase kopiëren we de opgehaalde afhankelijkheden naar de uiteindelijke image.

Laten we ons Docker-bestand aanpassen voor de 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"]

We bewaren deze configuratie in een apart bestand — Dockerfile2.

We bouwen de Docker-image met het commando:

docker build -f Dockerfile2 -t usersignup:v1 .

Na het uitvoeren van dit commando krijgen we de volgende 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

We zien dat de Docker-image wordt gemaakt met een image-ID, en vervolgens wordt getagd.

Ten slotte voeren we het Dive-commando uit, zoals eerder, om de lagen binnen de gegenereerde Docker-image te bekijken. We kunnen ofwel de image-ID of de tag als invoer voor het Dive-commando opgeven:

dive userssignup:v1

Zoals te zien is uit de output, neemt de laag die de applicatie bevat nu slechts 11 KB in beslag, en worden de afhankelijkheden gecached in aparte lagen. 

Geoptimaliseerde Docker-images maken voor een Spring Boot-applicatie

Interne afhankelijkheden ophalen in aparte lagen.

We kunnen de grootte van de applicatielaag verder verminderen door enkele van onze aangepaste afhankelijkheden in een aparte laag op te halen, in plaats van ze samen met de applicatie in te pakken, door ze in te voeren in ymleen dergelijk 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.idxhebben we een aanpasbare afhankelijkheid toegevoegd met de naam, io.myorgdie afhankelijkheden van de organisatie bevat, verkregen uit een openbare repository.

Uitslag

In dit artikel hebben we het gebruik van Cloud-Native Buildpacks besproken om een containerafbeelding direct vanuit de broncode te creëren. Dit is een alternatief voor het gebruik van Docker om een containerafbeelding op de gebruikelijke manier te maken: eerst wordt een zware uitvoerbare JAR-bestand gemaakt en vervolgens wordt deze verpakt in een containerafbeelding door instructies in het Docker-bestand op te geven.

We hebben ook de optimalisatie van onze container besproken door de laagfunctie op te nemen, die afhankelijkheden in aparte lagen extrahert, die op de host worden gecached, terwijl de dunne laag van de applicatie tijdens de planning wordt geladen in de uitvoeringsmechanismen van de container.

Je kunt de gehele broncode die in dit artikel is gebruikt vinden op Github .

Commando-gids

Hier is een samenvatting van de commando's die we in dit artikel hebben gebruikt voor een snel overzicht.

Context opschonen:

docker system prune -a

Een containerafbeelding maken met een Docker-bestand:

docker build -f  -t  .

Een containerafbeelding bouwen vanuit broncode (zonder Dockerfile):

mvn spring-boot:build-image

Bekijk de lagen van afhankelijkheden. Zorg ervoor dat de laagfunctie is ingeschakeld in de spring-boot-maven-plugin voordat je het JAR-bestand van de applicatie bouwt:

java -Djarmode=layertools -jar application.jar list

Afhankelijkheidslagen extraheren. Zorg ervoor dat de laagfunctie is ingeschakeld in de spring-boot-maven-plugin voordat je het JAR-bestand van de applicatie bouwt:

 java -Djarmode=layertools -jar application.jar extract

Bekijk de lijst met containerafbeeldingen

docker images

Bekijk de lagen binnen de containerafbeelding (zorg ervoor dat de tool voor het duiken is geïnstalleerd):

dive

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster