Skapa optimerade Docker-bilder för en Spring Boot-applikation

Behållare har blivit det föredragna sättet att paketera en applikation med alla dess mjukvaru- och operativsystemberoenden och sedan leverera dem till olika miljöer.

Den här artikeln tar upp olika sätt att behålla en Spring Boot-applikation:

  • bygga en docker-bild med en dockerfil,
  • bygga en OCI-bild från källan med hjälp av Cloud-Native Buildpack,
  • och bildoptimering under körning genom att separera JAR-delar i olika nivåer med hjälp av lagerverktyg.

 Kodexempel

Den här artikeln åtföljs av ett exempel på en fungerande kod på GitHub .

Behållarterminologi

Vi börjar med behållarterminologin som används i hela artikeln:

  • Behållarbild: en fil av ett specifikt format. Vi konverterar vår applikation till en containerbild genom att köra byggverktyget.
  • behållare: En körbar instans av behållaravbildningen.
  • Containermotor: Demonprocessen som ansvarar för att köra behållaren.
  • Container värd: Värdmaskinen som containermotorn körs på.
  • Behållarregistret: Den allmänna platsen som används för att publicera och distribuera behållarbilden.
  • OCI standardOpen Container Initiative (OCI) är ett lätt hanteringsramverk med öppen källkod bildat av Linux Foundation. OCI Image Specification definierar industristandarder för containerbildformat och körtid för att säkerställa att alla containermotorer kan köra containerbilder skapade av vilket byggverktyg som helst.

För att behålla en applikation lindar vi in ​​vår applikation i en behållarbild och publicerar den bilden till det offentliga registret. Behållarens körtid hämtar den här bilden från registret, packar upp den och kör applikationen inuti den.

Version 2.3 av Spring Boot tillhandahåller plugins för att bygga OCI-bilder.

Hamnarbetare är den mest använda containerimplementeringen, och vi använder Docker i våra exempel, så alla efterföljande containerreferenser i den här artikeln kommer att referera till Docker.

Bygga en containerbild på traditionellt sätt

Att bygga Docker-bilder för Spring Boot-applikationer är mycket enkelt genom att lägga till några instruktioner till din Dockerfil.

Vi skapar först en körbar JAR och, som en del av Dockerfile-instruktionerna, kopierar vi den körbara JAR-en ovanpå JRE-basbilden efter att ha tillämpat de nödvändiga anpassningarna.

Låt oss skapa vår vårapplikation på Spring Initializr med beroenden weblombokи actuator. Vi lägger också till en vilokontroller för att förse ett API med GETmetod.

Skapa en dockerfil

Vi placerar sedan denna applikation i en behållare genom att lägga till Dockerfile:

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

Vår Docker-fil innehåller en basbild från adoptopenjdk, på vilken vi kopierar vår JAR-fil och öppnar sedan porten, 8080som kommer att lyssna efter förfrågningar.

Applikationsmontering

Först måste du skapa en applikation med Maven eller Gradle. Här använder vi Maven:

mvn clean package

Detta skapar en körbar JAR-fil för programmet. Vi måste konvertera denna körbara JAR till en Docker-avbildning för att köras på Docker-motorn.

Skapa en containerbild

Vi lägger sedan in den här JAR-körbara filen i Docker-avbildningen genom att köra kommandot docker buildfrån rotkatalogen för projektet som innehåller Dockerfilen som skapades tidigare:

docker build  -t usersignup:v1 .

Vi kan se vår bild i listan med kommandot:

docker images 

Utdata från kommandot ovan inkluderar vår bild usersignuptillsammans med basbilden, adoptopenjdkspecificeras i vår Dockerfile.

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

Visa lager inuti en behållarbild

Låt oss titta på bunten av lager inuti bilden. Vi kommer använda инструмент  dyka, för att se dessa lager:

dive usersignup:v1

Här är en del av resultatet av kommandot Dive: 

Skapa optimerade Docker-bilder för en Spring Boot-applikation

Som vi kan se utgör applikationslagret en betydande del av bildstorleken. Vi vill minska storleken på detta lager i följande avsnitt som en del av vår optimering.

Bygga en containerbild med Buildpack

Monteringspaket (Byggpaket) är en generisk term som används av olika Platform as a Service-erbjudanden (PAAS) för att skapa en containerbild från källkoden. Den lanserades av Heroku 2011 och har sedan dess antagits av Cloud Foundry, Google App Engine, Gitlab, Knative och några andra.

Skapa optimerade Docker-bilder för en Spring Boot-applikation

Fördel med Cloud Build-paket

En av de främsta fördelarna med att använda Buildpack för att skapa bilder är att bildkonfigurationsändringar kan hanteras centralt (byggare) och spridas till alla applikationer som använder byggaren.

Byggpaketen var nära knutna till plattformen. Cloud-Native Buildpacks möjliggör standardisering över plattformar genom att stödja OCI-bildformatet, vilket säkerställer att bilden kan köras av Docker-motorn.

Använder Spring Boot Plugin

Spring Boot-pluginen bygger OCI-bilder från källan med hjälp av Buildpack. Bilder skapas med hjälp av bootBuildImageuppgifter (Gradle) eller spring-boot:build-imagemål (Maven) och lokal Docker-installation.

Vi kan anpassa namnet på bilden som vi behöver skicka till Docker-registret genom att ange namnet 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>

Låt oss använda Maven för att köra build-imagemål för att skapa en applikation och skapa en containerbild. Vi använder för närvarande inga Dockerfiler.

mvn spring-boot:build-image

Resultatet blir ungefär så här:

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

Från utgången ser vi det paketo Cloud-Native buildpackanvänds för att skapa en fungerande OCI-bild. Som tidigare kan vi se bilden listad som en Docker-bild genom att köra kommandot:

docker images 

Slutsats:

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

Skapa en containerbild med Jib

Jib är ett plugin för bildskapande från Google som tillhandahåller en alternativ metod för att skapa en behållarbild från källan.

Installation jib-maven-plugini pom.xml:

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

Därefter kör vi Jib-plugin med hjälp av Maven-kommandot för att bygga applikationen och skapa behållarbilden. Som tidigare använder vi inga Dockerfiler här:

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

Efter att ha utfört ovanstående Maven-kommando får vi följande utdata:

[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

Utdata visar att behållaravbildningen har skapats och placerats i registret.

Motivationer och metoder för att skapa optimerade bilder

Vi har två huvudskäl till optimering:

  • Производительность: I ett containerorkestreringssystem hämtas en containeravbildning från avbildningsregistret till den värd som kör containermotorn. Denna process kallas planering. Att dra stora bilder från registret resulterar i långa schemaläggningstider i containerorkestreringssystem och långa byggtider i CI-pipelines.
  • Безопасность: stora bilder har också ett stort område för sårbarheter.

En Docker-bild är uppbyggd av en stapel med lager, som vart och ett representerar ett uttalande i vår Dockerfil. Varje lager representerar deltat av förändringar i det underliggande lagret. När vi hämtar en Docker-bild från registret dras den i lager och cachelagras på värden.

Spring Boot använder "fat JAR" i som standardförpackningsformat. När vi tittar på en fet JAR ser vi att applikationen är en väldigt liten del av hela JAR. Det är den delen som förändras mest. Resten består av Spring Framework-beroenden.

Optimeringsformeln är centrerad kring att isolera applikationen på en separat nivå från Spring Framework-beroenden.

Beroendelagret som utgör huvuddelen av den tjocka JAR-filen laddas bara ner en gång och cachelagras på värdsystemet.

Endast ett tunt lager av appen dras under appuppdateringar och containerschemaläggning, som visas i detta diagram:

Skapa optimerade Docker-bilder för en Spring Boot-applikation

I följande avsnitt kommer vi att titta på hur man skapar dessa optimerade bilder för en Spring Boot-applikation.

Skapa en optimerad containerbild för en Spring Boot-applikation med Buildpack

Spring Boot 2.3 stöder skiktning genom att extrahera delar av en tjock JAR-fil i separata lager. Lagerfunktionen är inaktiverad som standard och måste uttryckligen aktiveras med 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 kommer att använda den här konfigurationen för att bygga vår containerbild först med Buildpack och sedan med Docker i följande avsnitt.

Låt oss springa build-imageMaven-mål för att skapa behållarbild:

mvn spring-boot:build-image

Om vi ​​kör Dive för att se lagren i den resulterande bilden kan vi se att applikationslagret (markerat i rött) är mycket mindre i kilobyteintervallet jämfört med vad vi fick med det tjocka JAR-formatet:

Skapa optimerade Docker-bilder för en Spring Boot-applikation

Skapa en optimerad containerbild för en Spring Boot-applikation med Docker

Istället för att använda ett Maven- eller Gradle-plugin kan vi också skapa en Docker JAR-bild i lager med en Docker-fil.

När vi använder Docker måste vi ta två extra steg för att extrahera lagren och kopiera dem till den slutliga bilden.

Innehållet i den resulterande JAR efter att ha byggt med Maven med lager aktiverat kommer att se ut så här:

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

Utgången visar en extra JAR som heter spring-boot-jarmode-layertoolsи layersfle.idxfil. Denna ytterligare JAR-fil tillhandahåller lagerfunktioner, som beskrivs i nästa avsnitt.

Extrahera beroenden på separata lager

För att visa och extrahera lager från vår lager JAR använder vi systemegenskapen -Djarmode=layertoolstill start spring-boot-jarmode-layertoolsJAR istället för applikation:

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

Att köra det här kommandot producerar en utdata som innehåller de tillgängliga kommandoalternativen:

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

Utgången visar kommandona listextractи helpс helpvara standard. Låt oss köra kommandot med listalternativ:

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

Vi ser en lista över beroenden som kan läggas till som lager.

Standardlager:

Lagrets namn

Innehåll

dependencies

något beroende vars version inte innehåller SNAPSHOT

spring-boot-loader

JAR-lastarklasser

snapshot-dependencies

alla beroenden vars version innehåller SNAPSHOT

application

applikationsklasser och resurser

Lager definieras i layers.idxfilen i den ordning som de ska läggas till i Docker-bilden. Dessa lager cachelagras på värden efter den första hämtningen eftersom de inte ändras. Endast det uppdaterade applikationslagret laddas ner till värden, vilket är snabbare på grund av den minskade storleken .

Bygga en bild med beroenden extraherade i separata lager

Vi kommer att bygga den slutliga bilden i två steg med hjälp av en metod som kallas flerstegsmontering . I det första steget kommer vi att extrahera beroenden och i det andra steget kommer vi att kopiera de extraherade beroendena till det sista.

Låt oss modifiera vår Dockerfile för en flerstegsbyggnad:

# 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 sparar denna konfiguration i en separat fil - Dockerfile2.

Vi bygger Docker-bilden med kommandot:

docker build -f Dockerfile2 -t usersignup:v1 .

Efter att ha utfört detta kommando får vi följande utdata:

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 att Docker-bilden skapas med ett bild-ID och sedan taggas.

Slutligen kör vi kommandot Dive som tidigare för att kolla in lagren inuti den genererade Docker-bilden. Vi kan tillhandahålla ett bild-ID eller tagg som input till kommandot Dive:

dive userssignup:v1

Som du kan se från utdata är lagret som innehåller applikationen nu bara 11 KB och beroenden cachelagras i separata lager. 

Skapa optimerade Docker-bilder för en Spring Boot-applikation

Extrahera interna beroenden på enskilda lager

Vi kan ytterligare minska storleken på applikationslagret genom att extrahera något av våra anpassade beroenden till ett separat lager istället för att paketera dem med applikationen genom att deklarera dem i ymlliknande fil namngiven 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 denna fil layers.idxvi har lagt till ett anpassat beroende som heter, io.myorgsom innehåller organisationsberoende som hämtats från det delade arkivet.

Utgång

I den här artikeln tittade vi på hur vi använder Cloud-Native Buildpacks för att bygga en containerbild direkt från källan. Detta är ett alternativ till att använda Docker för att skapa en containeravbild på vanligt sätt: först skapas en tjock körbar JAR-fil och paketeras sedan i en containeravbild genom att specificera instruktionerna i Dockerfilen.

Vi tittade också på att optimera vår container genom att inkludera en lagerfunktion som extraherar beroenden till separata lager som cachelagras på värden och ett tunt applikationslager laddas vid schemaläggningstid i containerns exekveringsmotorer.

Du kan hitta all källkod som används i artikeln på Github .

Kommandoreferens

Här är en sammanfattning av de kommandon vi använde i den här artikeln för en snabb referens.

Kontextrensning:

docker system prune -a

Bygga en containerbild med en Dockerfil:

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

Bygg behållarbild från källan (utan Dockerfile):

mvn spring-boot:build-image

Visa beroendelager. Innan du bygger applikationsjar-filen, se till att lagerfunktionen är aktiverad i spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Extrahera beroendelager. Innan du bygger applikationsjar-filen, se till att lagerfunktionen är aktiverad i spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Visa en lista över behållarbilder

docker images

Visa till vänster inuti behållarbilden (se till att dykverktyget är installerat):

dive <image ID or image tag>

Källa: will.com