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 OCI: Iniciativa 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 web, lombokи 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:
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:
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.
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:
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.
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:
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:
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:
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:
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 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:
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:
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 list, extractи helpс helpseja o padrão. Vamos executar o comando com listopção:
java -Djarmode=layertools -jar target/usersignup-0.0.1-SNAPSHOT.jar list
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.
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:
Neste arquivo layers.idxadicionamos uma dependência personalizada chamada, io.myorgcontendo dependências da organização recuperadas de um repositório compartilhado.
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: