Creazione di immagini Docker ottimizzate per un'applicazione Spring Boot

I contenitori sono diventati il ​​mezzo preferito per creare pacchetti di un'applicazione con tutte le sue dipendenze software e del sistema operativo e quindi distribuirli in ambienti diversi.

Questo articolo illustra diversi modi per containerizzare un'applicazione Spring Boot:

  • creando un'immagine Docker utilizzando un file Docker,
  • creando un'immagine OCI dall'origine utilizzando il Buildpack cloud-nativo,
  • e ottimizzazione delle immagini in fase di esecuzione separando parti del JAR in diversi livelli utilizzando strumenti multilivello.

 Esempio di codice

Questo articolo è accompagnato da un esempio di codice funzionante su GitHub .

Terminologia del contenitore

Inizieremo con la terminologia del contenitore utilizzata nell'articolo:

  • Immagine del contenitore: file di un formato specifico. Convertiremo la nostra applicazione in un'immagine contenitore eseguendo lo strumento di compilazione.
  • contenitore: un'istanza eseguibile dell'immagine del contenitore.
  • Motore portacontainer: il processo demone responsabile dell'esecuzione del contenitore.
  • Ospite del contenitore: il computer host su cui viene eseguito il motore del contenitore.
  • Registro dei contenitori: il percorso generale utilizzato per pubblicare e distribuire l'immagine del contenitore.
  • Norma OCIIniziativa sui container aperti (OCI) è una struttura di governance leggera e aperta formata all'interno della Linux Foundation. La specifica dell'immagine OCI definisce gli standard di settore per i formati di immagine e runtime del contenitore per garantire che tutti i motori del contenitore possano eseguire immagini del contenitore create da qualsiasi strumento di creazione.

Per containerizzare un'applicazione, racchiudiamo la nostra applicazione in un'immagine del contenitore e pubblichiamo tale immagine in un registro condiviso. Il runtime del contenitore recupera questa immagine dal registro, la decomprime ed esegue l'applicazione al suo interno.

La versione 2.3 di Spring Boot fornisce plug-in per la creazione di immagini OCI.

docker è l'implementazione del contenitore più comunemente utilizzata e nei nostri esempi utilizziamo Docker, quindi tutti i successivi riferimenti al contenitore in questo articolo faranno riferimento a Docker.

Costruire un'immagine del contenitore nel modo tradizionale

Creare immagini Docker per le applicazioni Spring Boot è molto semplice aggiungendo alcune istruzioni al file Docker.

Creiamo innanzitutto un file JAR eseguibile e, come parte delle istruzioni del file Docker, copiamo il file JAR eseguibile sopra l'immagine JRE di base dopo aver applicato le impostazioni necessarie.

Creiamo la nostra applicazione Spring su Inizializzazione di primavera con dipendenze weblombokи actuator. Stiamo anche aggiungendo un controller di riposo per fornire un'API GETmetodo.

Creazione di un Dockerfile

Quindi containerizziamo questa applicazione aggiungendo Dockerfile:

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

Il nostro file Docker contiene un'immagine di base da adoptopenjdk, sopra il quale copiamo il nostro file JAR e quindi apriamo la porta, 8080che ascolterà le richieste.

Creazione dell'applicazione

Per prima cosa devi creare un'applicazione utilizzando Maven o Gradle. Qui usiamo Maven:

mvn clean package

Questo crea un file JAR eseguibile per l'applicazione. Dobbiamo convertire questo JAR eseguibile in un'immagine Docker da eseguire sul motore Docker.

Creazione di un'immagine del contenitore

Quindi inseriamo questo file JAR eseguibile nell'immagine Docker eseguendo il comando docker builddalla directory root del progetto contenente il Dockerfile creato in precedenza:

docker build  -t usersignup:v1 .

Possiamo vedere la nostra immagine nell'elenco usando il comando:

docker images 

L'output del comando precedente include la nostra immagine usersignupinsieme all'immagine di base, adoptopenjdkspecificato nel nostro file Docker.

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

Visualizza i livelli all'interno di un'immagine contenitore

Diamo un'occhiata alla pila di livelli all'interno dell'immagine. Noi useremo инструмент  tuffo, per visualizzare questi livelli:

dive usersignup:v1

Ecco parte dell'output del comando Dive: 

Creazione di immagini Docker ottimizzate per un'applicazione Spring Boot

Come possiamo vedere, il livello dell'applicazione costituisce una parte significativa della dimensione dell'immagine. Vogliamo ridurre la dimensione di questo livello nelle sezioni seguenti come parte della nostra ottimizzazione.

Creazione di un'immagine del contenitore utilizzando Buildpack

Pacchetti di assemblaggio (Pacchetti di costruzione) è un termine generale utilizzato da varie offerte PAAS (Platform as a Service) per creare un'immagine del contenitore dal codice sorgente. È stato lanciato da Heroku nel 2011 e da allora è stato adottato da Cloud Foundry, Google App Engine, Gitlab, Knative e molti altri.

Creazione di immagini Docker ottimizzate per un'applicazione Spring Boot

Il vantaggio dei pacchetti di creazione cloud

Uno dei principali vantaggi dell'utilizzo di Buildpack per creare immagini è questo Le modifiche alla configurazione dell'immagine possono essere gestite centralmente (builder) e propagate a tutte le applicazioni utilizzando builder.

I pacchetti di build erano strettamente collegati alla piattaforma. I Buildpack nativi del cloud forniscono standardizzazione tra piattaforme supportando il formato immagine OCI, che garantisce che l'immagine possa essere eseguita dal motore Docker.

Utilizzando il plugin Spring Boot

Il plug-in Spring Boot crea immagini OCI dall'origine utilizzando Buildpack. Le immagini vengono create utilizzando bootBuildImagecompiti (Gradle) o spring-boot:build-imagetarget (Maven) e installazione Docker locale.

Possiamo personalizzare il nome dell'immagine necessaria per eseguire il push nel registro Docker specificando il nome in 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>

Usiamo Maven per farlo build-imageobiettivi per la creazione di un'applicazione e la creazione di un'immagine del contenitore. Al momento non stiamo utilizzando alcun Dockerfile.

mvn spring-boot:build-image

Il risultato sarà qualcosa del genere:

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

Dall'output lo vediamo paketo Cloud-Native buildpackutilizzato per creare un'immagine OCI funzionante. Come prima, possiamo vedere l'immagine elencata come immagine Docker eseguendo il comando:

docker images 

Conclusione:

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

Creazione di un'immagine del contenitore utilizzando Jib

Jib è un plug-in per la creazione di immagini di Google che fornisce un metodo alternativo per creare un'immagine contenitore dal codice sorgente.

Configurazione jib-maven-pluginin pom.xml:

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

Successivamente, eseguiamo il plug-in Jib utilizzando il comando Maven per creare l'applicazione e creare un'immagine del contenitore. Come prima, qui non utilizziamo alcun file Docker:

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

Dopo aver eseguito il comando Maven precedente, otteniamo il seguente 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

L'output mostra che l'immagine del contenitore è stata creata e inserita nel registro.

Motivazioni e tecniche per creare immagini ottimizzate

Abbiamo due ragioni principali per l’ottimizzazione:

  • Производительность: in un sistema di orchestrazione del contenitore, un'immagine del contenitore viene recuperata dal registro delle immagini sull'host che esegue il motore del contenitore. Questo processo è chiamato pianificazione. L'estrazione di immagini di grandi dimensioni dal registro comporta tempi di pianificazione lunghi nei sistemi di orchestrazione dei contenitori e tempi di compilazione lunghi nelle pipeline CI.
  • sicurezza: Le immagini più grandi hanno anche un'area più ampia per le vulnerabilità.

Un'immagine Docker è costituita da una pila di livelli, ognuno dei quali rappresenta un'istruzione nel nostro Dockerfile. Ogni livello rappresenta un delta delle modifiche nel livello sottostante. Quando estraiamo un'immagine Docker dal registro, viene estratta a livelli e memorizzata nella cache dell'host.

Utilizza Spring Boot "JAR grasso" in come formato di confezionamento predefinito. Quando guardiamo il JAR spesso, vediamo che l'applicazione costituisce una porzione molto piccola dell'intero JAR. Questa è la parte che cambia più spesso. Il resto è costituito dalle dipendenze di Spring Framework.

La formula di ottimizzazione è incentrata sull'isolamento dell'applicazione a un livello separato dalle dipendenze di Spring Framework.

Il livello di dipendenza, che costituisce la maggior parte del grosso file JAR, viene scaricato solo una volta e memorizzato nella cache del sistema host.

Durante gli aggiornamenti dell'applicazione e la pianificazione del contenitore viene estratto solo uno strato sottile dell'applicazione. come mostrato in questo diagramma:

Creazione di immagini Docker ottimizzate per un'applicazione Spring Boot

Nelle sezioni seguenti vedremo come creare queste immagini ottimizzate per un'applicazione Spring Boot.

Creazione di un'immagine del contenitore ottimizzata per un'applicazione Spring Boot utilizzando Buildpack

Spring Boot 2.3 supporta la stratificazione estraendo parti di un file JAR spesso in livelli separati. La funzionalità di stratificazione è disabilitata per impostazione predefinita e deve essere abilitata esplicitamente utilizzando il plugin Spring Boot Maven:

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

Utilizzeremo questa configurazione per creare la nostra immagine del contenitore prima con Buildpack e poi con Docker nelle sezioni seguenti.

Lanciamo build-imageTarget Maven per la creazione dell'immagine del contenitore:

mvn spring-boot:build-image

Se eseguiamo Dive per vedere i livelli nell'immagine risultante, possiamo vedere che il livello dell'applicazione (delineato in rosso) è molto più piccolo nell'intervallo dei kilobyte rispetto a quello che abbiamo ottenuto utilizzando il formato JAR spesso:

Creazione di immagini Docker ottimizzate per un'applicazione Spring Boot

Creazione di un'immagine del contenitore ottimizzata per un'applicazione Spring Boot utilizzando Docker

Invece di utilizzare un plug-in Maven o Gradle, possiamo anche creare un'immagine JAR Docker a più livelli con un file Docker.

Quando utilizziamo Docker, dobbiamo eseguire due passaggi aggiuntivi per estrarre i livelli e copiarli nell'immagine finale.

Il contenuto del JAR risultante dopo la creazione utilizzando Maven con la stratificazione abilitata sarà simile al seguente:

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

L'output mostra un JAR aggiuntivo denominato spring-boot-jarmode-layertoolsи layersfle.idxfile. Questo file JAR aggiuntivo fornisce funzionalità di elaborazione a più livelli, come descritto nella sezione successiva.

Estrazione delle dipendenze sui singoli livelli

Per visualizzare ed estrarre i livelli dal nostro JAR a livelli, utilizziamo la proprietà di sistema -Djarmode=layertoolsper correre spring-boot-jarmode-layertoolsJAR invece dell'applicazione:

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

L'esecuzione di questo comando produce un output contenente le opzioni di comando disponibili:

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

L'output mostra i comandi listextractи helpс helpessere l'impostazione predefinita. Eseguiamo il comando con listopzione:

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

Vediamo un elenco di dipendenze che possono essere aggiunte come livelli.

Livelli predefiniti:

Nome del livello

contenuto

dependencies

qualsiasi dipendenza la cui versione non contiene SNAPSHOT

spring-boot-loader

Classi del caricatore JAR

snapshot-dependencies

qualsiasi dipendenza la cui versione contiene SNAPSHOT

application

classi e risorse dell'applicazione

I livelli sono definiti in layers.idxfile nell'ordine in cui devono essere aggiunti all'immagine Docker. Questi layer vengono memorizzati nella cache dell'host dopo il primo recupero perché non cambiano. Solo il livello dell'applicazione aggiornato viene scaricato sull'host, il che è più veloce grazie alle dimensioni ridotte .

Costruire un'immagine con dipendenze estratte in livelli separati

Costruiremo l'immagine finale in due fasi utilizzando un metodo chiamato assemblaggio a più stadi . Nel primo passaggio estrarremo le dipendenze e nel secondo passaggio copieremo le dipendenze estratte nell'immagine finale.

Modifichiamo il nostro Dockerfile per una build in più fasi:

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

Salviamo questa configurazione in un file separato - Dockerfile2.

Costruiamo l'immagine Docker utilizzando il comando:

docker build -f Dockerfile2 -t usersignup:v1 .

Dopo aver eseguito questo comando otteniamo il seguente 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

Possiamo vedere che un'immagine Docker viene creata con un ID immagine e quindi taggata.

Infine, eseguiamo il comando Dive come prima per ispezionare i livelli all'interno dell'immagine Docker generata. Possiamo fornire un ID immagine o un tag come input per il comando Dive:

dive userssignup:v1

Come puoi vedere nell'output, il livello contenente l'applicazione è ora di soli 11 KB e le dipendenze vengono memorizzate nella cache in livelli separati. 

Creazione di immagini Docker ottimizzate per un'applicazione Spring Boot

Estrazione delle dipendenze interne sui singoli livelli

Possiamo ridurre ulteriormente la dimensione del livello dell'applicazione estraendo le nostre dipendenze personalizzate in un livello separato invece di impacchettarle con l'applicazione dichiarandole in ymlfile simile denominato 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 questo fascicolo layers.idxabbiamo aggiunto una dipendenza personalizzata denominata, io.myorgcontenente le dipendenze dell'organizzazione recuperate da un repository condiviso.

conclusione

In questo articolo abbiamo esaminato l'utilizzo dei Buildpack cloud-nativi per creare un'immagine del contenitore direttamente dal codice sorgente. Si tratta di un'alternativa all'utilizzo di Docker per creare un'immagine del contenitore nel solito modo: prima creando un file JAR eseguibile spesso e quindi impacchettandolo in un'immagine del contenitore specificando le istruzioni nel file Docker.

Abbiamo anche esaminato l'ottimizzazione del nostro contenitore abilitando una funzionalità di stratificazione che inserisce le dipendenze in livelli separati memorizzati nella cache dell'host e un sottile strato dell'applicazione viene caricato al momento della pianificazione nei motori di esecuzione del contenitore.

Puoi trovare tutto il codice sorgente utilizzato nell'articolo su Github .

Riferimento al comando

Ecco una rapida carrellata dei comandi che abbiamo utilizzato in questo articolo.

Cancellazione del contesto:

docker system prune -a

Creazione di un'immagine contenitore utilizzando un file Docker:

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

Costruiamo l'immagine del contenitore dal codice sorgente (senza Dockerfile):

mvn spring-boot:build-image

Visualizza i livelli di dipendenza. Prima di creare il file JAR dell'applicazione, assicurati che la funzione di stratificazione sia abilitata in spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Estrazione dei livelli di dipendenza. Prima di creare il file JAR dell'applicazione, assicurati che la funzione di stratificazione sia abilitata in spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Visualizza un elenco di immagini del contenitore

docker images

Visualizza a sinistra all'interno dell'immagine del contenitore (assicurati che lo strumento di immersione sia installato):

dive <image ID or image tag>

Fonte: habr.com