Creación de imágenes de Docker optimizadas para una aplicación Spring Boot

Los contenedores se han convertido en el medio preferido para empaquetar una aplicación con todas sus dependencias de software y sistema operativo y luego entregarlas a diferentes entornos.

Este artículo cubre diferentes formas de contenerizar una aplicación Spring Boot:

  • construir una imagen de docker usando un dockerfile,
  • construyendo una imagen OCI desde la fuente usando Cloud-Native Buildpack,
  • y optimización de imágenes en tiempo de ejecución mediante la separación de partes JAR en diferentes niveles utilizando herramientas en capas.

 Ejemplo de código

Este artículo va acompañado de un ejemplo de código de trabajo en GitHub .

Terminología de contenedores

Comenzaremos con la terminología de contenedor utilizada a lo largo del artículo:

  • Imagen del contenedor: un archivo de un formato específico. Convertimos nuestra aplicación en una imagen de contenedor ejecutando la herramienta de compilación.
  • envase: una instancia ejecutable de la imagen del contenedor.
  • Motor de contenedores: El proceso daemon responsable de ejecutar el contenedor.
  • anfitrión del contenedor: la máquina host en la que se ejecuta el motor del contenedor.
  • Registro de contenedores: la ubicación general utilizada para publicar y distribuir la imagen del contenedor.
  • estándar OCIIniciativa de contenedor abierto (OCI) es un marco de gestión ligero y de código abierto formado por la Fundación Linux. La especificación de imagen OCI define los estándares de la industria para los formatos de imagen de contenedor y el tiempo de ejecución para garantizar que todos los motores de contenedor puedan ejecutar imágenes de contenedor creadas por cualquier herramienta de compilación.

Para contenerizar una aplicación, envolvemos nuestra aplicación en una imagen de contenedor y publicamos esa imagen en el registro público. El tiempo de ejecución del contenedor recupera esta imagen del registro, la descomprime y ejecuta la aplicación que contiene.

La versión 2.3 de Spring Boot proporciona complementos para crear imágenes OCI.

Docker es la implementación de contenedor más utilizada y usamos Docker en nuestros ejemplos, por lo que todas las referencias de contenedor posteriores en este artículo se referirán a Docker.

Construyendo una imagen de contenedor de la manera tradicional

Crear imágenes de Docker para aplicaciones Spring Boot es muy fácil al agregar algunas instrucciones a su Dockerfile.

Primero creamos un archivo JAR ejecutable y, como parte de las instrucciones de Dockerfile, copiamos el archivo JAR ejecutable sobre la imagen base de JRE después de aplicar las personalizaciones necesarias.

Vamos a crear nuestra aplicación Spring en Primavera Initializr con dependencias weblombokи actuator. También agregamos un controlador de descanso para proporcionar una API con GETmétodo.

Creación de un archivo Docker

Luego colocamos esta aplicación en un contenedor agregando Dockerfile:

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

Nuestro Dockerfile contiene una imagen base, de adoptopenjdk, encima de lo cual copiamos nuestro archivo JAR y luego abrimos el puerto, 8080que escuchará las solicitudes.

Ensamblaje de aplicaciones

Primero debe crear una aplicación usando Maven o Gradle. Aquí estamos usando Maven:

mvn clean package

Esto crea un archivo JAR ejecutable para la aplicación. Necesitamos convertir este archivo JAR ejecutable en una imagen de Docker para que se ejecute en el motor de Docker.

Crear una imagen de contenedor

Luego colocamos este ejecutable JAR en la imagen de Docker ejecutando el comando docker builddesde el directorio raíz del proyecto que contiene el Dockerfile creado anteriormente:

docker build  -t usersignup:v1 .

Podemos ver nuestra imagen en la lista con el comando:

docker images 

La salida del comando anterior incluye nuestra imagen usersignupjunto con la imagen base, adoptopenjdkespecificado en nuestro Dockerfile.

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

Ver capas dentro de una imagen de contenedor

Miremos la pila de capas dentro de la imagen. Usaremos инструмент  bucear, para ver estas capas:

dive usersignup:v1

Aquí hay parte de la salida del comando Dive: 

Creación de imágenes de Docker optimizadas para una aplicación Spring Boot

Como podemos ver, la capa de aplicación constituye una parte importante del tamaño de la imagen. Queremos reducir el tamaño de esta capa en las siguientes secciones como parte de nuestra optimización.

Creación de una imagen de contenedor con Buildpack

Paquetes de montaje (Paquetes de compilación) es un término genérico utilizado por varias ofertas de Plataforma como servicio (PAAS) para crear una imagen de contenedor a partir del código fuente. Fue lanzado por Heroku en 2011 y desde entonces ha sido adoptado por Cloud Foundry, Google App Engine, Gitlab, Knative y algunos otros.

Creación de imágenes de Docker optimizadas para una aplicación Spring Boot

Ventaja de los paquetes de compilación en la nube

Uno de los principales beneficios de usar Buildpack para crear imágenes es que Los cambios de configuración de imagen se pueden administrar de forma centralizada (constructor) y propagarse a todas las aplicaciones que utilizan el generador.

Los paquetes de compilación estaban estrechamente vinculados a la plataforma. Los paquetes de compilación nativos de la nube brindan estandarización en todas las plataformas al admitir el formato de imagen OCI, lo que garantiza que el motor Docker pueda ejecutar la imagen.

Uso del complemento Spring Boot

El complemento Spring Boot crea imágenes OCI desde la fuente utilizando Buildpack. Las imágenes se crean usando bootBuildImagetareas (Gradle) o spring-boot:build-imagedestino (Maven) e instalación local de Docker.

Podemos personalizar el nombre de la imagen que necesitamos enviar al registro de Docker especificando el nombre en 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>

Usemos Maven para ejecutar build-imageobjetivos para crear una aplicación y crear una imagen de contenedor. Actualmente no estamos usando ningún Dockerfile.

mvn spring-boot:build-image

El resultado será algo como esto:

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

De la salida, vemos que paketo Cloud-Native buildpackse utiliza para crear una imagen OCI de trabajo. Como antes, podemos ver la imagen listada como una imagen de Docker ejecutando el comando:

docker images 

Conclusión:

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

Creación de una imagen de contenedor con Jib

Jib es un complemento de creación de imágenes de Google que proporciona un método alternativo para crear una imagen de contenedor desde la fuente.

Configurando jib-maven-pluginen pom.xml:

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

A continuación, ejecutamos el complemento Jib con el comando Maven para compilar la aplicación y crear la imagen del contenedor. Como antes, no estamos usando ningún Dockerfile aquí:

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

Después de ejecutar el comando Maven anterior, obtenemos el siguiente resultado:

[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

El resultado muestra que la imagen del contenedor se ha creado y colocado en el registro.

Motivaciones y métodos para crear imágenes optimizadas

Tenemos dos razones principales para optimizar:

  • Rendimiento: en un sistema de orquestación de contenedores, una imagen de contenedor se extrae del registro de imágenes al host que ejecuta el motor de contenedor. Este proceso se llama planificación. Extraer imágenes grandes del registro da como resultado largos tiempos de programación en los sistemas de orquestación de contenedores y largos tiempos de construcción en las canalizaciones de CI.
  • seguridad: las imágenes grandes también tienen una gran área de vulnerabilidades.

Una imagen de Docker se compone de una pila de capas, cada una de las cuales representa una declaración en nuestro Dockerfile. Cada capa representa el delta de cambios en la capa subyacente. Cuando extraemos una imagen de Docker del registro, se extrae en capas y se almacena en caché en el host.

Usos de Spring Boot "TARRO gordo" en como formato de empaquetado predeterminado. Cuando miramos un JAR gordo, vemos que la aplicación es una parte muy pequeña de todo el JAR. Esta es la parte que más cambia. El resto consiste en dependencias de Spring Framework.

La fórmula de optimización se centra en aislar la aplicación en un nivel separado de las dependencias de Spring Framework.

La capa de dependencia que forma la mayor parte del archivo JAR grueso se descarga solo una vez y se almacena en caché en el sistema host.

Solo se extrae una capa delgada de la aplicación durante las actualizaciones de la aplicación y la programación del contenedor, como se muestra en este diagrama:

Creación de imágenes de Docker optimizadas para una aplicación Spring Boot

En las siguientes secciones, veremos cómo crear estas imágenes optimizadas para una aplicación Spring Boot.

Creación de una imagen de contenedor optimizada para una aplicación Spring Boot con Buildpack

Spring Boot 2.3 admite capas al extraer partes de un archivo JAR grueso en capas separadas. La función de capas está deshabilitada de forma predeterminada y debe habilitarse explícitamente mediante el complemento Spring Boot Maven:

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

Usaremos esta configuración para construir nuestra imagen de contenedor primero con Buildpack y luego con Docker en las siguientes secciones.

Corramos build-imageObjetivo de Maven para crear una imagen de contenedor:

mvn spring-boot:build-image

Si ejecutamos Dive para ver las capas en la imagen resultante, podemos ver que la capa de la aplicación (encerrada en un círculo rojo) es mucho más pequeña en el rango de kilobytes en comparación con lo que obtuvimos usando el formato JAR grueso:

Creación de imágenes de Docker optimizadas para una aplicación Spring Boot

Creación de una imagen de contenedor optimizada para una aplicación Spring Boot con Docker

En lugar de usar un complemento Maven o Gradle, también podemos crear una imagen JAR de Docker en capas con un archivo Docker.

Cuando usamos Docker, debemos realizar dos pasos adicionales para extraer las capas y copiarlas en la imagen final.

El contenido del JAR resultante después de construir con Maven con capas habilitadas se verá así:

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 salida muestra un JAR adicional llamado spring-boot-jarmode-layertoolsи layersfle.idxarchivo. Este archivo JAR adicional proporciona capacidades de capas, como se describe en la siguiente sección.

Extraer dependencias en capas separadas

Para ver y extraer capas de nuestro JAR en capas, usamos la propiedad del sistema -Djarmode=layertoolscorrer spring-boot-jarmode-layertoolsJAR en lugar de aplicación:

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

Ejecutar este comando produce una salida que contiene las opciones de comando 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 salida muestra los comandos. listextractи helpс helpser el predeterminado. Ejecutemos el comando con listopción:

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

Vemos una lista de dependencias que se pueden agregar como capas.

Capas por defecto:

Nombre de la capa

contenido

dependencies

cualquier dependencia cuya versión no contenga SNAPSHOT

spring-boot-loader

Clases de cargador JAR

snapshot-dependencies

cualquier dependencia cuya versión contenga SNAPSHOT

application

clases de aplicación y recursos

Las capas se definen en layers.idxarchivo en el orden en que deben agregarse a la imagen de Docker. Estas capas se almacenan en caché en el host después de la primera búsqueda porque no cambian. Solo se descarga en el host la capa de aplicación actualizada, que es más rápida debido al tamaño reducido .

Construyendo una imagen con dependencias extraídas en capas separadas

Construiremos la imagen final en dos pasos usando un método llamado montaje en varias etapas . En el primer paso extraeremos las dependencias y en el segundo paso copiaremos las dependencias extraídas en el archivo final.

Modifiquemos nuestro Dockerfile para una compilación de varias etapas:

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

Guardamos esta configuración en un archivo separado: Dockerfile2.

Construimos la imagen de Docker usando el comando:

docker build -f Dockerfile2 -t usersignup:v1 .

Después de ejecutar este comando, obtenemos el siguiente resultado:

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

Podemos ver que la imagen de Docker se crea con una ID de imagen y luego se etiqueta.

Finalmente, ejecutamos el comando Inmersión como antes para verificar las capas dentro de la imagen de Docker generada. Podemos proporcionar una identificación de imagen o una etiqueta como entrada para el comando Dive:

dive userssignup:v1

Como puede ver en el resultado, la capa que contiene la aplicación ahora tiene solo 11 KB y las dependencias se almacenan en caché en capas separadas. 

Creación de imágenes de Docker optimizadas para una aplicación Spring Boot

Extraer dependencias internas en capas separadas

Podemos reducir aún más el tamaño de la capa de la aplicación extrayendo cualquiera de nuestras dependencias personalizadas en una capa separada en lugar de empaquetarlas con la aplicación declarándolas en ymlarchivo similar llamado 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 este archivo layers.idxhemos agregado una dependencia personalizada llamada, io.myorgque contiene dependencias de la organización recuperadas del repositorio compartido.

conclusión

En este artículo, analizamos el uso de Cloud-Native Buildpacks para crear una imagen de contenedor directamente desde la fuente. Esta es una alternativa al uso de Docker para crear una imagen de contenedor de la manera habitual: primero, se crea un archivo JAR ejecutable grueso y luego se empaqueta en una imagen de contenedor especificando las instrucciones en el Dockerfile.

También analizamos la optimización de nuestro contenedor al incluir una función de capas que extrae las dependencias en capas separadas que se almacenan en caché en el host y una capa de aplicación delgada se carga en el momento de la programación en los motores de ejecución del contenedor.

Puede encontrar todo el código fuente utilizado en el artículo en Github .

Referencia de comandos

Aquí hay un resumen de los comandos que usamos en este artículo para una referencia rápida.

Limpieza de contexto:

docker system prune -a

Construyendo una imagen de contenedor con un Dockerfile:

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

Cree una imagen de contenedor desde la fuente (sin Dockerfile):

mvn spring-boot:build-image

Ver capas de dependencia. Antes de crear el archivo jar de la aplicación, asegúrese de que la función de capas esté habilitada en el complemento spring-boot-maven:

java -Djarmode=layertools -jar application.jar list

Extraer capas de dependencia. Antes de crear el archivo jar de la aplicación, asegúrese de que la función de capas esté habilitada en el complemento spring-boot-maven:

 java -Djarmode=layertools -jar application.jar extract

Visualización de una lista de imágenes de contenedores

docker images

Ver a la izquierda dentro de la imagen del contenedor (asegúrese de que la herramienta de buceo esté instalada):

dive <image ID or image tag>

Fuente: habr.com