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 .
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: — 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.
— 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 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 packageEsto 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 229MBVisualizando las capas dentro de la imagen del contenedor
Veamos la pila de capas dentro de la imagen. Usaremos para ver estas capas:
dive usersignup:v1Aquí está parte de los resultados de la ejecución del comando Dive:

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
() 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.

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-imageEl 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 éxitoDe 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 257MBCreació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:v1Despué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% completoLa 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 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:

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
trueUsaremos 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-imageSi 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 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.idxEn 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.jarLa 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 commandLa 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 listdependencies
spring-boot-loader
snapshot-dependencies
applicationVemos 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 . 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:v1Vemos 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:v1Como 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.

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 .
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 -aCrear 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-imageVer 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 listExtraer 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 extractVer lista de imágenes de contenedores
docker imagesVer capas dentro de la imagen del contenedor (asegúrate de tener instalada la herramienta de inmersión):
diveFuente: habr.com
