Skip to main content

Use o JLink para criar imagens Docker menores para sua aplicação Java com Spring Boot

Escrito por
feature kubernetes polp

24 de agosto de 2023

0 minutos de leitura

Os contêineres trazem mais flexibilidade e agilidade ao desenvolvimento e à implantação de software. No entanto, também criam uma nova superfície de ataque que agentes mal-intencionados podem explorar. Um contêiner comprometido pode dar a um invasor acesso a outros contêineres e até mesmo ao sistema host. Imagens menores, com menos artefatos, já ajudam bastante a reduzir a superfície de ataque.

Neste post, vamos explorar em detalhes como usar o JLink para otimizar o tamanho das imagens Docker e melhorar a segurança e o desempenho das aplicações. Vamos mostrar como usar o JLink e integrá-lo ao Docker para implantar com eficiência aplicações Spring Boot ou Java em geral.

Java é uma das linguagens de programação mais usadas no mundo para o desenvolvimento de aplicações empresariais. No entanto, muitas vezes os desenvolvedores enfrentam dificuldades com o tamanho das imagens Docker ao implantar aplicações Java em contêineres Docker. Uma das maneiras de resolver esse problema é usar o JLink, uma ferramenta introduzida no JDK 9.

O JLink (Java Linker) é uma ferramenta de linha de comando que reúne e otimiza um conjunto de módulos e suas dependências em uma imagem de runtime personalizada. Na prática, ele cria um ambiente de runtime Java mínimo, contendo apenas os módulos necessários para a sua aplicação.

$ jlink --module-path $JAVA_HOME/jmods:mlib --add-modules my.module --output myRunTime

No comando acima, my.module é o seu módulo, e myRuntime é a imagem de runtime personalizada que o JLink vai criar.

Ao criar imagens Docker para aplicações Java, o tamanho da imagem costuma ser uma preocupação — especialmente no caso de aplicações Spring Boot, que vêm com muitas dependências. Imagens Docker grandes podem aumentar o tempo de inicialização, os custos de armazenamento e a demora no processo de implantação.

Nas versões antigas do Java, o Java Development Kit (JDK) vinha acompanhado de um Java Runtime Environment (JRE). Para executar o artefato Java criado, bastava o JRE. Por isso, era comum usar o JRE na imagem Docker ou escolher uma imagem base com JRE para os contêineres. As versões mais recentes do Java nem sempre vêm com um JRE, embora alguns fornecedores ainda possam criar um JRE e as imagens base correspondentes. Você pode usar essas opções ou um runtime Java mais específico, adaptado à sua aplicação.

Com o JLink, você pode criar um runtime Java mínimo que contém apenas os módulos necessários. Assim, o tamanho da imagem Docker diminui significativamente. Por exemplo, um ambiente de runtime Java padrão pode ultrapassar 200 MB, mas, com o JLink, é possível reduzi-lo para menos de 50 MB.

O Spring Boot cria um fat JAR para as aplicações, que inclui todas as dependências. Além disso, muitas aplicações Spring Boot não têm uma declaração de módulos. Isso não precisa ser um problema, mas precisamos determinar de quais módulos a aplicação e todas as suas dependências precisam.

Como encontrar os módulos com o Jdeps


O Jdeps é uma ferramenta Java que mostra as dependências no nível de pacote ou de classe. Introduzida no Java 8, ela pode ser usada para entender as dependências de uma aplicação e, depois, criar uma imagem de runtime personalizada com o JLink.

Se todas as dependências estiverem em um único diretório, podemos usar o jdeps para exibir um resumo delas.

jdeps -cp 'mydeps/lib/*' -recursive --multi-release 17 -s target/MyJar.jar

Da mesma forma, podemos usar o jdeps para exibir recursivamente todas as dependências de módulos da aplicação Spring Boot e de suas dependências.

jdeps --ignore-missing-deps -q  --recursive  --multi-release 17  --print-module-deps  --class-path 'mydeps/lib/*'  target/MyJar.jar

A saída gerada pelo jdeps permite que o JLink crie um Java Runtime contendo apenas os módulos necessários para essa aplicação Spring Boot.

Como colocar as dependências do Spring Boot em uma pasta

Como mencionamos, o Spring Boot cria um fat JAR que inclui todas as dependências. No entanto, elas são empacotadas de uma forma específica dentro do JAR e, por isso, não são facilmente acessíveis pelo jdeps. Há duas soluções simples para disponibilizar as dependências ao jdeps.

  • Descompacte o arquivo fat JAR criado pelo Spring Boot.

    • Essa opção funciona muito bem se você já tiver o artefato de build pronto e não quiser ou não puder recompilar a aplicação. As dependências serão descompactadas em /BOOT/libs/.

  • Use um plugin da sua ferramenta de build para copiar as dependências para uma pasta específica.

    • No Maven, por exemplo, isso pode ser feito usando o maven-dependency-plugin. No exemplo abaixo, as dependências são copiadas para a pasta /target/dependency após o Maven concluir a fase de empacotamento.

<project>
    <!-- ... other configurations ... -->

    <build>
        <plugins>
            <!-- Add the maven-dependency-plugin -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.1.2</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <!-- Configure the output directory for the dependencies -->
                            <outputDirectory>${project.build.directory}/dependency</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- ... other configurations ... -->
</project>

Como criar uma imagem Docker com um Java Runtime personalizado

Agora, vamos combinar o jdeps e o JLink para criar um Java Runtime personalizado. Com esse runtime, podemos criar uma imagem Docker ideal e mínima, específica para uma aplicação Spring Boot.

FROM maven:3-eclipse-temurin-17 as build
RUN mkdir /usr/src/project
COPY . /usr/src/project
WORKDIR /usr/src/project
RUN mvn package -DskipTests
RUN jar xf target/JavaCoffeeShop.jar
RUN jdeps --ignore-missing-deps -q  \
    --recursive  \
    --multi-release 17  \
    --print-module-deps  \
    --class-path 'BOOT-INF/lib/*'  \
    target/JavaCoffeeShop.jar > deps.info
RUN jlink \
    --add-modules $(cat deps.info) \
    --strip-debug \
    --compress 2 \
    --no-header-files \
    --no-man-pages \
    --output /myjre
FROM debian:bookworm-slim
ENV JAVA_HOME /user/java/jdk17
ENV PATH $JAVA_HOME/bin:$PATH
COPY --from=build /myjre $JAVA_HOME
RUN mkdir /project
COPY --from=build /usr/src/project/target/JavaCoffeeShop.jar /project/
WORKDIR /project
ENTRYPOINT java -jar JavaCoffeeShop.jar

No exemplo acima, usei um build Docker em vários estágios. O primeiro estágio de build tem como base uma imagem eclipse-temurin JDK 17 que inclui o Maven. Esse estágio é usado para:

  • Criar o artefato Java. Com o Maven, crio o arquivo JAR executável com todas as dependências, que contém a aplicação completa.

  • Descompactar o arquivo JAR para obter todas as dependências. Isso só é necessário se você não usar o maven-dependency-plugin, como explicado anteriormente. Se você o incluiu, pode pular esta etapa.

  • Usar o jdeps para obter os módulos necessários. Aponte para o arquivo que contém todos os arquivos JAR das dependências e o artefato final e salve a lista em deps.info.

  • Executar o JLink para criar um Java Runtime personalizado. Use deps.info como entrada e armazene o resultado em /myjre. Adicionamos apenas os módulos necessários ao JLink e removemos informações de depuração, páginas de manual e arquivos de cabeçalho.

O segundo e último estágio cria a imagem de produção com base na imagem debian:stable-slim.

  • Definir as variáveis de ambiente. Defina JAVA_HOME como o caminho onde vou copiar myjre e adicione JAVA_HOME ao PATH.

  • Copiar o Java Runtime criado pelo JLink. Faça referência ao primeiro estágio e copie o Java Runtime personalizado para o local definido como JAVA_HOME.

  • Copiar o artefato Java criado. O fat JAR executável do Spring Boot é copiado para o diretório específico do projeto.

  • Definir o Entrypoint

O JLink oferece várias vantagens na criação de imagens Docker para aplicações Java com Spring Boot:

1. Redução do tamanho da imagem: Como mencionamos, o JLink ajuda a reduzir o tamanho da imagem Docker, acelerando a implantação e diminuindo os custos de armazenamento.

2. Inicialização mais rápida: Uma imagem Docker menor permite que a aplicação seja iniciada mais rapidamente, o que é essencial para aplicações que precisam escalar depressa.

3. Segurança: Ao incluir apenas os módulos necessários, você reduz a superfície de ataque da aplicação. Menos módulos significam menos vulnerabilidades de segurança em potencial.

Falando em segurança, vale destacar o papel da Snyk na proteção das suas aplicações. A Snyk é uma ferramenta de segurança para desenvolvedores que pode analisar seu código-fonte, pacotes open source, imagens de contêiner e configurações de nuvem em busca de vulnerabilidades. Com Snyk Container e Snyk Open Source, você pode detectar e corrigir problemas de segurança na sua aplicação e em suas dependências — inclusive nas imagens Docker.

$ snyk container test your-repo/your-image:tag

No comando acima, your-repo/your-image:tag é a sua imagem Docker. A Snyk vai analisá-la e informar todas as vulnerabilidades detectadas, além de sugerir como corrigi-las.

Crie imagens Docker menores e mais seguras para aplicações Java

Lembre-se de que os exemplos deste post servem para mostrar como usar o JLink para criar imagens Docker mais enxutas para seus projetos Java. Eles não seguem todas as práticas recomendadas para imagens Docker seguras. Para saber mais, confira nosso artigo “10 práticas recomendadas para criar um contêiner Java com Docker” e inspire-se.

Em resumo, o JLink é uma ferramenta poderosa que pode ajudar você a criar imagens Docker menores e mais seguras para suas aplicações Java com Spring Boot. Combinado com ferramentas de segurança como a Snyk, ele ajuda a garantir o desempenho e a segurança das suas aplicações. Então, por que esperar? Inscreva-se na Snyk hoje mesmo e comece a proteger suas aplicações.

Segurança de contêineres que prioriza os desenvolvedores

O Snyk encontra e corrige automaticamente vulnerabilidades em imagens de contêiner e workloads do Kubernetes.