본문 바로가기

DevOps/Docker

Spring Boot Application을 Docker Image로 생성하기 - 2. Docker Image Layer 활용

반응형

내용이 3편에 걸쳐 나누어져 있습니다.

 

Spring Boot Application을 Docker Image로 생성하기 - 1. Docker file (with Jar)

Spring Boot Application을 Docker Image로 생성하기 - 2. Docker Image Layer 활용

Spring Boot Application을 Docker Image로 생성하기 - 3. jib plugin을 활용 + 배포

Case 2. Layer를 세분화 해서 Image 생성

Docker image는 여러 개의 Layer로 나뉘어서 저장/관리되는데 기존 Image가 Update되더라고 변경되지 않는 Layer나 다른 Image들과 공유하는 Layer는 변경없이 그대로 다시 사용된다. 그런데 jar 파일을 기반으로 Docker image를 만들게 되면 약간의 Code만 수정되더라도 Jar 파일이 변경되서 Jar 파일을 포함하는 Layer 전체가 교체되야 한다. 이는 개발 과정에서 Docker Image를 생성하는 시간이나 사용자에게 배포하는 시간이 커지게 되는 문제를 야기한다. 예제로 사용했던 Application의 경우에는 <pic 1>과 같이 도식화 될 수 있다.

 

<pic 1> jar 파일 기반으로 생성한 Docker image 구조

 

위와 같이 jar 파일을 기반으로 해서 Dockerfile은 간단하다는 면에서 큰 장점이 있지만, Docker Image 생성/배포/관리 측면에서 커다란 단점이 있고 여기에 대해 Spring Guide 문서에서는 아래와 같이 언급하고 있다.

 

There is a clean separation between dependencies and application resources in a Spring Boot fat jar file, and we can use that fact to improve performance.

즉, 하나의 jar 파일을 거의 변경되지 않는 Dependency 부분과 자주 변경되는 application code나 resource를 분리해서 서로 다른 Layer에서 관리하면 Docker Image 생성 및 배포 시에  성능 향상을 기대할 수 있다는 말이다. 이 내용은 <pic 2>와 같이 도식화할 수 있다. Build 시에 다시 만들어지는 Layer의 크기가 제일 윗부분에 국한되게 된다. 전체 Image의 크기가 123MB인데 이중 Code 변경을 반영한 부분은 4Kb에 불과하기 때문에 Docker Image 생성/배포에 유리하다.

 

<pic 2> Layer를 분리해서 생성한 Docker Image 구조

<pic 2>와 같이 참조 Library를 별도의 Layer로 구성해서 Docker image를 생성하기 위해, 아래와 같이 gradle build를 통해 만들어진 jar 파일을 새로 만든 dockerbuild 디렉토리로 복사한 후 'jar -xf' command를 이용해서 구성 파일과 디렉토리를 분해했다. 

# Gradle Build는 동일
[browndwarf@ha-machine-1 ~]$mkdir dockerwebservice && cd dockerwebservice
[browndwarf@ha-machine-1 dockerwebservice]$ ./gradlew clean build
Starting a Gradle Daemon (subsequent builds will be faster)

> Task :test
2020-06-05 10:52:00.360  INFO 18140 --- [       Thread-5] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

BUILD SUCCESSFUL in 8s
6 actionable tasks: 6 executed
...

# Docker Image를 만들 디렉토리를 새로 성성한 후, Jar 파일을 복사해서 jar -xf 를 실행
[browndwarf@ha-machine-1 dockerwebservice]$ mkdir dockerbuild && cp build/libs/*.jar dockerbuild && cd dockerbuild
[browndwarf@ha-machine-1 dockerbuild]$ jar -xf *.jar
...

# 최종 폴더 구조
[browndwarf@ha-machine-1 dockerbuild]$ ls -la
total 17916
drwxrwxr-x. 5 dohoon dohoon       81  6월  5 10:56 .
drwxrwxr-x. 9 dohoon dohoon     4096  6월  5 10:55 ..
drwxrwxr-x. 4 dohoon dohoon       32  6월  5 10:51 BOOT-INF
-rw-rw-r--. 1 dohoon dohoon 18339606  6월  5 10:55 dockerwebservice-1.0.jar
drwxrwxr-x. 2 dohoon dohoon       25  6월  5 10:51 META-INF
drwxrwxr-x. 3 dohoon dohoon       29  1월 16 11:12 org

그리고 같은 디렉토리에 Dockerfile을 <code 1>과 같이 작성한다. 

FROM openjdk:8-jdk-alpine
ENV	USE_PROFILE default
ENV NORM_PROP=
ENV DIRECT_MSG=
COPY ./BOOT-INF/lib	/app/lib
COPY ./META-INF /app/META-INF
COPY ./BOOT-INF/classes /app
ENTRYPOINT ["java",  "-Dspring.profiles.active=${USE_PROFILE}", "-Dnormal.prop=${NORM_PROP}", "-Dconfig.healthmsg=${DIRECT_MSG}", "-cp",  "/app:/app/lib/*", "com.browndwarf.dockerwebservice.DockerwebserviceApplication"]
<Code 1> Layer를 세분화한 Dockerfile

 

전체 8 line 중에 아래 4개 line에 대해 부연적으로 설명하자면 

  • COPY ./BOOT-INF/lib /app/lib: Application에서 의존 관계를 가지는 Library들을 Docker Image의 /app/lib로 복사
  • COPY ./META-INF /app/META-INF : Application의 meta data를 Docker Image의 /app/META-INF로 복사
  • COPY ./BOOT-INF/classes /app /app : Application class file들을 /app으로 복사
  • ENTRYPOINT : java -cp (or -classpath) command으로 /app/lib, /app을path를 설정한 후, main function이 포함된 class를 지정해서 실행하게 설정.

'docker build'를 실행해서 아래와 같이 0.1 Tag를 가진 Image가 만든다. 

# Tag 0.1로 해서 Docker Image 생성
[browndwarf@ha-machine-1 dockerbuild]$ docker build -t dockerservice:0.1 .
Sending build context to Docker daemon  36.85MB
Step 1/8 : FROM openjdk:8-jdk-alpine
 ---> a3562aa0b991
Step 2/8 : ENV  USE_PROFILE default
 ---> Using cache
 ---> 5bc9a9661f4f
Step 3/8 : ENV NORM_PROP=
 ---> Using cache
 ---> 3c9321e82c92
Step 4/8 : ENV DIRECT_MSG=
 ---> Using cache
 ---> 4ac9252f80a7
Step 5/8 : COPY ./BOOT-INF/lib  /app/lib
 ---> 0a1682b3eda2
Step 6/8 : COPY ./META-INF /app/META-INF
 ---> 7446fbc26f2b
Step 7/8 : COPY ./BOOT-INF/classes /app
 ---> f6560c509df7
Step 8/8 : ENTRYPOINT ["java",  "-Dspring.profiles.active=${USE_PROFILE}", "-Dnormal.prop=${NORM_PROP}", "-Dconfig.healthmsg=${DIRECT_MSG}", "-cp",  "/app:/app/lib/*", "com.browndwarf.dockerwebservice.DockerwebserviceApplication"]
 ---> Running in fc7b49d1bce7
Removing intermediate container fc7b49d1bce7
 ---> c3ea279376fb
Successfully built c3ea279376fb
Successfully tagged dockerservice:0.1

# 생성된 Docker image 확인
[browndwarf@ha-machine-1 dockerbuild]$ docker images
REPOSITORY                   TAG                     IMAGE ID            CREATED             SIZE
dockerservice                0.1                     c3ea279376fb        8 seconds ago       123MB
dockerservice                latest                  805a07017c98        1 days ago          123MB

최종 만들어진 Image는 기존에 Jar 파일을 이용해서 만든 Docker Image(tag : latest)와 큰 차이 없이 모두 123MB로 되어있다. 그 후에 Code를 살짝 수정해서 Build 한 후, 0.2 Tag로 image를 만들어보면 <pic 2>에서 도식화 했던 <version 1>과 <version 2>에서 가리킨 내용을 확인할 수 있다.

[browndwarf@ha-machine-1 dockerbuild]$ docker build -t dockerservice:0.2 .
Sending build context to Docker daemon  36.85MB
Step 1/8 : FROM openjdk:8-jdk-alpine
 ---> a3562aa0b991
Step 2/8 : ENV  USE_PROFILE default
---> Using cache
 ---> 5bc9a9661f4f
Step 3/8 : ENV NORM_PROP=
---> Using cache
 ---> 3c9321e82c92
Step 4/8 : ENV DIRECT_MSG=
---> Using cache
 ---> 4ac9252f80a7
Step 5/8 : COPY ./BOOT-INF/lib  /app/lib
---> Using cache
 ---> 0a1682b3eda2
Step 6/8 : COPY ./META-INF /app/META-INF
---> Using cache
 ---> 7446fbc26f2b
Step 7/8 : COPY ./BOOT-INF/classes /app
 ---> c69f37ecf7cb
Step 8/8 : ENTRYPOINT ["java",  "-Dspring.profiles.active=${USE_PROFILE}", "-Dnormal.prop=${NORM_PROP}", "-Dconfig.healthmsg=${DIRECT_MSG}", "-cp",  "/app:/app/lib/*", "com.browndwarf.dockerwebservice.DockerwebserviceApplication"]
 ---> Running in 9a9f497057d5
Removing intermediate container 9a9f497057d5
 ---> e19cbba48d8f
Successfully built e19cbba48d8f
Successfully tagged dockerservice:0.2
[browndwarf@ha-machine-1 dockerbuild]$ docker images
REPOSITORY                   TAG                     IMAGE ID            CREATED              SIZE
dockerservice                0.2                     e19cbba48d8f        About a minute ago   123MB
dockerservice                0.1                     c3ea279376fb        18 minutes ago       123MB
dockerservice                latest                  805a07017c98        6 days ago           123MB

v0.2 Image가 만들어지는 단계를 보면 Step 6/8까지는 Cache를 이용하고 있다는 것을 알 수 있다.(1/8과 같은 Base Image는 기존에 존재하는 것을 사용하게 된다.) 수정된 Code가 반영되는 부분은 7/8 이므로 이전 6단계까지는 기존 v0.1 Image와 Layer를 동일하게 가져가고, 수정된 부분은 Step 7/8 에서 반영되게 된다. 결과적으로는 123MB중에 4KB의 Layer만 새로 만들어진다. <pic 1>에서 약 18MB의 Layer가 교체되야 하는 상황에 비해 build나 Image 배포 시간을 단축할 수 있다는 점에서 유리하다.

 

결과는 이전 Jar 파일을 기준으로 실행할 때와 동일하다.

# Docker image 실행
[browndwarf@ha-machine-1 dockerbuild]$ docker run -d --rm --name dockerservice -p 8080:8080 -e USE_PROFILE=prod -e NORM_PROP=docker01 -e DIRECT_MSG=Hi  dockerservice:0.1
439c0221ed0610791891dbe10d9259a6caf42b68e9f05901f82faa3f78ad433d

# Container Test
[browndwarf@ha-machine-1 ~]$ curl localhost:8080/health/
Hi
[browndwarf@ha-machine-1 ~]$ curl localhost:8080/whoami/
HostName : 439c0221ed06, IP Address : 172.17.0.2

 

위와 같은 이유로 Dockerfile은 최적화될 필요가 있는데, 이 부분은 subicura님의 블로그의 '도커 이미지 리팩토링' 부분을 참고하시면 좋다.

 

Remark. 이 방법으로 Docker Image를 만들면 Jar 파일의 제약사항(?)도 피할 수 있다는 점도 확인했다. (참조)