Custom JRE로 경령화된 Dockering I

업데이트:

개요

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

  • 컨테이너 내부에서 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
    

Jdeps5

  • Jdeps는 JAVA Class의 의존성 분석을 위한 도구로, 모듈화가 적용된 JAVA 9 버전 이상부터 사용이 가능하다.
Option Description
–ignore-missing-deps 외존성이 존재하지 않는 모듈은 제외.
–print-module-deps Jlink에서 요구하는 포멧에 맞는 쉼표로 구분된 모듈 의존성 목록을 출력.
–recursive 모든 런타임 종속성을 재귀적으로 탐색.
–multi-release ${VERSION} 의존성을 분석할 JAVA 버전을 ${VERSION}에 명시. 단, 모듈화가 적용된 9 버전 이상만 적용.
–class-path=”${PATH}” 의존성을 분석할 class 파일들을 찾을 위치를 ${PATH}에 지정.
  • Jlink는 Jdeps를 이용하여 분석된 의존성을 활용하여 최적화된 Custom JRE를 구성할 수 있는 도구이다.
Option Description
–verbose 진행 중 상세 내역을 출력.
–add-modules ${DEPENDENCY} jdeps를 이용하여 분석한 쉼표로 구분된 모듈 의존성 목록
–strip-debug 디버그 정보를 제거.
–no-man-pages man page(특정 명령이나 자원들의 메뉴얼을 출력하는 영역) 미사용.
–no-header-files header 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 Linux7 이미지를 아래의 환경 설정을 이용하여 사용한다.
  • 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

※ Sample Code는 여기에서 확인 가능합니다.

댓글남기기