ProHoster > Blog > administración > Creación de imágenes de Docker optimizadas para una aplicación Spring Boot
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 OCI: Iniciativa 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 web, lombokи 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:
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:
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.
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:
Usemos Maven para ejecutar build-imageobjetivos para crear una aplicación y crear una imagen de contenedor. Actualmente no estamos usando ningún Dockerfile.
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:
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í:
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:
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:
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 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í:
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:
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. list, extractи helpс helpser el predeterminado. Ejecutemos el comando con listopción:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
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.
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:
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: