Docker로 Spring 서비스를 운영 해보자 (Docker-2)

Docker image 만들기

Docker로 서비스를 운영하기 전, 가장먼저 해야 할 일은 Docker image를 만드는 것 입니다.
이번 포스팅에서는 Docker 운영배포를 위한 예제로써, Spring boot MVC로 구성한 간단한 User API 프로젝트가 이미 구성되어있다는 전제하여 진행을 하겠습니다.

가장먼저 우리가 해야 할 일은 Docker image를 만드는 일 입니다. Docker image를 생성 하기 위해서는 이미지화를 위해 프로젝트 정보를 명시하는 Dockerfile을 정의해 주어야 합니다.

Dockerfile은 프로젝트 최상단 root path에 만들어주시면 됩니다.

1

FROM java:8
ARG JAR_FILE=build/libs/example-api-0.0.1-SNAPSHOT.jar  
ADD ${JAR_FILE} sample.jar 
ENTRYPOINT ["java","-Xms1024m","-Xmx1024m","-jar","/sample.jar"]  

Dockerfile에서 사용하는 명령어는 다음과 같습니다.

  • FROM : 도커 이미지의 기반이 되는 이미지를 지정합니다.
  • MAINTANER : 이미지를 생성한 개발자 정보를 기록합니다. (deprecated Docker 1.13)
  • LABEL : 이미지에 메타데이터를 기록합니다.
  • RUN : 도커 컨테이너 내부에서 실행할 명령어를 정의합니다.
  • COPY : host의 파일 및 디렉토리를 docker container에 복사합니다.
  • ARG : 변수를 설정 합니다.
  • ADD : host의 파일 및 디렉토리를 docker container에 추가합니다. (COPY와 다른점 : 특정 압축파일일 경우 압축해제됨)
  • VOLUME : docker container에 연결할 host 디렉토리
  • EXPOSE : 외부 노출 포트 설정
  • CMD : 도커 컨테이너에서 실행할 프로세스를 지정합니다. (RUN은 빌드과정에서 여러번 실행하지만 CMD는 한번 실행됩니다.)
  • ENTRYPOINT :도커 컨테이너에서 실행할 프로세스를 지정합니다. (ENTRYPOINT로 실행하는 프로세스의 argument를 지정할수 있습니다. CMD의 경우 Bash가 entry point로 동작합니다. )

위처럼 Dockerfile 설정이 끝났다면 아래 build 명령을 통해 이미지 build를 진행 할 수 있습니다.

docker image build -t taesk/example-api:latest .

docker image build 명령을 통해 위에서 구성해둔 Docker file명령들이 수행되는데, 아마 위 설정을 따라하신 몇몇분들은 다음과 같은 에러를 만나셨을겁니다.

Step 3/4 : ADD ${JAR_FILE} example-api.jar
ADD failed: stat /var/lib/docker/tmp/docker-builder565264130/build/libs/example-api-0.0.1-SNAPSHOT.jar: no such file or directory

Docker file명령을 다시 한번 살펴보면 해당 명령은 프로젝트 build를 수행하지 않고 build된 프로젝트의 jar file을 가져와 docker image를 만들어주기때문에 image build 전, project build를 먼저 수행해주셔야 합니다.

docker image ls

명령을 통해 image가 생성된 것을 확인하실수 있습니다.

2

이 이미지는 이제 실행가능한 이미지로써 생성이 완료되었기 때문에 docker run 명령을 통해 서비스를 실행 시킬수 있습니다.

3

위와같이 간단하게 이미지를 생성하고 실행시킬수 있었지만 작은 문제가 하나 있습니다.
위 빌드된 이미지는 패키징된 jar 파일을 이미지화 시켰기 때문에 약간의 소스 수정이 일어나더라도 변경된 소스로 인해 dependency들이 포함된 jar 파일 전체가 새로운 이미지로 인식이 되어 전체 파일을 다시 빌드를 수행하게 됩니다.

간단한 소스수정후 docker history 명령으로 통해 이미지 구성사항을 비교해 보도록하겠습니다.

4

위에서 보시다시피 새로운 이미지 ID로 인식되어 jar파일이 ADD되고 실행되어지는것을 볼 수 있습니다. 현재 example-api프로젝트의 규모가 작아서 큰 차이는 안나지만 이는 대규모 프로젝트 일 수록 이미지 빌드를 할 때마다 오랜시간이 걸리게되는 큰 이슈일 것입니다.

따라서 이러한 문제를 해결하기 위해 layer를 나누어 이미지 빌드를 해 보도록 하겠습니다.
docker imagelayer를 나누어 캐싱해두고 변경된 layer들만 교체하는식으로 하여 image build 속도를 향상 시킬 수 있습니다.

gradle의 경우 build작업 이후에 ‘/build/libs/example-api.jar’ jar 파일이 생성되는데, layer를 나누기 위해 image build 전에 해당 패키지 unpacking 해야 합니다.

mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)

위 명령을 통해 ‘build/dependency’ 폴더 아래 jar파일이 unpacking 되었으니 dockerfile에서 layer를 나누어 설정해 보도록 하겠습니다.

FROM java:8
ARG DEPENDENCY=build/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.taes.api.ExampleApiApplication"]

위와같은 설정 이후 image build를 위한 수행 명령을 정리해보면 다음과 같습니다.

$ ./gradlew build
$ mkdir -p build/dependency && (cd build/dependency; jar -xf ../libs/*.jar)
$ docker image build -t taesk/example-api:latest .

자 이제 어떻게 변했는지 확인해보겠습니다.

5

lib, meta, classes를 나누어 각각의 layer를 설정 해 주었더니 코드를 변경한 classes에 대한 이미지만 교체된것을 확인하 실 수 있습니다. 변경된 사이즈도 비교해보자면 jar 파일일때는 ‘16.4MB’의 이미지가 교체되었지만, layer 형태로 변경했을때는 ‘2.73kB’의 이미지만이 교체된것을 비교 확인 하실 수 있습니다.


Docker image 만들기 - Spring boot 2.3.x

Spring boot를 사용하신다면, 작성일 기준 최신 버전인 Spring boot 2.3.x 부터 Docker Support기능이 추가되었습니다.

Docker support
Spring Boot 2.3 adds some interesting new features that can help you package up your Spring Boot application into Docker images.
Support for building Docker images using Cloud Native Buildpacks and has been added to the Maven and Gradle plugins via the spring-boot:build-image goal and the bootBuildImage task. The Paketo Java buildpack is used by default to create images.

Also, support for building jar files with contents separated into layers has been added to the Maven and Gradle plugins.

위 챕터에서 꽤나 간단하게 진행했음에도 불구하고, docker image를 생성하는것이 꽤나 번거로운 작업들이라고 생각이 드실지 모르겠습니다.(프로젝트 빌드하고, layer를 생성하고, image build 하고…)

그래서, Spring boot 2.3.x 부터 제공하는 Buildpack을 사용하여 docker image build를 수행 해 보도록 하겠습니다.

./gradlew bootBuildimage --imageName=taesk/boot-example-api

단지 위 명려어만 수행하면 끝입니다!

6

dockerfile 구성도 없이, 프로젝트 빌드나 layer 설정등의 작업 없이 간편하게 docker image build가 가능합니다.

또한, buildpack을 사용하지 않고 기존의 방식의 image builder를 편하게 해주기위해 layered jar가 추가되었습니다.

// build.gradle

bootJar {
    layered()
}
# dockerfile

FROM java:8 as builder
WORKDIR application
ARG JAR_FILE=build/libs/example-api-0.0.1-SNAPSHOT.jar
ADD ${JAR_FILE} example-api.jar
RUN java -Djarmode=layertools -jar example-api.jar extract

FROM java:8
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/snapshot-dependencies/ ./
#COPY --from=builder application/resources/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java","-cp","app:app/lib/*","com.taes.api.ExampleApiApplication"]

layered jar를 이용한 위와 같은 설정으로 이전에 layer를 나누기위해 jar를 직접 unpack 해서 copy 하던 방식 대신에 좀 더 편리하게 layer를 구성 할 수 있도록 기능이 지원되었습니다.


Docker image 배포하기

docker image를 만들었으니, 이미지를 어디서든 사용 할 수 있도록 저장소에 배포를 해보도록 하겠습니다.

일반적으로 이미지는 클라우드 공유 저장소인 docker hub혹은 private docker repository를 구성하여 이미지 저장소로 사용하게 되는데 이번 예제에서는 docker hub에 이미지를 등록해보도록 하겠습니다. (물론 docker hub를 유료버전으로 private 저장소로 사용 할 수 있습니다.)

먼저 locoal에서 docker hub login을 진행해줍니다.

7

docker push taesk/example-api

8

자 이제 docker hub에 생성한 이미지가 배포가 되어 언제든지 이미지를 가지고 올 수 있는 상태가 되었습니다!
테스트를 위해 기존에 local에서 생성되어있는 docker image를 삭제후에 docker hub로 부터 다시 이미지를 받아와 보도록 하겠습니다.

9

10


Docker 서비스 운영 배포하기

이미지가 docker hub에 업로드된것을 확인하였으니, 이제 서비스 운영배포를 진행 할 수 있습니다.
가장 간단한 배포 방법으로는 운영 서비스에서 docker 명령어를 실행해 컨테이너를 실행시키는 것 입니다.

$ docker pull taesk/example-api:latest
$ docker run -t taesk/example-api:lates

하지만, 위와같은 직접배포 방법은 많은 수고스러움을 동반하기때문에 위에서 작업했던 모든 과정을 CI/CD를 통해 처리하면 한결 수월한 배포과정이 될 수 있을 것입니다.

모든 과정을 직접 설명드리지는 않고 간략한 절차로써 아래와 같이 로직을 구성 할 수 있을 것 입니다.

-> User
-> Git push (Github, Bitbucket ...) 
-> Docker image build (Jenkins)
-> 운영서비스 Docker run (Jenkins)