Criando imagens Docker otimizadas para um aplicativo Spring Boot

Os contêineres se tornaram o meio preferido de empacotar um aplicativo com todas as suas dependências de software e sistema operacional e, em seguida, entregá-los a diferentes ambientes.

Este artigo aborda diferentes maneiras de conteinerizar um aplicativo Spring Boot:

  • criando uma imagem Docker usando um arquivo Docker,
  • criando uma imagem OCI a partir da fonte usando Cloud-Native Buildpack,
  • e otimização de imagem em tempo de execução, separando partes do JAR em diferentes camadas usando ferramentas multicamadas.

 Exemplo de código

Este artigo é acompanhado por um exemplo de código funcional no GitHub .

Terminologia de contêiner

Começaremos com a terminologia de contêiner usada no artigo:

  • Imagem do contêiner: arquivo de um formato específico. Converteremos nosso aplicativo em uma imagem de contêiner executando a ferramenta de construção.
  • recipiente: uma instância executável da imagem do contêiner.
  • Motor de contêiner: o processo daemon responsável pela execução do contêiner.
  • Hospedeiro de contêiner: o computador host no qual o mecanismo de contêiner é executado.
  • Registro de contêiner: o local geral usado para publicar e distribuir a imagem do contêiner.
  • Padrão OCIIniciativa de Contêiner Aberto (OCI) é uma estrutura de governança leve e aberta formada pela Linux Foundation. A especificação de imagem OCI define padrões do setor para imagens de contêiner e formatos de tempo de execução para garantir que todos os mecanismos de contêiner possam executar imagens de contêiner criadas por qualquer ferramenta de construção.

Para conteinerizar um aplicativo, envolvemos nosso aplicativo em uma imagem de contêiner e publicamos essa imagem em um registro compartilhado. O tempo de execução do contêiner recupera essa imagem do registro, descompacta-a ​​e executa o aplicativo dentro dela.

A versão 2.3 do Spring Boot fornece plug-ins para criação de imagens OCI.

Estivador é a implementação de contêiner mais comumente usada e usamos Docker em nossos exemplos, portanto, todas as referências subsequentes a contêineres neste artigo se referirão ao Docker.

Construindo uma imagem de contêiner da maneira tradicional

Criar imagens Docker para aplicativos Spring Boot é muito fácil adicionando algumas instruções ao arquivo Docker.

Primeiro criamos um arquivo JAR executável e, como parte das instruções do arquivo Docker, copiamos o arquivo JAR executável sobre a imagem JRE base após aplicar as configurações necessárias.

Vamos criar nosso aplicativo Spring em Spring Initializer com dependências weblombokи actuator. Também estamos adicionando um controlador rest para fornecer uma API com GETmétodo.

Criando um Dockerfile

Em seguida, colocamos esse aplicativo em contêiner adicionando Dockerfile:

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

Nosso arquivo Docker contém uma imagem base de adoptopenjdk, além do qual copiamos nosso arquivo JAR e depois abrimos a porta, 8080que ouvirá as solicitações.

Construindo o aplicativo

Primeiro você precisa criar um aplicativo usando Maven ou Gradle. Aqui usamos Maven:

mvn clean package

Isso cria um arquivo JAR executável para o aplicativo. Precisamos converter este JAR executável em uma imagem Docker para rodar no mecanismo Docker.

Criando uma imagem de contêiner

Em seguida, colocamos este arquivo JAR executável na imagem do Docker executando o comando docker builddo diretório raiz do projeto que contém o Dockerfile criado anteriormente:

docker build  -t usersignup:v1 .

Podemos ver nossa imagem na lista usando o comando:

docker images 

A saída do comando acima inclui nossa imagem usersignupjunto com a imagem base, adoptopenjdkespecificado em nosso arquivo Docker.

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

Visualizar camadas dentro de uma imagem de contêiner

Vejamos a pilha de camadas dentro da imagem. Nós vamos usar ferramenta  mergulhar, para visualizar essas camadas:

dive usersignup:v1

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

Criando imagens Docker otimizadas para um aplicativo Spring Boot

Como podemos ver, a camada de aplicação representa uma parte significativa do tamanho da imagem. Queremos reduzir o tamanho desta camada nas seções seguintes como parte de nossa otimização.

Criando uma imagem de contêiner usando Buildpack

Pacotes de montagem (Pacotes de construção) é um termo geral usado por várias ofertas de plataforma como serviço (PAAS) para criar uma imagem de contêiner a partir do código-fonte. Foi lançado pela Heroku em 2011 e desde então foi adotado pelo Cloud Foundry, Google App Engine, Gitlab, Knative e vários outros.

Criando imagens Docker otimizadas para um aplicativo Spring Boot

A vantagem dos pacotes de construção em nuvem

Um dos principais benefícios de usar o Buildpack para criar imagens é que As alterações na configuração da imagem podem ser gerenciadas centralmente (construtor) e propagadas para todos os aplicativos usando o construtor.

Os pacotes de construção foram fortemente acoplados à plataforma. Os Buildpacks Cloud-Native fornecem padronização entre plataformas, suportando o formato de imagem OCI, o que garante que a imagem possa ser executada pelo mecanismo Docker.

Usando o plug-in Spring Boot

O plugin Spring Boot cria imagens OCI a partir da fonte usando Buildpack. As imagens são criadas usando bootBuildImagetarefas (Gradle) ou spring-boot:build-imagealvos (Maven) e instalação local do Docker.

Podemos personalizar o nome da imagem necessária para enviar para o registro do Docker, especificando o nome em 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 usar o Maven para fazer isso build-imageobjetivos para criar um aplicativo e criar uma imagem de contêiner. Não estamos usando nenhum Dockerfile no momento.

mvn spring-boot:build-image

O resultado será algo como isto:

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

Pela saída vemos que paketo Cloud-Native buildpackusado para criar uma imagem OCI funcional. Como antes, podemos ver a imagem listada como imagem Docker executando o comando:

docker images 

Conclusão:

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

Criando uma imagem de contêiner usando Jib

Jib é um plugin de criação de imagens do Google que fornece um método alternativo para criar uma imagem de contêiner a partir do código-fonte.

Configurando jib-maven-pluginem pom.xml:

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

A seguir, executamos o plugin Jib usando o comando Maven para construir o aplicativo e criar uma imagem de contêiner. Como antes, não estamos usando nenhum arquivo Docker aqui:

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

Depois de executar o comando Maven acima, 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 imagem do contêiner foi criada e colocada no registro.

Motivações e técnicas para criação de imagens otimizadas

Temos dois motivos principais para otimização:

  • Desempenho: em um sistema de orquestração de contêiner, uma imagem de contêiner é recuperada do registro de imagem para o host que executa o mecanismo de contêiner. Esse processo é chamado de planejamento. Extrair imagens grandes do registro resulta em longos tempos de agendamento em sistemas de orquestração de contêineres e longos tempos de construção em pipelines de CI.
  • segurança: Imagens maiores também possuem uma área maior para vulnerabilidades.

Uma imagem Docker consiste em uma pilha de camadas, cada uma representando uma instrução em nosso Dockerfile. Cada camada representa um delta das alterações na camada subjacente. Quando extraímos uma imagem Docker do registro, ela é extraída em camadas e armazenada em cache no host.

Usos do Spring Boot "JAR gordo" em como formato de embalagem padrão. Quando olhamos para o JAR grosso, vemos que o aplicativo representa uma porção muito pequena de todo o JAR. Esta é a parte que muda com mais frequência. O restante consiste nas dependências do Spring Framework.

A fórmula de otimização gira em torno do isolamento do aplicativo em um nível separado das dependências do Spring Framework.

A camada de dependência, que forma a maior parte do arquivo JAR espesso, é baixada apenas uma vez e armazenada em cache no sistema host.

Apenas uma camada fina do aplicativo é extraída durante as atualizações do aplicativo e o agendamento do contêiner. conforme mostrado neste diagrama:

Criando imagens Docker otimizadas para um aplicativo Spring Boot

Nas seções a seguir, veremos como criar essas imagens otimizadas para um aplicativo Spring Boot.

Criando uma imagem de contêiner otimizada para um aplicativo Spring Boot usando Buildpack

Spring Boot 2.3 suporta camadas extraindo partes de um arquivo JAR espesso em camadas separadas. O recurso de camadas está desabilitado por padrão e deve ser habilitado explicitamente usando o plugin Spring Boot Maven:

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

Usaremos essa configuração para construir nossa imagem de contêiner primeiro com Buildpack e depois com Docker nas seções a seguir.

Vamos lançar build-imageAlvo Maven para criação de imagem de contêiner:

mvn spring-boot:build-image

Se executarmos o Dive para ver as camadas na imagem resultante, podemos ver que a camada de aplicação (destacada em vermelho) é muito menor na faixa de kilobytes em comparação com o que obtivemos usando o formato JAR espesso:

Criando imagens Docker otimizadas para um aplicativo Spring Boot

Criando uma imagem de contêiner otimizada para um aplicativo Spring Boot usando Docker

Em vez de usar um plugin Maven ou Gradle, também podemos criar uma imagem Docker JAR em camadas com um arquivo Docker.

Quando usamos o Docker, precisamos realizar duas etapas adicionais para extrair as camadas e copiá-las na imagem final.

O conteúdo do JAR resultante após a construção usando Maven com camadas habilitadas será semelhante a este:

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 um JAR adicional chamado spring-boot-jarmode-layertoolsи layersfle.idxarquivo. Esse arquivo JAR adicional fornece recursos de processamento em camadas, conforme descrito na próxima seção.

Extraindo dependências em camadas individuais

Para visualizar e extrair camadas do nosso JAR em camadas, usamos a propriedade do sistema -Djarmode=layertoolscorrer spring-boot-jarmode-layertoolsJAR em vez de aplicativo:

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

A execução deste comando produz uma saída contendo as opções de comando disponíveis:

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с helpseja o padrão. Vamos executar o comando com listopção:

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

Vemos uma lista de dependências que podem ser adicionadas como camadas.

Camadas padrão:

Nome da camada

Conteúdo

dependencies

qualquer dependência cuja versão não contenha SNAPSHOT

spring-boot-loader

Classes de carregador JAR

snapshot-dependencies

qualquer dependência cuja versão contenha SNAPSHOT

application

classes e recursos de aplicativos

As camadas são definidas em layers.idxarquivo na ordem em que devem ser adicionados à imagem do Docker. Essas camadas são armazenadas em cache no host após a primeira recuperação porque não mudam. Apenas a camada de aplicação atualizada é baixada para o host, o que é mais rápido devido ao tamanho reduzido .

Construindo uma imagem com dependências extraídas em camadas separadas

Construiremos a imagem final em duas etapas usando um método chamado montagem em vários estágios . Na primeira etapa extrairemos as dependências e na segunda etapa copiaremos as dependências extraídas para a imagem final.

Vamos modificar nosso Dockerfile para uma compilação de vários estágios:

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

Salvamos esta configuração em um arquivo separado - Dockerfile2.

Construímos a imagem Docker usando o comando:

docker build -f Dockerfile2 -t usersignup:v1 .

Depois 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 uma imagem Docker é criada com um ID de imagem e depois marcada.

Finalmente, executamos o comando Dive como antes para inspecionar as camadas dentro da imagem Docker gerada. Podemos fornecer um ID de imagem ou tag como entrada para o comando Dive:

dive userssignup:v1

Como você pode ver na saída, a camada que contém o aplicativo agora tem apenas 11 KB e as dependências são armazenadas em cache em camadas separadas. 

Criando imagens Docker otimizadas para um aplicativo Spring Boot

Extraindo dependências internas em camadas individuais

Podemos reduzir ainda mais o tamanho da camada do aplicativo extraindo qualquer uma de nossas dependências personalizadas em uma camada separada, em vez de empacotá-las com o aplicativo, declarando-as em ymlarquivo semelhante chamado 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 arquivo layers.idxadicionamos uma dependência personalizada chamada, io.myorgcontendo dependências da organização recuperadas de um repositório compartilhado.

Jogar aviator online grátis: hack aviator funciona

Neste artigo, vimos o uso de Buildpacks nativos da nuvem para construir uma imagem de contêiner diretamente do código-fonte. Esta é uma alternativa ao uso do Docker para criar uma imagem de contêiner da maneira usual: primeiro criando um arquivo JAR executável espesso e depois empacotando-o em uma imagem de contêiner especificando instruções no arquivo Docker.

Também analisamos a otimização de nosso contêiner, habilitando um recurso de camadas que coloca dependências em camadas separadas que são armazenadas em cache no host e uma camada fina do aplicativo é carregada no momento do agendamento nos mecanismos de execução do contêiner.

Você pode encontrar todo o código-fonte usado no artigo em Github .

Referência de comando

Aqui está um rápido resumo dos comandos que usamos neste artigo.

Limpeza de contexto:

docker system prune -a

Criando uma imagem de contêiner usando um arquivo Docker:

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

Construímos a imagem do contêiner a partir do código-fonte (sem Dockerfile):

mvn spring-boot:build-image

Visualize camadas de dependência. Antes de construir o arquivo JAR do aplicativo, certifique-se de que o recurso de camadas esteja habilitado no spring-boot-maven-plugin:

java -Djarmode=layertools -jar application.jar list

Extraindo camadas de dependência. Antes de construir o arquivo JAR do aplicativo, certifique-se de que o recurso de camadas esteja habilitado no spring-boot-maven-plugin:

 java -Djarmode=layertools -jar application.jar extract

Ver uma lista de imagens de contêiner

docker images

Veja à esquerda dentro da imagem do contêiner (certifique-se de que a ferramenta de mergulho esteja instalada):

dive <image ID or image tag>

Fonte: habr.com