Container

springboot 컨테이너 이미지 사이즈 경량화 방법

seungkyua@gmail.com 2023. 10. 25. 11:18
반응형

스프링 부트 기반으로 개발한 어플리케이션을 컨테이너 이미지로 만들 때 이미지 사이즈를 줄이는 방법에 대해서 알아보자.

먼저, sample source 를 다운 받는다.

$ git clone https://github.com/seungkyua/springboot-docker.git
$ cd springboot-docker

다운 받은 소스에서 mvn 으로 package 를 빌드한다.

$ mvn package

빌드 후에 소스의 디렉토리 구조는 다음과 같다.

$ tree . -L 2
.
├── LICENSE
├── README.md
├── docker
│   ├── Dockerfile
│   ├── Dockerfile2
│   └── Dockerfile3
├── logs
│   └── access_log.log
├── pom.xml
├── scripts
│   └── run.sh
├── src
│   └── main
├── target
│   ├── classes
│   ├── example-0.0.1-SNAPSHOT.jar
│   ├── example-0.0.1-SNAPSHOT.jar.original
│   ├── generated-sources
│   ├── maven-archiver
│   └── maven-status
└── work
    └── Tomcat

오늘은 자바 소스는 상관없이 도커 이미지 빌드에 대해서만 설명하므로 Dockerfile 만 살펴보자.

컨테이너 이미지 만들기 - 기본편

docker/Dockerfile2 을 보면 아래와 같다.

FROM openjdk:17.0.2-jdk-slim
MAINTAINER Ahn Seungkyu

ARG JAR_FILE=example-0.0.1-SNAPSHOT.jar

RUN mkdir -p /app
WORKDIR /app

COPY target/${JAR_FILE} /app/app.jar
COPY scripts/run.sh .
ENTRYPOINT ["./run.sh"]

base 이미지로 openjdk:17.0.2-jdk-slim 이미지를 사용하였고 target 아래의 빌드된 jar 파일을 복사하여 실행하는 방법이다.

실행 명령어는 scripts/run.sh 파일을 보면 알 수 있다.

#!/bin/sh

java ${JAVA_OPTS} -jar app.jar ${@}

JAVA_OPTS 는 환경 변수 이므로 컨테이너를 실행할 때 해당 값을 전달하는 것이 가능하다.

이제 컨테이너 이미지를 빌드하면 그 사이즈는 다음과 같다.

$ docker build -t seungkyua/springboot-docker -f docker/Dockerfile2 .

------- output -----------
[+] Building 2.6s (11/11) FINISHED                                                                                                                                                                                                               docker:desktop-linux
 => [internal] load build definition from Dockerfile2                                                                                                                                                                                                            0.0s
 => => transferring dockerfile: 442B                                                                                                                                                                                                                             0.0s
 => [internal] load .dockerignore                                                                                                                                                                                                                                0.0s
 => => transferring context: 2B                                                                                                                                                                                                                                  0.0s
 => [internal] load metadata for docker.io/library/openjdk:17.0.2-jdk-slim                                                                                                                                                                                       2.2s
 => [auth] library/openjdk:pull token for registry-1.docker.io                                                                                                                                                                                                   0.0s
 => CACHED [1/5] FROM docker.io/library/openjdk:17.0.2-jdk-slim@sha256:aaa3b3cb27e3e520b8f116863d0580c438ed55ecfa0bc126b41f68c3f62f9774                                                                                                                          0.0s
 => [internal] load build context                                                                                                                                                                                                                                0.0s
 => => transferring context: 291B                                                                                                                                                                                                                                0.0s
 => [2/5] RUN mkdir -p /app                                                                                                                                                                                                                                      0.1s
 => [3/5] WORKDIR /app                                                                                                                                                                                                                                           0.0s
 => [4/5] COPY target/example-0.0.1-SNAPSHOT.jar /app/app.jar                                                                                                                                                                                                    0.1s
 => [5/5] COPY scripts/run.sh .                                                                                                                                                                                                                                  0.0s
 => exporting to image                                                                                                                                                                                                                                           0.1s
 => => exporting layers                                                                                                                                                                                                                                          0.1s
 => => writing image sha256:6d6ba6764805971eef0532e21ec28feb6308ddb04bb650a7d087ab689d0d65be                                                                                                                                                                     0.0s
 => => naming to docker.io/seungkyua/springboot-docker
$ docker image ls 
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
seungkyua/springboot-docker   latest    6d6ba6764805   54 seconds ago   473MB

이미지 크기가 473M 로 만들어졌다. 어플리케이션 jar 파일의 크기가 68M 이므로 JVM 을 포함하는 이미지가 405M 의 크기가 된다는 의미이다.

보통 이미지를 작게하기 위해서 base 이미지 태그로 alpine 이나 slim 을 많이 사용한다. 여기서 slim 을 사용했는데도 이 정도 크기라면 사이즈가 작다고 할 수 없다.

더 작은 사이즈를 만들기 위해서 alpine 이미지를 찾아서 적용해 보자.

base 이미지를 amazoncorretto 로 변경하고 다시 사이즈를 비교해 보자.

FROM amazoncorretto:17-alpine
MAINTAINER Ahn Seungkyu

ARG JAR_FILE=example-0.0.1-SNAPSHOT.jar

RUN mkdir -p /app
WORKDIR /app

COPY target/${JAR_FILE} /app/app.jar
COPY scripts/run.sh .
ENTRYPOINT ["./run.sh"]
$ docker build -t seungkyua/springboot-docker -f docker/Dockerfile2 .

$ docker image ls
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
seungkyua/springboot-docker   latest    ed08524545ba   31 seconds ago   358MB
<none>                        <none>    6d6ba6764805   7 minutes ago    473MB

조회되는 이미지를 보면 base 이미지만을 바꿨을 뿐인데 358M 로 줄어들었다.

컨테이너 이미지 만들기 - 멀티 스테이지

컨테이너 이미지 사이즈를 줄이기 위해서 멀티 스테이지를 써야 한다는 말을 들어봤을 것이다. 여기서도 사이즈를 줄이기 위해 멀티 스테이지를 사용해 보자.

docker/Dockerfile3 를 보면 멀티 스테이지를 어떻게 구성하는지 알 수 있다.

# syntax=docker/dockerfile:1
FROM maven:3.8.5-openjdk-17 as build
MAINTAINER Ahn Seungkyu

WORKDIR /app
COPY . /app
RUN --mount=type=cache,target=/root/.m2 mvn -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true package

FROM amazoncorretto:17-alpine
MAINTAINER Ahn Seungkyu

ARG JAR_FILE=example-0.0.1-SNAPSHOT.jar

WORKDIR /app
COPY --from=build /app/target/${JAR_FILE} /app/app.jar
COPY scripts/run.sh /app/
ENTRYPOINT ["./run.sh"]

빌드하는 이미지와 런타임에서 실행되는 이미지가 나눠져 있다.

# syntax=docker/dockerfile:1 는 이미지 안에서 소스 빌드를 할 때 디펜던시가 있는 파일을 매번 가져오지 말고 한 번만 가져와서 캐싱하여 효율적으로 사용하고자 할 때 사용한다.

--mount=type=cache,target=/root/.m2 는 로컬에 저장해서 재활용하자는 의미이고 -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true 메이븐으로 패키지할 때 jvm 이 ssl 인증서 없이 사용하기 위해서 추가되었다.

첫번째는 어플리케이션을 빌드하기 위해 필요한 이미지 정의이고 두번째는 빌드된 어플리케이션을 복사하는 이미지 정의로 두번째는 앞서 기본편에서 설명한 것과 동일하다.

jar 파일만 빌드해서 복사하는 구조라 실제로 이미지 차이는 없을 것이다. 다만, 빌드를 이미지를 만들 때 로컬 환경 구성 없이도 만들 수 있다는 장점이 있다.

$ docker build -t seungkyua/springboot-docker -f docker/Dockerfile3 .

$ docker image ls
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
seungkyua/springboot-docker   latest    33ed3cbf0300   31 seconds ago   358MB
<none>                        <none>    ed08524545ba   4 minutes ago    358MB
<none>                        <none>    6d6ba6764805   7 minutes ago    473MB

컨테이너 이미지 만들기 - alpine 에 추가

여전히 사이즈가 358M 로 작지 않은 사이즈이다.

이제 기본 alpine 이미지에 JRE 와 어플리케이션을 설치하는 방법으로 이미지 사이즈를 줄여보자.

docker/Dockerfile 을 살펴보자.

FROM amazoncorretto:17-alpine3.18 as builder-jre

RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main/ binutils=2.41-r0

RUN $JAVA_HOME/bin/jlink \
         --module-path "$JAVA_HOME/jmods" \
         --verbose \
         --add-modules ALL-MODULE-PATH \
         --strip-debug \
         --no-man-pages \
         --no-header-files \
         --compress=2 \
         --output /jre

#=========================================================================

# syntax=docker/dockerfile:1
FROM maven:3.8.5-openjdk-17 as build
MAINTAINER Ahn Seungkyu

WORKDIR /app
COPY . /app
RUN --mount=type=cache,target=/root/.m2 mvn -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true package

#=========================================================================

FROM alpine:3.18.4
MAINTAINER Ahn Seungkyu

ENV JAVA_HOME=/jre
ENV PATH="$JAVA_HOME/bin:$PATH"
ARG JAR_FILE=example-0.0.1-SNAPSHOT.jar

COPY --from=builder-jre /jre $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 --from=build /app/target/${JAR_FILE} /app/app.jar
COPY scripts/run.sh /app/

WORKDIR /app
EXPOSE 8080

ENTRYPOINT ["./run.sh"]

이미지가 3개로 구성되어 있다.

첫번째는 작은 사이즈의 jre 를 만드는 이미지, 두번째는 어플리케이션을 빌드하는 이미지, 마지막으로 세번째는 작은 alpine 베이스 이미지에 jre 와 빌드된 어플리케이션 jar 파일을 복사하는 이미지이다.

첫번째 이미지 만드는 부분에서 binutils 을 설치하는데 다운 받을 리파지토리를 지정하여 에러가 없게 한다. binutils 는 jre 을 만들 때 strip-debug 옵션을 사용하기 위해서 설치한다.

세번째 이미지 만드는 부분에서 사용자를 추가하는 로직이 있는데 이는 보안상 이미지내 실행 프로세스를 root 가 아닌 지정된 사용자로 하기 위해서 일반적으로 추가한다.

이미지 사이즈는 다음과 같다.

$ docker build -t seungkyua/springboot-docker -f docker/Dockerfile .

$ docker image ls                                                   
REPOSITORY                    TAG       IMAGE ID       CREATED          SIZE
seungkyua/springboot-docker   latest    f5dc2f994864   28 seconds ago   168MB
<none>                        <none>    ed08524545ba   24 minutes ago   358MB
<none>                        <none>    6d6ba6764805   31 minutes ago   473MB

이미지가 최종적으로 168M 로 줄어들었다.

이제 실제 이미지가 정상적으로 실행되는지 확인해 보자.

mysql 을 띄우고 어플리케이션을 띄운 후에 curl 로 데이터를 입력해 본다.

1. mysql 실행

$ mkdir -p ~/.docker-data/mysql

$ docker run --cap-add=sys_nice -d --restart=always -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password \
-v ~/.docker-data/mysql:/var/lib/mysql \
--name mysql-ask mysql:8.0.34 \
--character-set-server=utf8mb4 \
--collation-server=utf8mb4_unicode_ci

2. database 생성

$ docker exec -it mysql-ask bash
# mysql -uroot -ppassword
mysql> create database if not exists order_service;

3. 어플리케이션 실행

$ docker run -d --name springboot --rm -p 19090:19090 --link mysql-ask:localhost seungkyua/springboot-docker

4. 데이터 조회

$ curl -X POST http://127.0.0.1:19090/orders \
   -H "Content-Type: application/json" \
   -d '{"customerId": 1, "orderTotal": 12.23}'

--- output ---
{"orderId":1}

정상적으로 어플리케이션이 실행되어 동작하는 것을 확인할 수 있다.

반응형