본문 바로가기

jdeps와 jlink를 활용한 Java 기반 컨테이너 경량화 본문

DevOps

jdeps와 jlink를 활용한 Java 기반 컨테이너 경량화

겨울바람_ 2024. 9. 12. 14:02

Overview

컨테이너로 배포되는 애플리케이션은 컨테이너 이미지의 크기가 작을수록 빠르게 실행하고 확장할 수 있으며 이미지 보관 및 전송에 드는 비용이 절감된다.

 

Java 기반의 애플리케이션은 JVM이 함께 배포되어야 하기 때문에 Go 언어와 같은 바이너리 형태로 배포되는 애플리케이션에 비해 컨테이너 이미지의 크기가 비대하다.

 

이번 글에서는 jdepsjlink가 각각 어떤 명령어인지 알아보고 해당 명령어를 통해 Java 기반의 애플리케이션 컨테이너를 경량화 하는 방법에 대해 알아보고자 한다.

 

본 포스팅은 AWS 기술 블로그와 일본 개발자분의 포스팅을 토대로 작성되었다.

 

https://qiita.com/yTakada-gxp/items/f681d28f31e999e5dfae
https://aws.amazon.com/ko/blogs/tech/amazon-corretto-base-container-diet/

jdeps

https://docs.oracle.com/en/java/javase/11/tools/jdeps.html#GUID-A543FEBE-908A-49BF-996C-39499367ADB4
jdeps 명령어는 .class 파일의 패키지 혹은 클래스 레벨의 의존성을 보여준다. 모듈화가 적용된 jdk9 버전 이후에만 동작하기 때문에 버전 지정시 주의해야 한다. jdeps에는 여러가지 옵션이 존재하지만, 이번 글에서 주요하게 사용하게 되는 옵션은 다음과 같다.

 

--ignore-missing-deps : 결락되어 있는 의존성을 무시한다. 즉, 의존성을 알 수 없는 모듈은 제외
--print-module-deps : 모듈 의존성을 콤마로 구분된 리스트 형태로 출력한다.
--class-path <path> : 클래스 파일의 위치를 지정한다.

 

위에 설명한 옵션들을 사용해서 의존성이 존재하는 모듈을 추출하는 코드는 다음과 같다.

$ jdeps \
    --ignore-missing-deps \
    --print-module-deps \
    -q \
    -R \ ##모든 런타임의 종속성을 재귀적으로 탐색한다
    --multi-release 17 \ ##의존성을 분석할 버전을 지정한다. 
    --class-path="./<app_name>/BOOT-INF/lib/*" \
    --module-path="./<app_name>/BOOT-INF/lib/*" \
    build/libs/<jar_file_name>.jar

 

SpringBoot를 사용하는 경우 jar파일 내부에 /BOOT-INF/lib/라는 디렉토리에 의존 라이브러리를 보유하고, 실행될 때 읽어오는 구조를 가지고 있다. 해당 jarjdeps의 클래스패스에 지정할 필요가 있다.

jlink

https://docs.oracle.com/en/java/javase/11/tools/jlink.html
jlink 명령어를 통해 모듈과 모듈의 의존성을 커스텀 런타임 이미지에 결집하고 최적화 할 수 있다. jlink의 여러 옵션들 중 이번 글에서 주요 사용되는 옵션은 다음과 같다.

 

--add-modules [...] : 모듈과 모드를 기본 루트 모듈에 추가한다. 기본 루트 모듈은 비어있다.
--strip-debug : 출력에서 디버그 정보를 제거한다.
--no-man-page : man pages를 제거한다.
--no-header-files : header files를 제거한다.
--compress=2 : 리소스의 압축을 가능하게 한다.
    0 : 압축 불가
    1 : 상수 문자열 공유
    2 : ZIP
--output path : 런타임 이미지를 생성할 위치를 지정한다.

$ jlink \ --verbose \. ##상세한 추적을 활성화 하여 로깅합니다.
    --add-modules [] \ ## jdeps 명령어를 통해 확인한 의존성을 입력한다.
    --strip-debug \
    --no-man-pages \  
    --no-header-files \ 
    --compress=2 \ 
    --output <path>

 

jdepsjlink를 활용하여 만들어진 jre 파일은 해당 자바 애플리케이션을 실행하기 위한 최소 모듈만을 포함하고 있으며 최소한의 CLI만 포함된 상태로 생성된다.

Write Dockerfile

위에서 작성한 jdepsjlink 명령어를 도커파일에 적용시켜 경량화된 컨테이너 이미지를 생성해보는 실습을 진행해보자.

 

해당 Dockerfile은 애플리케이션이 이미 jar파일로 빌드된 상태임을 가정하고 작성되었다. 또한, 클라우드 플랫폼 중립적으로 작성하기 위해 amazon-correctto가 아닌 eclipse-temurin을 기반으로 작성했다.

# Stage 1
FROM eclipse-temurin:<version>-alpine as deps

COPY ./<jar_file_name>.jar /app/<jar_file_name>.jar

RUN mkdir /app/unpacked && \
    cd /app/unpacked && \
    unzip ../<jar_file_name>.jar && \
    cd .. && \
    $JAVA_HOME/bin/jdeps \
    --ignore-missing-deps \ 
    --print-module-deps \ 
    -q \ 
    --recursive \ 
    --multi-release <version> \ 
    --class-path="./unpacked/BOOT-INF/lib/*" \ 
    --module-path="./unpacked/BOOT-INF/lib/*" \ 
    ./<jar_file_name>.jar > /deps.info

# Stage 2
FROM eclipse-temurin:<version>-alpine as temurin-jdk

RUN apk add --no-cache binutils

COPY --from=deps /apo/deps.info /deps.info

RUN $JAVA_HOME/bin/jlink \
        --verbose \
        --add-modules $(cat /deps.info) \ 
        --strip-debug \ 
        --no-man-pages \ 
        --no-header-files \ 
        --compress=2 \ 
        --output /<custom_jre_name>

# Stage 3
FROM alpine:<version>
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"

COPY --from=temurin-jdk /<custom_jre_name> $JAVA_HOME

ARG APPLICATION_USER=appuser
RUN adduser --no-create-home -u 1000 -D $APPLICATION_USER

RUN mkdir /app && \
    chown -R $APPLICATION_USER /app

USER 1000

COPY --chown=1000:1000 ./<jar_file_name>.jar /app/<jar_file_name>.jar
WORKDIR /app

EXPOSE 8080
ENTRYPOINT [ "/jre/bin/java", "-jar", "/app/<jar_file_name>.jar" ]

 

Stage 1temurin:<version>-alpine 이미지를 베이스 이미지로 사용하여 jdeps를 이용한 의존성 분석 및 분석 결과를 deps.info라는 이름으로 생성한다.

 

Stage 2Stage 1과 동일한 이미지와 생성된 의존성 목록을 바탕으로 jlink를 사용해 경량화된 커스텀 jre를 생성한다.

 

Stage 3alpine:<version>을 베이스로 Stage 2에서 생성된 커스텀 jre를 사용하여 최종 이미지를 생성한다. 이 과정에서 보안을 위해 별도의 유저를 생성하여 권한을 부여하고 jar 파일을 실행한다.

 

여기까지 작성한 Dockerfiledocker build -t <image_name>:<version> . 명령을 통해 이미지로 빌드시킨다. Dockerfile 설정을 잘못 적지 않았다면 성공적으로 확연하게 줄어든 43MB 용량의 이미지가 생성된 것을 확인할 수 있다.

비교를 위해 alpine jdk만을 써서 빌드한 이미지의 용량과 비교해보자. 아래는 비교를 위해 작성한 간단한 Dockerfile이다.

FROM eclipse-temurin:17-alpine

CMD ["./gradlew", "clean", "build"]

VOLUME /tmp

ARG JAR_FILE=./app.jar

COPY ${JAR_FILE} app.jar

EXPOSE 8080

ENTRYPOINT ["java","-jar","/app.jar"]

 

jdepsjlink를 사용하지 않았기 때문에 의존성이 최적화되지 않아 불필요한 의존성까지 모두 포함된 이미지가 생성된다.

리사이징된 이미지와 비교했을 때 이미지의 용량이 약 80~90% 감소됐다.

실행 또한 성공적으로 되는 것을 확인할 수 있다.

'DevOps' 카테고리의 다른 글

Observability (O11y)  (0) 2025.03.10
top 명령어 제대로 알고 쓰자  (0) 2025.02.04
[Terraform] 테라폼 기초 (3)  (1) 2024.08.28
[Terraform] 테라폼 기초 (2)  (0) 2024.08.19
[Helm] Helm Quick Start  (0) 2024.08.10
Comments