Creazione di immagini Docker ottimizzate per l'applicazione Spring Boot

I contenitori sono diventati il mezzo preferito per imballare le applicazioni con tutte le dipendenze del software e del sistema operativo, e poi per consegnarle a diversi ambienti.

In questo articolo vengono considerati vari metodi di containerizzazione dell'applicazione Spring Boot:

  • creazione di un'immagine Docker utilizzando un file Docker,
  • creazione di un'immagine OCI dal codice sorgente con Cloud-Native Buildpack,
  • e ottimizzazione dell'immagine durante l'esecuzione suddividendo parti del JAR in diversi livelli usando strumenti multistrato.

 Esempio di codice

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

Terminologia dei contenitori

Iniziamo con la terminologia dei contenitori utilizzata nell'articolo:

  • Immagine del contenitore (Container image): un file di un certo formato. Convertiamo la nostra applicazione in un'immagine del contenitore eseguendo uno strumento di compilazione.
  • Contenitore: un'istanza eseguibile dell'immagine del contenitore.
  • Motore del contenitore (Container engine): un processo demone responsabile dell'esecuzione del contenitore.
  • Host del contenitore (Container host): il computer host su cui opera il motore del contenitore.
  • Registro dei contenitori (Container registry): un'ubicazione comune utilizzata per pubblicare e distribuire l'immagine del contenitore.
  • Standard OCIOpen Container Initiative (OCI) è una struttura di governance aperta e leggera, formata all'interno della Linux Foundation. La specifica dell'immagine OCI definisce standard settoriali per i formati delle immagini dei contenitori e gli ambienti di esecuzione, garantendo che tutti i motori dei contenitori possano eseguire immagini dei contenitori generate da qualsiasi strumento di compilazione.

Per inserire l'applicazione in un contenitore, racchiudiamo la nostra applicazione in un'immagine del contenitore e pubblichiamo quest'immagine in un registro comune. L'ambiente di esecuzione del contenitore estrae quest'immagine dal registro, la decomprime e avvia l'applicazione al suo interno.

La versione 2.3 di Spring Boot offre plugin per creare immagini OCI.

Docker è l'implementazione di contenitore più comunemente utilizzata, e noi utilizziamo Docker nei nostri esempi, quindi tutti i seguenti riferimenti al contenitore in questo articolo si riferiranno a Docker.

Creazione dell'immagine del contenitore nel modo tradizionale

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

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

Creiamo la nostra applicazione Spring su Spring Initializr con dipendenze weblombokactuator. Aggiungiamo anche un controller rest per fornire API con e username/password: admin/admin.metodo.

Creazione del file Docker

Poi mettiamo questa applicazione nel contenitore, 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 la quale copriamo il nostro file JAR e poi apriamo la porta, 8080che ascolterà le richieste.

Costruzione dell'applicazione

Per prima cosa, dobbiamo creare l'applicazione utilizzando Maven o Gradle. Qui utilizziamo Maven:

mvn clean package

Questo genera un file JAR eseguibile dell'applicazione. Dobbiamo convertire questo file JAR eseguibile in un'immagine Docker per l'esecuzione nel motore Docker.

Creazione dell'immagine del contenitore

Dopo mettiamo questo file JAR eseguibile nell'immagine Docker eseguendo il comando docker builddalla radice del progetto contenente il file Docker creato in precedenza:

docker build  -t usersignup:v1 .

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

docker images 

Il risultato dell'esecuzione del comando sopra include la nostra immagine usersignupinsieme all'immagine di base, adoptopenjdk, specificata nel nostro file Docker.

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

Visualizzazione degli strati all'interno dell'immagine del contenitore

Diamo un'occhiata alla pila di strati all'interno dell'immagine. Useremo strumento  dive, per visualizzare questi strati:

dive usersignup:v1

Ecco parte dei risultati dell'esecuzione del comando Dive: 

Creazione di immagini Docker ottimizzate per l'applicazione Spring Boot

Come possiamo vedere, il livello dell'applicazione rappresenta una parte significativa della dimensione dell'immagine. Vogliamo ridurre la dimensione di questo strato nelle sezioni successive nell'ambito della nostra ottimizzazione.

Creazione dell'immagine del contenitore utilizzando Buildpack

Buildpack (I Buildpack) sono un termine generico utilizzato da varie offerte di "Piattaforma come servizio" (PAAS) 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 altri.

Creazione di immagini Docker ottimizzate per l'applicazione Spring Boot

Il vantaggio dei buildpack nel cloud

Uno dei principali vantaggi dell'utilizzare i Buildpack per creare immagini è che Le modifiche alla configurazione dell'immagine possono essere gestite centralmente (builder) e distribuite a tutte le applicazioni che utilizza il builder.

I pacchetti di build erano strettamente legati alla piattaforma. I Cloud-Native Buildpacks offrono standardizzazione tra le piattaforme, supportando il formato immagine OCI, che garantisce che l'immagine possa essere eseguita dal motore Docker.

Utilizzo del plugin Spring Boot

Il plugin Spring Boot crea immagini OCI dal codice sorgente utilizzando Buildpack. Le immagini vengono create utilizzando bootBuildImagetask (Gradle) oppure spring-boot:build-imagegoal (Maven) e l'installazione locale di Docker.

Possiamo configurare il nome dell'immagine necessaria per l'invio al 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>

Utilizziamo Maven per eseguire la build-imagegoal per creare l'applicazione e l'immagine del contenitore. Attualmente non stiamo utilizzando alcun file Docker.

mvn spring-boot:build-image

Il risultato sarà più o meno il seguente:

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Costruzione dell'immagine 'docker.io\/pratikdas\/usersignup:v1'
[INFO] 
[INFO]  > Estrazione dell'immagine builder 'gcr.io\/paketo-buildpacks\/builder:base-platform-api-0.3' 0%
.
.
.. [creatore]     Aggiunta dell'etichetta 'org.springframework.boot.version'
.. [creatore]     *** Immagini (c311fe74ec73):
.. [creatore]           docker.io\/pratikdas\/usersignup:v1
[INFO] 
[INFO] Immagine 'docker.io\/pratikdas\/usersignup:v1' costruita con successo

Dai risultati, vediamo che il paketo Cloud-Native buildpackviene utilizzato per costruire un'immagine OCI funzionante. Come prima, possiamo vedere l'immagine, indicata 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 dell'immagine del contenitore con Jib

Jib è un plugin per la creazione di immagini di Google che fornisce un metodo alternativo per costruire un'immagine del contenitore dal codice sorgente.

Configuriamo jib-maven-pluginnel pom.xml:

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

Successivamente, eseguiamo il plugin Jib utilizzando il comando Maven per costruire l'applicazione e creare l'immagine del contenitore. Come prima, qui non stiamo utilizzando alcun file Docker:

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

Dopo aver eseguito il comando Maven sopra indicato, otteniamo il seguente output:

[INFO] Contenitore dell'applicazione in fase di creazione per pratikdas/usersignup:v1...
.
.
[INFO] Punto di ingresso del contenitore impostato su [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO] 
[INFO] Immagine costruita e inviata come pratikdas/usersignup:v1
[INFO] Esecuzione delle attività:
[INFO] [==============================] 100,0% completato

I dati di output mostrano che l'immagine del contenitore è stata creata e posizionata nel registro.

Motivazioni e metodi per la creazione di immagini ottimizzate

Abbiamo due principali motivi per ottimizzare:

  • Prestazioni: nel sistema di orchestrazione dei contenitori, l'immagine del contenitore viene estratta dal registro delle immagini sull'host in cui è in esecuzione il motore del contenitore. Questo processo è chiamato pianificazione. L'estrazione di immagini di grandi dimensioni dal registro porta a tempi di pianificazione prolungati nei sistemi di orchestrazione dei contenitori e a tempi di build lunghi nei pipeline CI.
  • Sicurezza: immagini di grandi dimensioni hanno anche una superficie maggiore per vulnerabilità.

Un'immagine Docker è composta da uno stack di livelli, ognuno dei quali rappresenta un'istruzione nel nostro Dockerfile. Ogni livello rappresenta una delta delle modifiche rispetto al livello sottostante. Quando estraiamo un'immagine Docker dal registro, essa viene estratta a livelli e messa in cache sull'host.

Spring Boot utilizza un "fat JAR" come formato di imballaggio predefinito. Quando esaminiamo il fat JAR, vediamo che l'applicazione rappresenta una parte molto piccola dell'intero JAR. Questa è la parte che cambia più frequentemente. Il resto è composto dalle dipendenze del Spring Framework. La formula di ottimizzazione si concentra sull'isolamento dell'applicazione su un livello separato dalle dipendenze del Spring Framework.

Il livello delle dipendenze, che forma la parte principale del fat JAR, viene caricato solo una volta e messo in cache nel sistema host.

Solo il sottile livello dell'applicazione viene estratto durante gli aggiornamenti dell'applicazione e la pianificazione dei contenitori,

come mostrato in questo diagramma: Nelle sezioni seguenti vedremo come creare queste immagini ottimizzate per un'applicazione Spring Boot.

Creazione di immagini Docker ottimizzate per l'applicazione Spring Boot

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

Spring Boot 2.3 supporta la multi-layering estraendo parti del fat JAR in livelli separati. La funzione di layering è disabilitata per impostazione predefinita e deve essere esplicitamente abilitata tramite il plugin Spring Boot Maven:

Spring Boot 2.3 supporta la multilivello estraendo parti di un file JAR pesante in strati separati. La funzione di overlay è disabilitata per impostazione predefinita e deve essere attivata esplicitamente utilizzando il plugin Spring Boot Maven:

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

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

Avviamo build-imagel'obiettivo di Maven per creare l'immagine del contenitore:

mvn spring-boot:build-image

Se avviamo Dive per vedere i layer nell'immagine risultante, noteremo che il layer dell'applicazione (evidenziato in rosso) è molto più piccolo, nell'ordine di kilobyte, rispetto a quello che abbiamo ottenuto utilizzando il formato JAR pesante:

Creazione di immagini Docker ottimizzate per l'applicazione Spring Boot

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

Invece di utilizzare il plugin Maven o Gradle, possiamo anche creare un'immagine JAR Docker multi-layer con un file Docker.

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

Il contenuto del JAR risultante dopo la creazione con Maven con la funzionalità di layering 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

Nell'output viene visualizzato un JAR aggiuntivo di nome spring-boot-jarmode-layertoolslayersfle.idxfile. Questo file JAR aggiuntivo fornisce la funzionalità di gestione multi-layer, come descritto nella sezione successiva.

Estrazione delle dipendenze su layer separati

Per visualizzare ed estrarre i layer dal nostro JAR multi-layer, utilizziamo la proprietà di sistema -Djarmode=layertoolsper eseguire spring-boot-jarmode-layertoolsJAR anziché l'applicazione:

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

Eseguendo questo comando otteniamo un output contenente i parametri di comando disponibili:

Usage:
  java -Djarmode=layertools -jar usersignup-0.0.1-SNAPSHOT.jar

Available commands:
  list     Elenca i layer dal jar che possono essere estratti
  extract  Estrae i layer dal jar per la creazione dell'immagine
  help     Aiuto su qualsiasi comando

L'output mostra i comandi listextracthelpcon helpessere per impostazione predefinita. Eseguiamo il comando con listl'opzione:

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

Vediamo un elenco delle dipendenze che possono essere aggiunte come layer.

Layer di default:

Nome del layer

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 delle applicazioni e risorse

I livelli sono definiti in layers.idxun file nell'ordine in cui devono essere aggiunti all'immagine Docker. Questi livelli vengono memorizzati nella cache sull'host dopo il primo download, poiché non vengono modificati. Solo il livello aggiornato dell'applicazione viene caricato sull'host, il che avviene più rapidamente a causa delle dimensioni ridotte. .

Costruendo l'immagine con le dipendenze estratte in livelli separati.

Costruiremo l'immagine finale in due fasi, utilizzando un metodo chiamato compilazione multi-stage . Nella prima fase estrarremo le dipendenze, e nella seconda fase copieremo le dipendenze estratte nell'immagine finale.

Modifichiamo il nostro file Docker per la compilazione multi-stage:

# 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 con il comando:

docker build -f Dockerfile2 -t usersignup:v1 .

Dopo aver eseguito questo comando, otteniamo l'output seguente:

Inviando il contesto di build al demone Docker  20.41MB
Passo 1/12 : DA adoptopenjdk:14-jre-hotspot come builder
14-jre-hotspot: Downloading from library/adoptopenjdk
.
.
Costruito con successo a9ebf6970841
Etichettato con successo userssignup:v1

Vediamo che l'immagine Docker viene creata con un identificatore dell'immagine e poi viene etichettata.

Infine, eseguiamo il comando Dive, come prima, per controllare i livelli all'interno dell'immagine Docker generata. Possiamo specificare l'identificativo dell'immagine o il tag come input per il comando Dive:

dive userssignup:v1

Come si può vedere dall'output, il livello contenente l'applicazione ora occupa solo 11 KB, mentre le dipendenze sono memorizzate nella cache in livelli separati. 

Creazione di immagini Docker ottimizzate per l'applicazione Spring Boot

Estrazione delle dipendenze interne in livelli separati.

Possiamo ulteriormente ridurre le dimensioni del livello dell'applicazione estraendo qualsiasi dipendenza personalizzata in un livello separato, anziché imballarle insieme all'applicazione, dichiarandole in ymlun file simile chiamato 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 file layers.idxabbiamo aggiunto una dipendenza personalizzata di nome, io.myorgcontenente le dipendenze dell'organizzazione, ottenute da un repository comune.

Conclusione

In questo articolo abbiamo esaminato l'uso di Cloud-Native Buildpacks per creare un'immagine del contenitore direttamente dal codice sorgente. Questa è un'alternativa all'uso di Docker per creare un'immagine del contenitore in modo tradizionale: prima si crea un file JAR eseguibile e poi lo si imballa in un'immagine del contenitore specificando le istruzioni nel file Docker.

Abbiamo anche visto come ottimizzare il nostro contenitore includendo la funzionalità di sovrapposizione, che estrae le dipendenze in singoli livelli memorizzati nella cache sull'host, mentre lo strato sottile dell'applicazione viene caricato durante il scheduling nei meccanismi di esecuzione del contenitore.

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

Guida ai comandi

Ecco un riepilogo dei comandi che abbiamo utilizzato in questo articolo per una rapida consultazione.

Pulizia del contesto:

docker system prune -a

Creazione dell'immagine del contenitore utilizzando il file Docker:

docker build -f  -t  .

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

mvn spring-boot:build-image

Visualizzazione dei livelli delle dipendenze. Prima di costruire il file JAR dell'applicazione, assicurati che la funzionalità di sovrapposizione sia abilitata nel spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Estrazione dei livelli delle dipendenze. Prima di costruire il file JAR dell'applicazione, assicurati che la funzionalità di sovrapposizione sia abilitata nel spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Visualizzazione dell'elenco delle immagini del contenitore

docker images

Visualizzazione dei layer all'interno dell'immagine del contenitore (assicurati che l'outil dive sia installato):

dive

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster