Creació d'imatges Docker optimitzades per a una aplicació Spring Boot

Els contenidors s'han convertit en el mitjà preferit per empaquetar una aplicació amb totes les dependències del seu programari i sistema operatiu i després lliurar-les a diferents entorns.

Aquest article tracta diferents maneres de contenidor una aplicació Spring Boot:

  • creant una imatge Docker mitjançant un fitxer Docker,
  • creant una imatge OCI des de la font mitjançant Cloud-Native Buildpack,
  • i optimització de la imatge en temps d'execució mitjançant la separació de parts del JAR en diferents capes mitjançant eines de diversos nivells.

 Exemple de codi

Aquest article s'acompanya d'un exemple de codi de treball a GitHub .

Terminologia dels contenidors

Començarem amb la terminologia del contenidor utilitzada a l'article:

  • Imatge del contenidor: fitxer d'un format específic. Convertirem la nostra aplicació en una imatge de contenidor executant l'eina de compilació.
  • envàs: una instància executable de la imatge del contenidor.
  • Motor de contenidors: el procés dimoni responsable d'executar el contenidor.
  • Amfitrió del contenidor: l'ordinador amfitrió on s'executa el motor del contenidor.
  • Registre de contenidors: La ubicació general utilitzada per publicar i distribuir la imatge del contenidor.
  • Estàndard OCIIniciativa de contenidors oberts (OCI) és una estructura de govern oberta i lleugera formada dins de la Fundació Linux. L'OCI Image Specification defineix els estàndards de la indústria per a la imatge de contenidor i els formats d'execució per garantir que tots els motors de contenidors puguin executar imatges de contenidors creades per qualsevol eina de creació.

Per contenidoritzar una aplicació, emboliquem la nostra aplicació en una imatge de contenidor i publiquem aquesta imatge en un registre compartit. El temps d'execució del contenidor recupera aquesta imatge del registre, la desempaqueta i executa l'aplicació dins d'ella.

La versió 2.3 de Spring Boot proporciona connectors per crear imatges OCI.

estibador és la implementació de contenidors més utilitzada, i fem servir Docker als nostres exemples, de manera que totes les referències de contenidors posteriors d'aquest article es referiran a Docker.

Construir una imatge de contenidor de la manera tradicional

Crear imatges de Docker per a aplicacions Spring Boot és molt fàcil afegint unes quantes instruccions al fitxer Docker.

Primer creem un fitxer JAR executable i, com a part de les instruccions del fitxer Docker, copiem el fitxer JAR executable a la part superior de la imatge JRE base després d'aplicar la configuració necessària.

Creem la nostra aplicació Spring a Inicialització de primavera amb dependències weblombokи actuator. També estem afegint un controlador de descans per proporcionar una API GETmètode.

Creació d'un Dockerfile

A continuació, contenitzem aquesta aplicació afegint-hi Dockerfile:

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

El nostre fitxer Docker conté una imatge base de adoptopenjdk, a sobre del qual copiem el nostre fitxer JAR i després obrim el port, 8080que escoltarà les peticions.

Construcció de l'aplicació

Primer heu de crear una aplicació amb Maven o Gradle. Aquí fem servir Maven:

mvn clean package

Això crea un fitxer JAR executable per a l'aplicació. Hem de convertir aquest JAR executable en una imatge Docker per executar-lo al motor Docker.

Creació d'una imatge de contenidor

A continuació, posem aquest fitxer JAR executable a la imatge de Docker executant l'ordre docker builddes del directori arrel del projecte que conté el Dockerfile creat anteriorment:

docker build  -t usersignup:v1 .

Podem veure la nostra imatge a la llista mitjançant l'ordre:

docker images 

La sortida de l'ordre anterior inclou la nostra imatge usersignupjuntament amb la imatge base, adoptopenjdkespecificat al nostre fitxer Docker.

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

Visualitza les capes dins d'una imatge de contenidor

Mirem la pila de capes dins de la imatge. Farem servir инструмент  immersió per veure aquestes capes:

dive usersignup:v1

Aquí hi ha part de la sortida de l'ordre Dive: 

Creació d'imatges Docker optimitzades per a una aplicació Spring Boot

Com podem veure, la capa d'aplicació constitueix una part important de la mida de la imatge. Volem reduir la mida d'aquesta capa a les seccions següents com a part de la nostra optimització.

Creació d'una imatge de contenidor amb Buildpack

Paquets de muntatge (Paquets de construcció) és un terme general utilitzat per diverses ofertes de Platform as a Service (PAAS) per crear una imatge de contenidor a partir del codi font. Va ser llançat per Heroku el 2011 i des de llavors ha estat adoptat per Cloud Foundry, Google App Engine, Gitlab, Knative i diversos altres.

Creació d'imatges Docker optimitzades per a una aplicació Spring Boot

L'avantatge dels paquets de compilació al núvol

Un dels principals avantatges d'utilitzar Buildpack per crear imatges és això Els canvis de configuració d'imatge es poden gestionar de manera centralitzada (generador) i propagar-se a totes les aplicacions mitjançant el generador.

Els paquets de compilació estaven estretament connectats a la plataforma. Cloud-Native Buildpacks ofereix estandardització entre plataformes gràcies al format d'imatge OCI, que garanteix que el motor Docker pugui executar la imatge.

Utilitzant el connector Spring Boot

El connector Spring Boot crea imatges OCI des de la font mitjançant Buildpack. Les imatges es creen utilitzant bootBuildImagetasques (Gradle) o spring-boot:build-imageobjectius (Maven) i instal·lació local de Docker.

Podem personalitzar el nom de la imatge necessària per enviar-la al registre de Docker especificant-ne el nom 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>

Utilitzem Maven per fer-ho build-imageobjectius per crear una aplicació i crear una imatge de contenidor. No estem utilitzant cap fitxer Dockerfile en aquest moment.

mvn spring-boot:build-image

El resultat serà una cosa així:

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

A la sortida ho veiem paketo Cloud-Native buildpacks'utilitza per crear una imatge OCI de treball. Com abans, podem veure la imatge llistada com a imatge de Docker executant l'ordre:

docker images 

Conclusió:

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

Creació d'una imatge de contenidor amb Jib

Jib és un connector de creació d'imatges de Google que proporciona un mètode alternatiu per crear una imatge de contenidor a partir del codi font.

Configurant jib-maven-plugina pom.xml:

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

A continuació, executem el connector Jib mitjançant l'ordre Maven per crear l'aplicació i crear una imatge de contenidor. Com abans, no estem utilitzant cap fitxer Docker aquí:

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

Després d'executar l'ordre Maven anterior, obtenim la següent sortida:

[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

La sortida mostra que la imatge del contenidor s'ha creat i col·locat al registre.

Motivacions i tècniques per crear imatges optimitzades

Tenim dues raons principals per a l'optimització:

  • Productivitat: En un sistema d'orquestració de contenidors, es recupera una imatge de contenidor del registre d'imatges a l'amfitrió que executa el motor de contenidors. Aquest procés s'anomena planificació. L'extracció d'imatges grans del registre comporta temps de programació llargs en sistemes d'orquestració de contenidors i temps de construcció llargs en canalitzacions CI.
  • Безопасность: Les imatges més grans també tenen una àrea més gran de vulnerabilitats.

Una imatge de Docker consta d'una pila de capes, cadascuna de les quals representa una instrucció al nostre Dockerfile. Cada capa representa un delta dels canvis de la capa subjacent. Quan traiem una imatge de Docker del registre, s'extreu en capes i s'emmagatzema a la memòria cau a l'amfitrió.

Ús de Spring Boot "FAT JAR" a com a format d'embalatge predeterminat. Quan mirem el JAR gruixut, veiem que l'aplicació constitueix una part molt petita de tot el JAR. Aquesta és la part que canvia més sovint. La resta consisteix en les dependències de Spring Framework.

La fórmula d'optimització se centra en aïllar l'aplicació a un nivell separat de les dependències de Spring Framework.

La capa de dependència, que forma la major part del fitxer JAR gruixut, només es baixa una vegada i es guarda a la memòria cau al sistema amfitrió.

Només s'extreu una capa fina de l'aplicació durant les actualitzacions de l'aplicació i la programació de contenidors. com es mostra en aquest diagrama:

Creació d'imatges Docker optimitzades per a una aplicació Spring Boot

A les seccions següents, veurem com crear aquestes imatges optimitzades per a una aplicació Spring Boot.

Creació d'una imatge de contenidor optimitzada per a una aplicació Spring Boot mitjançant Buildpack

Spring Boot 2.3 admet la superposició extraient parts d'un fitxer JAR gruixut en capes separades. La funció de capes està desactivada de manera predeterminada i s'ha d'habilitar explícitament mitjançant el connector Spring Boot Maven:

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

Utilitzarem aquesta configuració per crear la nostra imatge de contenidor primer amb Buildpack i després amb Docker a les seccions següents.

Llencem build-imageObjectiu de Maven per crear imatges de contenidors:

mvn spring-boot:build-image

Si executem Dive per veure les capes de la imatge resultant, podem veure que la capa de l'aplicació (delineada en vermell) és molt més petita en el rang de kilobytes en comparació amb la que hem obtingut amb el format JAR gruixut:

Creació d'imatges Docker optimitzades per a una aplicació Spring Boot

Creació d'una imatge de contenidor optimitzada per a una aplicació Spring Boot mitjançant Docker

En lloc d'utilitzar un complement Maven o Gradle, també podem crear una imatge JAR de Docker en capes amb un fitxer Docker.

Quan fem servir Docker, hem de realitzar dos passos addicionals per extreure les capes i copiar-les a la imatge final.

El contingut del JAR resultant després de la construcció utilitzant Maven amb la capa activada serà així:

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

La sortida mostra un JAR addicional anomenat spring-boot-jarmode-layertoolsи layersfle.idxdossier. Aquest fitxer JAR addicional proporciona capacitats de processament en capes, tal com es descriu a la secció següent.

Extracció de dependències de capes individuals

Per veure i extreure capes del nostre JAR en capes, utilitzem la propietat del sistema -Djarmode=layertoolsper començar spring-boot-jarmode-layertoolsJAR en lloc de l'aplicació:

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

L'execució d'aquesta ordre produeix una sortida que conté les opcions d'ordres disponibles:

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

La sortida mostra les ordres listextractи helpс helpsigui el predeterminat. Executem l'ordre amb listopció:

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

Veiem una llista de dependències que es poden afegir com a capes.

Capes per defecte:

Nom de la capa

Contingut

dependencies

qualsevol dependència la versió de la qual no conté SNAPSHOT

spring-boot-loader

Classes de càrrega JAR

snapshot-dependencies

qualsevol dependència la versió de la qual contingui SNAPSHOT

application

classes d'aplicació i recursos

Les capes es defineixen a layers.idxfitxer en l'ordre en què s'han d'afegir a la imatge de Docker. Aquestes capes s'emmagatzemen a la memòria cau a l'amfitrió després de la primera recuperació perquè no canvien. Només es baixa a l'amfitrió la capa d'aplicació actualitzada, que és més ràpida a causa de la mida reduïda .

Construir una imatge amb dependències extretes en capes separades

Construirem la imatge final en dues etapes utilitzant un mètode anomenat muntatge en diverses etapes . En el primer pas extreurem les dependències i en el segon pas copiarem les dependències extretes a la imatge final.

Modifiquem el nostre Dockerfile per a una construcció en diverses etapes:

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

Desem aquesta configuració en un fitxer separat - Dockerfile2.

Construïm la imatge de Docker mitjançant l'ordre:

docker build -f Dockerfile2 -t usersignup:v1 .

Després d'executar aquesta comanda, obtenim la següent sortida:

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

Podem veure que es crea una imatge de Docker amb un ID d'imatge i després s'etiqueta.

Finalment, executem l'ordre Dive com abans per inspeccionar les capes dins de la imatge Docker generada. Podem proporcionar una identificació d'imatge o una etiqueta com a entrada a l'ordre d'immersió:

dive userssignup:v1

Com podeu veure a la sortida, la capa que conté l'aplicació ara només té 11 KB i les dependències s'emmagatzemen a la memòria cau en capes separades. 

Creació d'imatges Docker optimitzades per a una aplicació Spring Boot

Extracció de dependències internes de capes individuals

Podem reduir encara més la mida del nivell d'aplicació extraient qualsevol de les nostres dependències personalitzades en un nivell separat en lloc d'empaquetar-les amb l'aplicació declarant-les a ymlarxiu similar anomenat 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/"

En aquest fitxer layers.idxhem afegit una dependència personalitzada anomenada, io.myorgque conté dependències de l'organització recuperades d'un repositori compartit.

Sortida

En aquest article, vam analitzar l'ús de Cloud-Native Buildpacks per crear una imatge de contenidor directament des del codi font. Aquesta és una alternativa a l'ús de Docker per crear una imatge de contenidor de la manera habitual: primer creant un fitxer JAR executable gruixut i després empaquetant-lo en una imatge de contenidor especificant instruccions al fitxer Docker.

També vam analitzar l'optimització del nostre contenidor habilitant una funció de capes que agrupa les dependències en capes separades que s'emmagatzemen a la memòria cau a l'amfitrió i es carrega una capa fina de l'aplicació en el moment de la programació als motors d'execució del contenidor.

Podeu trobar tot el codi font utilitzat a l'article a Github .

Referència de comanda

Aquí teniu un resum ràpid de les ordres que hem utilitzat en aquest article.

Neteja de context:

docker system prune -a

Creació d'una imatge de contenidor mitjançant un fitxer Docker:

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

Construïm la imatge del contenidor a partir del codi font (sense Dockerfile):

mvn spring-boot:build-image

Visualitza les capes de dependència. Abans de crear el fitxer JAR de l'aplicació, assegureu-vos que la funció de capes estigui habilitada a spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Extracció de capes de dependència. Abans de crear el fitxer JAR de l'aplicació, assegureu-vos que la funció de capes estigui habilitada a spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Veure una llista d'imatges de contenidors

docker images

Veure a l'esquerra dins de la imatge del contenidor (assegureu-vos que l'eina de busseig estigui instal·lada):

dive <image ID or image tag>

Font: www.habr.com