Docker 이미지 용량 줄이기

5 분 소요

쿠버네티스에서 임의의 REST API를 호출하면서 테스트를 해볼 겸, Spring Boot에서 Member라는 타입과 이를 저장할 LinkedHashMap을 만들었다. 이 LinkedHashMap 데이터에 대해 CRUD기능을 수행하는 REST API를 구현하고 jar 파일로 빌드하였다.

그리고 도커 이미지로 빌드한 후 docker images 명령어로 생성된 이미지를 확인했는데, 별다른 기능이 없는 것에 비하여 용량이 크다.

$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-img (외부 빌드) latest e8bdaf354424 About an hour ago 670M

Dockerfile 내용

FROM java:8
ARG JAR_FILE=demo-0.0.1-SNAPSHOT.jar
ADD ${JAR_FILE} demo-spring.jar
ENTRYPOINT ["java","-jar","/demo-spring.jar"]

이미지 크기 줄이기

불필요한 공간을 점유하는 건 낭비이기도 하고 성능에도 영향을 줄 수 있다고 생각하는데, 찾아보니 이 문제를 해결할 몇 가지 방법이 존재한다.

가장 간단한(?) 방법으로 보이는 것은 자바 실행을 위한 기초 이미지에 경량화된 이미지를 사용하는 것이다.

사용되는 기초 이미지를 java에서, GCR(Google Container Registry)이 제공하는 gcr.io/distroless/java로 변경한다.

(distroless는 자바 실행을 위해 경량화된 이미지이다.)

FROM gcr.io/distroless/java:8
ARG JAR_FILE=demo-0.0.1-SNAPSHOT.jar
ADD ${JAR_FILE} demo-spring.jar
ENTRYPOINT ["java","-jar","/demo-spring.jar"]

도커 이미지를 빌드하고 생성된 이미지를 확인해본 결과, 513MB나 감소되었다.

REPOSITORY TAG IMAGE ID SIZE
spring-img-optional (외부 빌드 - 경량) latest 712139df72e7 157MB
spring-img (외부 빌드) latest e8bdaf354424 670MB

하지만 이 방법은 호스트에 JDK가 설치되어 있어서 미리 jar파일을 생성하고 컨테이너 이미지를 만든 경우이다.

Dockerfile에 java 기초 이미지가 포함되어 있는데, 왜 호스트에서 JDK를 설치해서 빌드하고 추가하는 것일까?

답을 먼저 제시하자면, 일반적으로 내부 빌드 시 도커 이미지의 크기가 증가하기 때문이다.

이제 컨테이너 내부에서 빌드한 예시를 살펴보자.

컨테이너 내부에서 빌드하기

아래와 같이 Dockerfile을 작성하고 빌드를 수행한다.

FROM java:8
LABEL description="Spring Demo"
EXPOSE 8080
RUN git clone https://github.com/ksh021144/spring-demo.git
WORKDIR spring-demo
RUN chmod 700 mvnw
RUN ./mvnw clean package
RUN ls -alF
RUN mv target/demo-0.0.1-SNAPSHOT.jar /opt/demo-0.0.1-SNAPSHOT.jar
WORKDIR /opt
ENTRYPOINT ["java", "-jar", "demo-0.0.1-SNAPSHOT.jar"]
$ docker build -t spring-img .
(생략)
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.7/commons-lang3-3.7.jar (500 kB at 113 kB/s)
Downloaded from central: https://repo.maven.apache.org/maven2/com/google/guava/guava/28.2-android/guava-28.2-android.jar (2.6 MB at 508 kB/s)
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:42 min
[INFO] Finished at: 2021-08-20T07:39:51Z
[INFO] ------------------------------------------------------------------------
Removing intermediate container 2b353f427214
 ---> 7d170910c167
Step 8/11 : RUN ls -alF
 ---> Running in ba8f5ac6f163
total 32
drwxr-xr-x. 1 root root    20 Aug 20 07:38 ./
drwxr-xr-x. 1 root root     6 Aug 20 07:39 ../
drwxr-xr-x. 8 root root   163 Aug 20 07:37 .git/
-rw-r--r--. 1 root root   395 Aug 20 07:37 .gitignore
drwxr-xr-x. 3 root root    21 Aug 20 07:37 .mvn/
-rw-r--r--. 1 root root   132 Aug 20 07:37 README.md
-rwx------. 1 root root 10070 Aug 20 07:37 mvnw*
-rw-r--r--. 1 root root  6608 Aug 20 07:37 mvnw.cmd
-rw-r--r--. 1 root root  1515 Aug 20 07:37 pom.xml
drwxr-xr-x. 4 root root    30 Aug 20 07:37 src/
drwxr-xr-x. 9 root root   233 Aug 20 07:39 target/
Removing intermediate container ba8f5ac6f163
 ---> a6ffa577b4f4
Step 9/11 : RUN mv target/demo-0.0.1-SNAPSHOT.jar /opt/demo-0.0.1-SNAPSHOT.jar
 ---> Running in 9b2dcb43eb3b
Removing intermediate container 9b2dcb43eb3b
 ---> 1f894a72a1a5
Step 10/11 : WORKDIR /opt
 ---> Running in 11a99ad40a82
Removing intermediate container 11a99ad40a82
 ---> 7aba119c7657
Step 11/11 : ENTRYPOINT ["java", "-jar", "demo-0.0.1-SNAPSHOT.jar"]
 ---> Running in cb7c96c0fcf3
Removing intermediate container cb7c96c0fcf3
 ---> dfa60bceb5c6
Successfully built dfa60bceb5c6
Successfully tagged spring-img:latest
$ docker images
REPOSITORY TAG IMAGE ID SIZE
spring-img (내부 빌드) latest dfa60bceb5c6 786MB
spring-img-optional (외부 빌드 - 경량) latest 712139df72e7 157MB
spring-img (외부 빌드) latest e8bdaf354424 670MB

내부 빌드외부 빌드만 확인하면 되는데 외부 빌드를 했을 때보다 도커 이미지의 크기가 커진 모습이다.

이처럼 호스트에서 jar 빌드를 하지 않고 컨테이너 내부에서 jar 빌드를 진행하면, 빌드 과정에서 생성된 파일들과 다운로드 된 라이브러리 캐시들이 남아있기 때문에 이미지 크기가 더 커진다.

즉, 자바 소스를 호스트에서 빌드했는지 컨테이너 내부에서 빌드했는지에 따라 빌드되는 이미지 크기에 차이가 있다.

Multi-Stage Build

Multi-Stage Build를 이용하면 최종 이미지의 용량을 줄이면서도 호스트에 빌드 도구를 설치할 필요가 없다.

(Multi-Stage Build를 사용하려면 Docker v17.06.0-ce 이상이어야 한다. 물론 이렇게 생성된 이미지는 자체는 Docker v17.06.0-ce 이하에서도 사용할 수 있다.)

# Step 1
FROM java:8 AS inner-build # 이미지에 alias를 설정
LABEL description="jar builder"
RUN git clone https://github.com/ksh021144/spring-demo.git
WORKDIR spring-demo
RUN chmod 700 mvnw
RUN ./mvnw clean package

# Step 2
FROM gcr.io/distroless/java:8
LABEL description="Spring Demo"
EXPOSE 8080

# inner-build에서 빌드된 jar를 복사
COPY --from=inner-build spring-demo/target/demo-0.0.1-SNAPSHOT.jar /opt/demo-0.0.1-SNAPSHOT.jar
WORKDIR /opt
ENTRYPOINT ["java", "-jar", "demo-0.0.1-SNAPSHOT.jar"]
$ docker build -t spring-multistage-img .
(생략)
Downloaded from central: https://repo.maven.apache.org/maven2/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar (8.8 kB at 2.3 kB/s)
Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-lang3/3.7/commons-lang3-3.7.jar (500 kB at 122 kB/s)
Downloaded from central: https://repo.maven.apache.org/maven2/com/google/guava/guava/28.2-android/guava-28.2-android.jar (2.6 MB at 606 kB/s)
[INFO] Replacing main artifact with repackaged archive
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:42 min
[INFO] Finished at: 2021-08-20T08:04:21Z
[INFO] ------------------------------------------------------------------------
Removing intermediate container caaf7d555906
 ---> 5ec92d0276ba
Step 7/12 : FROM gcr.io/distroless/java:8
 ---> b762aad6c014
Step 8/12 : LABEL description="Spring Demo"
 ---> Running in 056c7fed24bc
Removing intermediate container 056c7fed24bc
 ---> b0a71082d830
Step 9/12 : EXPOSE 8080
 ---> Running in 3ff3b06c4138
Removing intermediate container 3ff3b06c4138
 ---> 4284eecbd680
Step 10/12 : COPY --from=inner-build spring-demo/target/demo-0.0.1-SNAPSHOT.jar /opt/demo-0.0.1-SNAPSHOT.jar
 ---> 9826a119e43d
Step 11/12 : WORKDIR /opt
 ---> Running in 2a14f636b9b2
Removing intermediate container 2a14f636b9b2
 ---> 13d12a896724
Step 12/12 : ENTRYPOINT ["java", "-jar", "demo-0.0.1-SNAPSHOT.jar"]
 ---> Running in ef4cc8f5f8d0
Removing intermediate container ef4cc8f5f8d0
 ---> 6cb47759e7e4
Successfully built 6cb47759e7e4
Successfully tagged spring-multistage-img:latest
$ docker images
REPOSITORY TAG IMAGE ID SIZE
spring-multistage-img (멀티 스테이지) latest 6cb47759e7e4 157MB
<none> <none> 5ec92d0276ba 759MB
spring-img (내부 빌드) latest dfa60bceb5c6 786MB
spring-img-optional (외부 빌드 - 경량) latest 712139df72e7 157MB
spring-img (외부 빌드) latest e8bdaf354424 670MB

멀티 스테이지로 빌드된 컨테이너(spring-multistage-img)의 용량을 확인하면 spring-img-optional과 같다.

두 번째에 <none>으로 표시되는 이미지는 이름이 없는 Dangling image인데, 멀티 스테이지 과정 중 자바 소스를 빌드할 때 생성된 이미지이다. Dangling image는 삭제해주면 된다.

$ docker rmi $(docker images -f dangling=true -q)

Deleted: sha256:5ec92d0276ba8174fd188302ab24e810a7f5bee57dbdf3fff711cb3fca60e5a7
(생략)

컨테이너 실행하기

docker run -d -p 8080:8080 --name multistage-spring --restart always spring-multistage-img

브라우저에서 http://192.168.56.10:8080/swagger-ui.html으로 접속되는 것을 확인할 수 있다.

swagger

멤버에 대해서 간단한 CRUD 수행이 가능한데, 처음에는 저장된 정보가 없어서 등록을 해주어야 한다.

Insomnia REST를 활용해서 POST 요청을 통해 멤버를 추가한다.

insomnia

이제 GET 요청을 보내면 정보 조회가 가능하다.

http://192.168.56.10:8080/api/member/10001

{
    message: "정보 조회 성공",
    info: {
        id: "10001",
        name: "kim",
        email: "email@mail.com",
        age: 24
    }
}

http://192.168.56.10:8080/api/member

{
    members: [
        {
            id: "10001",
            name: "kim",
            email: "email@mail.com",
            age: 24
        }
    ],
    message: "전체 멤버 조회 성공"
}

여기까지 도커 이미지를 만들고, 이미지 크기를 경량화 하고, 실행해보았다.


하지만 이렇게 생성한 도커 이미지를 쿠버네티스에서 pod로 생성하려고 하면, 기본적으로 도커 허브에서 받으려고 하기 때문에 에러가 발생한다.

호스트에서 생성한 이미지를 쿠버네티스에서 사용하려면 모든 노드에서 공통적으로 접근하는 레지스트리(저장소)가 필요하기 때문이다. 기본적으로는 도커 허브에 업로드해서 해결할 수 있기는 한데, 직접 만든 이미지가 외부에 공개되는 것을 원하지 않은 경우에는 사설 저장소를 사용해야 하고 무료 사용자는 사설 저장소 사용에 제약이 있다. (무료 사용자는 1개의 사설 저장소만 사용할 수 있고 이미지 Pull 횟수에 제한이 있다.)

제약 없이 사용할 수 있는 저장소가 필요한 경우 레지스트리를 직접 구축해야 하는데 도커 레지스트리(Docker Registry)를 활용할 수 있다.

(도커 레지스트리의 기능이 풍부하지 않다고는 하지만, 컨테이너를 하나만 구동하여 사용할 수 있어서 설치가 간편하고 내부에서 테스트 목적으로 사용할 때 적합하다고 한다.)

차후에는 도커 레지스트리를 활용하여 직접 생성한 도커 이미지(Spring Boot REST API)를 쿠버네티스 환경에 올려서 사용할 수 있도록 해보려고 한다.