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

Los contenedores se han convertido en el medio preferido para empaquetar aplicaciones con todas sus dependencias de software y del sistema operativo, y luego entregarlas en diferentes entornos.

En este artículo se examinan diversas formas de contenerizar una aplicación Spring Boot:

  • creación de una imagen Docker a través de un archivo Docker,
  • creación de una imagen OCI a partir del código fuente utilizando Cloud-Native Buildpack,
  • y optimización de la imagen en tiempo de ejecución dividiendo partes del JAR en diferentes capas utilizando herramientas de múltiples capas.

 Ejemplo de código

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

Terminología de contenedores

Comenzaremos con la terminología de contenedores utilizada en el artículo:

  • Imagen de contenedor (Container image): archivo de un formato específico. Convertimos nuestra aplicación en una imagen de contenedor ejecutando una herramienta de construcción.
  • Contenedor: instancia ejecutable de una imagen de contenedor.
  • Motor de contenedor (Container engine): proceso demonio responsable de ejecutar el contenedor.
  • Host de contenedor (Container host): computadora anfitriona en la que se está ejecutando el motor de contenedor.
  • Registro de contenedores (Container registry): ubicación común utilizada para publicar y distribuir imágenes de contenedores.
  • Estándar OCI: Open Container Initiative (OCI) — es una estructura de gestión abierta y ligera, formada bajo la Linux Foundation. La especificación de imágenes OCI define estándares de la industria para los formatos de imágenes de contenedores y entornos de ejecución, asegurando que todos los motores de contenedores puedan ejecutar imágenes de contenedores creadas por cualquier herramienta de construcción.

Para colocar una aplicación en un contenedor, encerramos nuestra aplicación en una imagen de contenedor y publicamos esta imagen en un registro común. El entorno de ejecución del contenedor extrae esta imagen del registro, la descomprime y ejecuta la aplicación dentro de ella.

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 empleamos Docker en nuestros ejemplos, por lo que todas las referencias posteriores al contenedor en este artículo se referirán a Docker.

Construcción de imágenes de contenedor de la manera tradicional

Es muy fácil crear imágenes Docker para aplicaciones Spring Boot añadiendo algunas instrucciones en el archivo Docker.

Primero, creamos un archivo JAR ejecutable y, como parte de las instrucciones del archivo Docker, copiamos el archivo JAR ejecutable sobre la imagen base de JRE después de aplicar la configuración necesaria.

Creemos nuestra aplicación Spring en Spring Initializr con dependencias web, lomboky actuator. También añadimos un controlador REST para proporcionar una API con GETmétodo.

Creando el archivo Docker

Luego, colocamos esta aplicación en un contenedor, añadiendo Dockerfile:

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

Nuestro archivo Docker contiene la imagen base a partir de adoptopenjdk, sobre la cual copiamos nuestro archivo JAR, y luego abrimos el puerto, 8080que estará escuchando solicitudes.

Construyendo la aplicación

Primero, necesitamos crear la aplicación utilizando Maven o Gradle. Aquí usamos Maven:

mvn clean package

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

Creando la imagen del contenedor

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

docker build -t usersignup:v1 .

Podemos ver nuestra imagen en la lista utilizando el comando:

docker images 

El resultado de la ejecución del comando anterior incluye nuestra imagen usersignupjunto con la imagen base, adoptopenjdk, especificada en nuestro archivo Docker.

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

Visualizando las capas dentro de la imagen del contenedor

Veamos la pila de capas dentro de la imagen. Usaremos herramienta  dive, para ver estas capas:

dive usersignup:v1

Aquí está parte de los resultados de la ejecución del comando Dive: 

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

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

Creando la imagen del contenedor con Buildpack

Buildpacks (Buildpacks) es un término general utilizado por varias ofertas de 'Plataforma como Servicio' (PAAS) para crear una imagen de contenedor a partir de código fuente. Fue lanzado por Heroku en 2011 y desde entonces ha sido adoptado por Cloud Foundry, Google App Engine, Gitlab, Knative y otros.

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

La ventaja de los Buildpacks en la nube

Una de las principales ventajas de usar Buildpacks para crear imágenes es que Los cambios en la configuración de la imagen se pueden gestionar de forma centralizada (builder) y distribuirse a todas las aplicaciones que utilizan builder.

Los paquetes de construcción estaban estrechamente relacionados con la plataforma. Cloud-Native Buildpacks proporcionan estandarización entre plataformas, soportando el formato de imagen OCI, que garantiza que la imagen puede ejecutarse en el motor Docker.

Uso del plugin de Spring Boot

El plugin de Spring Boot crea imágenes OCI a partir del código fuente utilizando Buildpack. Las imágenes se crean utilizando bootBuildImagetareas (Gradle) o spring-boot:build-imageobjetivos (Maven) y la instalación local de Docker.

Podemos configurar el nombre de la imagen necesaria para 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>

Vamos a utilizar Maven para ejecutar build-imagela meta de creación de la aplicación y de la imagen de contenedor. Ahora no estamos utilizando ningún archivo Docker.

mvn spring-boot:build-image

El resultado será aproximadamente así:

[INFO] --- spring-boot-maven-plugin:2.3.3.RELEASE:build-image (default-cli) @ usersignup ---
[INFO] Construyendo la imagen 'docker.io\/pratikdas\/usersignup:v1'
[INFO] 
[INFO]  > Descargando imagen de builder 'gcr.io\/paketo-buildpacks\/builder:base-platform-api-0.3' 0%
.
.
.. [creator]     Añadiendo etiqueta 'org.springframework.boot.version'
.. [creator]     *** Imágenes (c311fe74ec73):
.. [creator]           docker.io\/pratikdas\/usersignup:v1
[INFO] 
[INFO] Imagen 'docker.io\/pratikdas\/usersignup:v1' construida con éxito

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

docker images 

Salida:

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 plugin de creación de imágenes de Google que proporciona un método alternativo para crear una imagen de contenedor a partir del código fuente.

Configuramos 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 plugin Jib utilizando el comando de Maven para construir la aplicación y crear la imagen del contenedor. Al igual que antes, aquí no estamos utilizando ningún archivo Docker:

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

Después de ejecutar el comando de Maven mencionado anteriormente, obtenemos la siguiente salida:

[INFO] Contenerizando la aplicación a pratikdas/usersignup:v1...
.
.
[INFO] Punto de entrada del contenedor configurado a [java, -cp, /app/resources:/app/classes:/app/libs/*, io.pratik.users.UsersignupApplication]
[INFO] 
[INFO] Imagen construida y enviada como pratikdas/usersignup:v1
[INFO] Ejecutando tareas:
[INFO] [==============================] 100.0% completo

La salida muestra que la imagen del contenedor ha sido creada y almacenada en el registro.

Motivaciones y métodos para crear imágenes optimizadas

Tenemos dos razones principales para la optimización:

  • Rendimiento: en el sistema de orquestación de contenedores, la imagen del contenedor se extrae del registro en el host donde se ejecuta el mecanismo del contenedor. Este proceso se llama programación. Extraer imágenes grandes del registro conduce a un tiempo de programación prolongado en los sistemas de orquestación de contenedores y a largos tiempos de construcción en las canalizaciones de CI.
  • Seguridad: las imágenes grandes también tienen un mayor alcance de vulnerabilidades.

La imagen de Docker consta de una pila de capas, cada una de las cuales representa una instrucción en nuestro Dockerfile. Cada capa representa una delta de cambios en la capa subyacente. Cuando extraemos la imagen de Docker del registro, se extrae por capas y se guarda en el host.

Spring Boot utiliza "JAR grueso" como formato de empaquetado por defecto. Cuando observamos el JAR grueso, vemos que la aplicación constituye una parte muy pequeña de todo el JAR. Esta es la parte que cambia con más frecuencia. El resto consiste en las dependencias del Spring Framework.

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

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

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

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

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

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

Spring Boot 2.3 admite la multicapas extrayendo partes del archivo JAR grueso en capas separadas. La función de superposición está desactivada por defecto y debe ser habilitada explícitamente usando el complemento Maven de Spring Boot:

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

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

Vamos a ejecutar build-imageel objetivo de Maven para crear la imagen del contenedor:

mvn spring-boot:build-image

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

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

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

En lugar de usar el plugin de Maven o Gradle, también podemos crear una imagen JAR de Docker multi-capa con un archivo Docker.

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

El contenido del JAR obtenido después de construir con Maven con la función de capas habilitada se verá de la siguiente manera:

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

En la salida se muestra el JAR adicional llamado spring-boot-jarmode-layertoolsy layersfle.idxarchivo. Este archivo JAR adicional proporciona la capacidad de procesamiento de múltiples capas, como se describe en la siguiente sección.

Extrayendo dependencias en capas separadas

Para ver y extraer capas de nuestro JAR de múltiples capas, utilizamos la propiedad del sistema -Djarmode=layertoolspara ejecutar spring-boot-jarmode-layertoolsel JAR en lugar de la aplicación:

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

La ejecución de este comando da como resultado 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 list, extracty helpcon helpserán predeterminados. Vamos a ejecutar el comando con listla opció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 del cargador JAR

snapshot-dependencies

cualquier dependencia cuya versión contenga SNAPSHOT

application

clases de aplicaciones y recursos

Las capas están definidas en layers.idxel archivo 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 extracción, ya que no cambian. Solo se carga la capa actualizada de la aplicación en el host, lo que ocurre más rápido debido a su tamaño reducido. .

Construir una imagen con dependencias extraídas en capas separadas

Construiremos la imagen final en dos etapas, utilizando un método llamado compilación de múltiples etapas . En la primera etapa, extraeremos las dependencias y en la segunda etapa copiaremos las dependencias extraídas a la imagen final.

Modifiquemos nuestro archivo Docker para la compilación de múltiples 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 con el comando:

docker build -f Dockerfile2 -t usersignup:v1 .

Después de ejecutar este comando, obtenemos la siguiente salida:

Enviando el contexto de construcción al demonio de Docker  20.41MB
Paso 1/12 : FROM adoptopenjdk:14-jre-hotspot as builder
14-jre-hotspot: Extrayendo de library/adoptopenjdk
.
.
Construido correctamente a a9ebf6970841
Etiquetado correctamente userssignup:v1

Vemos que la imagen de Docker se crea con un ID de imagen y luego se etiqueta.

Finalmente, ejecutamos el comando Dive, como antes, para verificar las capas dentro de la imagen de Docker generada. Podemos especificar el ID de imagen o la etiqueta como entrada para el comando Dive:

dive userssignup:v1

Como se puede ver en la salida, la capa que contiene la aplicación ahora ocupa solo 11 KB, mientras que las dependencias se almacenan en capas separadas. 

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

Extracción de 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 junto con la aplicación, declarándolas en ymlun archivo 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 añadido una dependencia personalizada con el nombre, io.myorgcontiene las dependencias de la organización, obtenidas de un repositorio común.

Salida

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

También hemos revisado la optimización de nuestro contenedor, incorporando la función de capas, que extrae las dependencias en niveles separados que se almacenan en caché en el host, mientras que la capa delgada de la aplicación se carga durante la planificación en los mecanismos de ejecución del contenedor.

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

Guía de comandos

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

Limpiar el contexto:

docker system prune -a

Crear una imagen de contenedor usando un archivo Docker:

docker build -f  -t  .

Construir una imagen de contenedor desde el código fuente (sin Dockerfile):

mvn spring-boot:build-image

Ver capas de dependencias. Antes de construir el archivo JAR de la aplicación, asegúrate de que la función de capas esté habilitada en spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Extraer capas de dependencias. Antes de construir el archivo JAR de la aplicación, asegúrate de que la función de capas esté habilitada en spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Ver lista de imágenes de contenedores

docker images

Ver capas dentro de la imagen del contenedor (asegúrate de tener instalada la herramienta de inmersión):

dive

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster