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 standard: Open 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 web, lombokи 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:
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:
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.
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:
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.
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:
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:
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:
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:
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 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:
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:
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 list, extractи helpс helpvara standard. Låt oss köra kommandot med listalternativ:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
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.
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:
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: