Creación de imaxes Docker optimizadas para unha aplicación Spring Boot

Os contedores convertéronse no medio preferido para empaquetar unha aplicación con todas as súas dependencias de software e sistema operativo e, a continuación, entregalas a diferentes ambientes.

Este artigo abarca diferentes formas de contener unha aplicación Spring Boot:

  • construír unha imaxe docker usando un dockerfile,
  • construír unha imaxe OCI desde a orixe usando Cloud-Native Buildpack,
  • e optimización de imaxes en tempo de execución separando as partes JAR en diferentes niveis mediante ferramentas en capas.

 Exemplo de código

Este artigo vai acompañado dun exemplo de código de traballo en github .

Terminoloxía dos contedores

Comezaremos coa terminoloxía do contedor utilizada ao longo do artigo:

  • Imaxe do recipiente: un ficheiro dun formato específico. Convertemos a nosa aplicación nunha imaxe de contedor executando a ferramenta de compilación.
  • Recipiente: unha instancia executable da imaxe do contedor.
  • Motor de contedores: O proceso daemon responsable de executar o contedor.
  • Host de contedores: a máquina host na que se está a executar o motor de contedores.
  • Rexistro de contedores: A localización xeral utilizada para publicar e distribuír a imaxe do contedor.
  • Estándar OCIOpen Container Initiative (OCI) é un marco de xestión lixeiro e de código aberto formado pola Fundación Linux. A Especificación de imaxes OCI define os estándares do sector para os formatos de imaxe de contedores e o tempo de execución para garantir que todos os motores de contedores poidan executar imaxes de contedores creadas por calquera ferramenta de compilación.

Para contener unha aplicación, envolvemos a nosa aplicación nunha imaxe de contedor e publicamos esa imaxe no rexistro público. O tempo de execución do contedor recupera esta imaxe do rexistro, desempaqueta e executa a aplicación no seu interior.

A versión 2.3 de Spring Boot ofrece complementos para crear imaxes OCI.

Estivador é a implementación de contedores máis usada e usamos Docker nos nosos exemplos, polo que todas as referencias de contedores posteriores deste artigo referiranse a Docker.

Construír unha imaxe de contedor de xeito tradicional

Crear imaxes de Docker para aplicacións Spring Boot é moi sinxelo engadindo algunhas instrucións ao teu Dockerfile.

Primeiro creamos un JAR executable e, como parte das instrucións de Dockerfile, copiamos o JAR executable enriba da imaxe JRE base despois de aplicar as personalizacións necesarias.

Imos crear a nosa aplicación Spring en Inicialización de primavera con dependencias weblombokи actuator. Tamén engadimos un controlador de descanso para proporcionar unha API GETmétodo.

Creando un Dockerfile

Despois colocamos esta aplicación nun recipiente engadindo Dockerfile:

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

O noso Dockerfile contén unha imaxe base, de adoptopenjdk, enriba do cal copiamos o noso ficheiro JAR e despois abrimos o porto, 8080que escoitará as solicitudes.

Montaxe da aplicación

Primeiro cómpre crear unha aplicación usando Maven ou Gradle. Aquí estamos usando Maven:

mvn clean package

Isto crea un ficheiro JAR executable para a aplicación. Necesitamos converter este JAR executable nunha imaxe de Docker para executalo no motor Docker.

Crea unha imaxe de contedor

Despois colocamos este executable JAR na imaxe de Docker executando o comando docker builddende o directorio raíz do proxecto que contén o Dockerfile creado anteriormente:

docker build  -t usersignup:v1 .

Podemos ver a nosa imaxe na lista co comando:

docker images 

A saída do comando anterior inclúe a nosa imaxe usersignupxunto coa imaxe base, adoptopenjdkespecificado no noso Dockerfile.

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

Ver capas dentro dunha imaxe de contedor

Vexamos a pila de capas dentro da imaxe. Usaremos ferramenta  mergullo, para ver estas capas:

dive usersignup:v1

Aquí está parte da saída do comando Dive: 

Creación de imaxes Docker optimizadas para unha aplicación Spring Boot

Como podemos ver, a capa de aplicación constitúe unha parte importante do tamaño da imaxe. Queremos reducir o tamaño desta capa nas seguintes seccións como parte da nosa optimización.

Creando unha imaxe de contedor con Buildpack

Paquetes de montaxe (Paquetes de construción) é un termo xenérico utilizado por varias ofertas de Plataforma como servizo (PAAS) para crear unha imaxe de contedor a partir do código fonte. Foi lanzado por Heroku en 2011 e desde entón foi adoptado por Cloud Foundry, Google App Engine, Gitlab, Knative e algúns outros.

Creación de imaxes Docker optimizadas para unha aplicación Spring Boot

Vantaxe dos paquetes Cloud Build

Un dos principais beneficios de usar Buildpack para construír imaxes é que Os cambios na configuración da imaxe pódense xestionar de forma centralizada (construtor) e propagarse a todas as aplicacións que utilicen o creador.

Os paquetes de compilación estaban estreitamente ligados á plataforma. Os paquetes de compilación nativos na nube proporcionan estandarización en todas as plataformas ao admitir o formato de imaxe OCI, o que garante que a imaxe poida ser executada polo motor Docker.

Usando o complemento Spring Boot

O complemento Spring Boot constrúe imaxes OCI desde a orixe usando Buildpack. As imaxes créanse usando bootBuildImagetarefas (Gradle) ou spring-boot:build-imagedestino (Maven) e instalación local de Docker.

Podemos personalizar o nome da imaxe que necesitamos enviar ao rexistro de Docker especificando o nome 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 executar build-imageobxectivos para crear unha aplicación e crear unha imaxe de contedor. Actualmente non estamos usando ningún Dockerfile.

mvn spring-boot:build-image

O resultado será algo así:

[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 partir da saída, vemos que paketo Cloud-Native buildpackusado para crear unha imaxe OCI de traballo. Como antes, podemos ver a imaxe listada como imaxe de Docker executando o comando:

docker images 

Conclusión:

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

Creando unha imaxe de contedor con Jib

Jib é un complemento de creación de imaxes de Google que ofrece un método alternativo para crear unha imaxe de contedor desde a orixe.

Personalizar 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, executamos o complemento Jib usando o comando Maven para construír a aplicación e crear a imaxe do contedor. Como antes, non estamos a usar ningún Dockerfile aquí:

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

Despois de executar o comando Maven anterior, obtemos a seguinte saída:

[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

A saída mostra que a imaxe do contedor foi creada e colocada no rexistro.

Motivacións e métodos para crear imaxes optimizadas

Temos dúas razóns principais para optimizar:

  • Produtividade: nun sistema de orquestración de contedores, unha imaxe do contedor tírase do rexistro de imaxes ao host que executa o motor de contedores. Este proceso chámase planificación. A extracción de imaxes grandes do rexistro dá como resultado longos tempos de programación nos sistemas de orquestración de contedores e longos tempos de construción en canalizacións de CI.
  • Безопасность: as imaxes grandes tamén teñen unha gran área para vulnerabilidades.

Unha imaxe de Docker está formada por unha pila de capas, cada unha representando unha declaración no noso ficheiro Docker. Cada capa representa o delta dos cambios na capa subxacente. Cando extraemos unha imaxe de Docker do rexistro, tírase en capas e almacénase na caché no host.

Usos Spring Boot "XARRO gordo" en como formato de empaquetado predeterminado. Cando miramos un JAR gordo, vemos que a aplicación é unha parte moi pequena de todo o JAR. Esta é a parte que máis cambia. O resto consiste en dependencias de Spring Framework.

A fórmula de optimización céntrase en illar a aplicación nun nivel separado das dependencias de Spring Framework.

A capa de dependencia que forma a maior parte do groso ficheiro JAR descárgase só unha vez e almacénase na memoria caché no sistema host.

Só se extrae unha fina capa da aplicación durante as actualizacións da aplicación e a programación do contedor. como se mostra neste diagrama:

Creación de imaxes Docker optimizadas para unha aplicación Spring Boot

Nas seguintes seccións, veremos como crear estas imaxes optimizadas para unha aplicación Spring Boot.

Construír unha imaxe de contedor optimizada para unha aplicación Spring Boot con Buildpack

Spring Boot 2.3 admite a estratificación extraendo partes dun ficheiro JAR groso en capas separadas. A función de capas está desactivada de forma predeterminada e debe activarse de forma explícita mediante o 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 construír a nosa imaxe de contedor primeiro con Buildpack e despois con Docker nas seguintes seccións.

Imos correr build-imageMaven apunta a crear unha imaxe de contedor:

mvn spring-boot:build-image

Se executamos Dive para ver as capas da imaxe resultante, podemos ver que a capa da aplicación (con un círculo en vermello) é moito máis pequena no rango de kilobytes en comparación co que obtivemos usando o formato JAR groso:

Creación de imaxes Docker optimizadas para unha aplicación Spring Boot

Construír unha imaxe de contedor optimizada para unha aplicación Spring Boot con Docker

En lugar de usar un complemento Maven ou Gradle, tamén podemos crear unha imaxe Docker JAR en capas cun ficheiro Docker.

Cando usamos Docker, necesitamos facer dous pasos adicionais para extraer as capas e copialas na imaxe final.

O contido do JAR resultante despois de construír con Maven coa capas activada terá o seguinte aspecto:

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

A saída mostra un JAR adicional chamado spring-boot-jarmode-layertoolsи layersfle.idxarquivo. Este ficheiro JAR adicional ofrece capacidades de capas, como se describe na seguinte sección.

Extraer dependencias en capas separadas

Para ver e extraer capas do noso JAR en capas, usamos a propiedade do sistema -Djarmode=layertoolspara comezar spring-boot-jarmode-layertoolsJAR en lugar de aplicación:

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

Executar este comando produce unha saída que contén as opcións de comando dispoñibles:

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

A saída mostra os comandos listextractи helpс helpser o predeterminado. Imos executar o comando con listopción:

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

Vemos unha lista de dependencias que se poden engadir como capas.

Capas por defecto:

Nome da capa

Contido

dependencies

calquera dependencia cuxa versión non conteña SNAPSHOT

spring-boot-loader

Clases de cargador JAR

snapshot-dependencies

calquera dependencia cuxa versión conteña SNAPSHOT

application

clases de aplicación e recursos

As capas defínense en layers.idxficheiro na orde na que deberían engadirse á imaxe de Docker. Estas capas almacénanse na memoria caché no host despois da primeira recuperación porque non cambian. Só se descarga no host a capa de aplicación actualizada, que é máis rápida debido ao tamaño reducido .

Construír unha imaxe con dependencias extraídas en capas separadas

Construiremos a imaxe final en dous pasos mediante un método chamado montaxe de varias etapas . No primeiro paso extraeremos as dependencias e no segundo copiaremos as dependencias extraídas no .

Modifiquemos o noso Dockerfile para unha compilación en 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"]

Gardamos esta configuración nun ficheiro separado - Dockerfile2.

Construímos a imaxe de Docker usando o comando:

docker build -f Dockerfile2 -t usersignup:v1 .

Despois de executar este comando, obtemos a seguinte saída:

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 a imaxe de Docker é creada cun ID de imaxe e logo etiquetada.

Finalmente, executamos o comando Dive como antes para comprobar as capas dentro da imaxe Docker xerada. Podemos proporcionar unha ID de imaxe ou etiqueta como entrada para o comando Dive:

dive userssignup:v1

Como podes ver na saída, a capa que contén a aplicación agora só ten 11 KB e as dependencias están almacenadas na caché en capas separadas. 

Creación de imaxes Docker optimizadas para unha aplicación Spring Boot

Extrae dependencias internas en capas separadas

Podemos reducir aínda máis o tamaño da capa da aplicación extraendo calquera das nosas dependencias personalizadas nunha capa separada en lugar de empaquetalas coa aplicación declarándoas en ymlficheiro semellante nomeado 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/"

Neste ficheiro layers.idxEngadimos unha dependencia personalizada chamada, io.myorgque contén dependencias da organización recuperadas do repositorio compartido.

Saída

Neste artigo, analizamos o uso de Cloud-Native Buildpacks para construír unha imaxe de contedor directamente desde a orixe. Esta é unha alternativa ao uso de Docker para crear unha imaxe de contedor do xeito habitual: primeiro, créase un ficheiro JAR executable groso e despois empaquetado nunha imaxe de contedor especificando as instrucións no ficheiro Docker.

Tamén analizamos a optimización do noso contedor incluíndo unha función de estratificación que extrae dependencias en capas separadas que se almacenan na memoria caché no servidor e cárgase unha capa fina de aplicación no momento da programación nos motores de execución do contedor.

Podes atopar todo o código fonte utilizado no artigo en Github .

Referencia de comandos

Aquí tes un resumo dos comandos que usamos neste artigo para unha referencia rápida.

Borrado de contexto:

docker system prune -a

Construír unha imaxe de contedor cun Dockerfile:

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

Construír a imaxe do contedor desde a orixe (sen Dockerfile):

mvn spring-boot:build-image

Ver capas de dependencia. Antes de crear o ficheiro jar da aplicación, asegúrese de que a función de capas estea activada no spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Extraer capas de dependencia. Antes de crear o ficheiro jar da aplicación, asegúrese de que a función de capas estea activada no spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Visualización dunha lista de imaxes de contedores

docker images

Ver á esquerda dentro da imaxe do recipiente (asegúrese de que a ferramenta de mergullo está instalada):

dive <image ID or image tag>

Fonte: www.habr.com