티스토리 뷰

개요

  • Java 애플리케이션은 JVM(Java Virtual Machine)이 함께 컨테이너에 배포되어야 하기 때문에 비교적 타 언어로 배포되는 컨테이너보다 용량이 매우 크므로, 이 부분을 해소하기 위한 경량화된 Dockerizng을 Amazon Crretto JDK를 이용하여 설명하고자한다.
  • 여기서는 Custom JRE를 생성하는 Docker의 Multi-Stage builds를 활용하여 실제 빌드하는 JAVA 애플리케이션을 경량화한다.

Stage 1

  • Stage 1에서는 애플리케이션 의존 관계를 분석하여 경량화된 Custom JRE를 만드는 부분에 중점을 둔다.
  • 부가적으로 빌드된 jar 파일을 필요한 파일들만 사용하기 위하여 압축 해제한다.
# Stage 1. Create custom JRE
FROM amazoncorretto:21-alpine AS jrebuilder

# Add the application jar to the container
COPY ./build/libs/hello-docker-*-SNAPSHOT.jar /app.jar

# Install binutils
RUN apk add --no-cache binutils

# Extract jar file and generate custom JRE using dependency
RUN mkdir -p /app && (cd /app; jar -xf /app.jar) \
    && DEPENDENCY=$(jdeps --ignore-missing-deps --print-module-deps --recursive --multi-release 21 --class-path="/app/BOOT-INF/lib/*" /app.jar) \
    && ${JAVA_HOME}/bin/jlink \
        --verbose \
        --add-modules ${DEPENDENCY} \
        --strip-debug \
        --no-man-pages \
        --no-header-files \
        --compress=2 \
        --output \
        customjre

Image

  • Custom JRE는 JDK에서 필요한 의존성만 추출해야 하므로 기본 이미지는 JDK 이미지로 사용하고, jrebuilder로 명명한다.

Copy application

  • Gradle을 이용하여 빌드된 jar파일은 /build/libs/ 경로 아래 생성되며, 해당 jar 파일을 사용하기 위해 컨테이너 내부로 복사한다.

Install Binutils

  • 컨테이너 내부에서 jlink 명령어를 사용하기 위해서 objcopy가 필요한데, 이는 binutils를 설치하여 사용할 수 있다.

  • 만일 해당 부분을 설치하지 않는다면 아래의 오류를 마주할 수 있다.

    Error: java.io.IOException: Cannot run program "objcopy": error=2, No such file or directory

Generate custom JRE

  • 이제 가장 중요한 Custom JRE를 만드는 부분이다.

  • "/app" 폴더를 만든 후 해당 경로에 애플리케이션 jar 파일을 압축 해제해준다.

  • 빌드된 jar 파일 구성은 아래와 같다.

    ./hello-docker
    ├─ /BOOT-INF
    │  ├─ /classes ## Folder where the class file where the
    │  │           ## JAVA file is compiled is stored.
    │  └─ /lib     ## Folder where the dependency injected jar
    │              ## library is stored.
    ├─ /META-INF   ## Folder where the meta data and setting data.
    └─ /org

Jdeps

  • Jdeps는 JAVA Class의 의존성 분석을 위한 도구로, 모듈화가 적용된 JAVA 9 버전 이상부터 사용이 가능하다.
--ignore-missing-deps외존성이 존재하지 않는 모듈은 제외.
--print-module-depsJlink에서 요구하는 포멧에 맞는 쉼표로 구분된 모듈 의존성 목록을 출력.
--recursive모든 런타임 종속성을 재귀적으로 탐색.
--multi-release ${VERSION}의존성을 분석할 JAVA 버전을 ${VERSION}에 명시. 단, 모듈화가 적용된 9 버전 이상만 적용.
--class-path="${PATH}"의존성을 분석할 class 파일들을 찾을 위치를 ${PATH}에 지정.

Jlink

  • Jlink는 Jdeps를 이용하여 분석된 의존성을 활용하여 최적화된 Custom JRE를 구성할 수 있는 도구이다.
--verbose진행 중 상세 내역을 출력.
--add-modules ${DEPENDENCY}jdeps를 이용하여 분석한 쉼표로 구분된 모듈 의존성 목록
--strip-debug디버그 정보를 제거.
--no-man-pagesman page(특정 명령이나 자원들의 메뉴얼을 출력하는 영역) 미사용.
--no-header-filesheader files(로직이 저장된 파일) 미사용.
--compress=2리소스 압축 여부. 0 : 미사용, 1 : 상수 문자열 공유, 2 : 압축을 의미하며, 보편적으로 2를 사용.
--output=${PATH}리소스를 저장할 위치를 ${PATH}에 지정.

Stage 2

  • Stage 2에서는 Stage 1에서 분석된 의존성을 통해 완성된 Custom JRE와 압축 해제한 jar에서 필요한 파일로 이미지를 구성하여 경량화된 JAVA 애플리케이션 컨테이너를 구성한다.
# Stage 2. Make container for application
FROM alpine:3.20
ENV JAVA_HOME=/jre
ENV PATH="${JAVA_HOME}/bin:${PATH}"
ARG DEPENDENCY=/app

# Add Maintainer Info
LABEL maintainer="GracefulSoul on <gracefulsoul@github.com>"

# Copy custom JRE
COPY --from=jrebuilder /customjre ${JAVA_HOME}

# Copy extract files in jar
COPY --from=jrebuilder ${DEPENDENCY}/BOOT-INF/lib ${DEPENDENCY}/lib
COPY --from=jrebuilder ${DEPENDENCY}/META-INF ${DEPENDENCY}/META-INF
COPY --from=jrebuilder ${DEPENDENCY}/BOOT-INF/classes ${DEPENDENCY}

# Move work directory
WORKDIR ${DEPENDENCY}

# Run application
ENTRYPOINT [ "java", "-cp", "${DEPENDENCY}:${DEPENDENCY}/lib/*", "gracefulsoul.HelloDockerApplication" ]

Image

  • Stage 1에서 Custom JRE를 만들었으므로, 경량화된 Alpine Linux 이미지를 아래의 환경 설정을 이용하여 사용한다.
  • JAVA_HOME 환경 변수를 추가하기 위하여 JAVA_HOME은 "/jre"로 준 후 PATH에 "JAVA_HOME/bin"을 추가한다.
  • "/app" 폴더를 전역 변수로 사용하기 위한 DEPENDENCY를 정의한다.

Copy custom JRE

  • Stage 1에서 생성한 Custom JRE가 저장된 "/customjre" 폴더를 "/jre"로 복사한다.

Copy extract files in jar

  • jar 파일에서 JAVA 애플리케이션 구동을 위해 필요한 아래의 폴더들을 복사한다.
  • 의존성 주입된 Library가 저장된 "/app/BOOT-INF/lib" 폴더를 "/app/lib" 폴더로 복사한다.
  • 메타 데이터와 설정 정보를 저장된 "/app/META-INF" 폴더를 "/app/META-INF" 폴더로 복사한다.
  • 컴파일된 class 파일이 저장된 "/app/BOOT-INF/classes" 폴더를 "/app" 폴더로 복사한다.

Move work directory

  • JAVA 애플리케이션 실행 위치인 "/app" 위치를 컨테이너 기본 위치로 설정한다.

Run application

  • ENTRYPOINT인 컨테이너가 실행될 때 기본으로 동작하는 명령어는 아래의 조합으로 완성한다.
  • "java" 명령어는 JAVA 애플리케이션 실행을 위한 명령어이다.
  • "-cp /app:/app/lib/*"은 classpath를 지정하는 명령어로, 컴파일된 class 파일 루트 위치인 "/app"과 의존성으로 추가된 Library들을 포함하는 "/app/lib/*"을 이어준다.
  • 실행하고자 하는 Main Class 이름을 "package.className" 형태인 "gracefulsoul.HelloDockerApplication"로 정의한다.

정리

  • 위에서 하나씩 살펴본 JAVA 애플리케이션 경량화 Docker Conatiner는 MSA 구성에 있어서 아주 기본적인 서비스 빌드 방법의 하나를 살펴보았다.
  • 애플리케이션을 돌리기 위한 컨테이너는 동작에 필요한 최소한의 리소스를 이용한 컨테이너 경량화는 배포 크기의 감소와 성능 향상, 비용 감소 등의 이점이 있으므로 선택이 아닌 필수이다.

Reference

Spring-Boot-Docker_Springio-Guides
Multi-Stage-builds-Docker
Amazon-Crretto
Binutils
Jdeps_Oracle-Document
Jlink_Oracle-Document
Alpine_Linux-Wiki
※ Sample Code는 여기에서 확인 가능합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함