Spring Boot Application을 Docker Image로 생성하기 - 1. Docker file (with Jar)
이번 포스팅에서는 Spring Boot로 만들어진 Web Application을 Docker Image로 만들어서 docker hub에 push하는 과정을 정리해 보았다. 고맙게도 스프링 사이트에서 이 내용에 대해 정리를 잘 해주셨고(Link), 이 포스팅은 거기에 있는 내용 중에 개인적으로 필요한 사항 위주로 정리하면서 약간의 실무적인 내용과 개인의 삽질을 추가하였다. 내용이 길어 둘로 나누어 작성하였다.
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을 활용 + 배포
Spring Boot Sample application
이 포스팅을 위해 간단한 Spring Boot Application을 하나 만들었다. 아래와 같은 환경에서 Project를 생성했고, 간단하게 Controller class를 하나 추가하였다.
- Spring Boot 2.1.2
- Open JDK
- Build Tool : Gradle 5.6.4
- Package Name : com.browndwarf
- Project Name : dockerwebservice
Build.gradle
plugins {
id 'org.springframework.boot' version '2.1.12.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
group = 'com.browndwarf'
version = '0.1'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Controller
...
@RestController
@RequestMapping("/")
public class InfoConteroller {
@GetMapping("whoami")
public String getSystemInfo() {
String returnMsg;
try {
returnMsg = new StringBuilder("HostName : ")
.append(InetAddress.getLocalHost().getHostName())
.append(", IP Address : ")
.append(InetAddress.getLocalHost().getHostAddress())
.toString();
}catch (Exception e) {
returnMsg = new StringBuilder("Error : ").append(e.getMessage()).toString();
}
return returnMsg;
}
@GetMapping("health")
public String checkHealth() {
return "Hello. Browndwarf World.";
}
Code는 위와 같이 단순하고 특이한 내용은 없다.
Case 1. Jar 파일을 이용한 Docker Image 생성
Spring Boot Application을 Contrainerizing하는 첫 번째 방법은 maven이나 gradle로 Build한 후 생성되는 jar 파일을 바로 이용해서 docker image를 생성하는 경우이다. DockerFile은 <code 1>과 같다. 단순히 openjdk layer위에 jar파일을Copy 한 후에 'java -jar' command를 통해 실행하는 방식으로 Docker Image가 만들어 지게 구성했다
FROM openjdk:8-jdk-alpine
COPY build/libs/*.jar dockerservice.jar
ENTRYPOINT ["java","-jar","/dockerservice.jar"]
Gradle을 이용해 Spring Boot Application을 build하고, <code 1>의 Dockerfile과 'docker build' command로 docker image를 만드는 전체 과정은 아래와 같고, 최종적으로 docker image가 만들어진 것을 확인할 수 있다.
Gradle Build
[browndwarf@ha-machine-1 dockerwebservice]$ ./gradlew clean build
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :test
2020-05-27 09:20:19.452 INFO 4889 --- [ Thread-5] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
BUILD SUCCESSFUL in 9s
6 actionable tasks: 6 executed
Docker Image 생성
[browndwarf@ha-machine-1 dockerwebservice]$ docker build -t dockerservice:latest .
Sending build context to Docker daemon 28.51MB
Step 1/3 : FROM openjdk:8-jdk-alpine
---> a3562aa0b991
Step 2/3 : COPY build/libs/*.jar dockerservice.jar
---> df7c8830f1a3
Step 3/3 : ENTRYPOINT ["java","-jar","/dockerservice.jar"]
---> Running in 264abaa90bc4
Removing intermediate container 264abaa90bc4
---> 2521a50f7386
Successfully built 2521a50f7386
Successfully tagged dockerservice:latest
Docker Image 조회
[browndwarf@ha-machine-1 dockerwebservice]$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
dockerservice latest 2521a50f7386 3 minutes ago 123MB
'docker run' command로 만들어진 docker image를 background로 실행한 후 curl command를 통해 간단하게 rest api를 호출했더니 정상적으로 동작하고 있다.
Docker Image 실행
[browndwarf@ha-machine-1 dockerwebservice]$ docker run -d --rm --name dockerservice -p 8080:8080 dockerservice:latest
c247026468e774976481d2408e9d8ccfd6abea1d64035c5ee6a76d165992d942
Docker Container List 조회
[browndwarf@ha-machine-1 dockerwebservice]$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c247026468e7 dockerservice:latest "java -jar /dockerse…" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp dockerservice
동작 확인
[browndwarf@ha-machine-1 dockerwebservice]$ curl localhost:8080/health
Hello. Browndwarf World.
[browndwarf@ha-machine-1 dockerwebservice]$ curl localhost:8080/whoami
HostName : c247026468e7, IP Address : 172.17.0.3
실행 결과 마지막에 Host Name이 Docker Container의 ID이고 실행되는 Machine의 IP가 172.17.X.X/16 대역의 Docker Network에서 제공되고 있다는 것을 확인할 수 있다.
Tip. Spring Boot Application Docker Image에 Property 적용
하나의 Application을 실행할 때 외부에서 설정한 Property를 이용하는 경우가 많고, Spring Boot Application에서 이렇게 외부에서 설정한 Property를 반영하는 방식은 여러가지가 있으나 이 포스팅에서는 아래 3가지 Case를 가정해서 Dockerizing된 Image에 적용해 보았다.
- profile에 따라 다른 property set이 적용되는 경우
- 직접 property를 전달하는 경우 (application property, system property)
- 기존에 정의된 property를 Override하는 경우
Case 1. spring.profiles 이용
application.yml 파일에 <code 2>와 같이 Profile을 default, prod로 나눠 정의하고, 각 Profile에서 config.healthmsg를 'Healthy'와 'I'm live'로 다르게 정의하였다.
---
spring:
profiles: default
config:
healthmsg: Healthy.
---
spring:
profiles: prod
config:
healthmsg: I'm live.
그리고 기존의 controller class를 <code 3>과 같이 수정해서 GET /health api를 호출했을 때 Property에서 정의한 config.healthmsg가 Return 되게 하였다. (정의된 property를 찾지 못했을 경우에는 'NO_DEFINE'이 return 되게 하였다.)
@RequestMapping("/")
public class InfoConteroller {
@Value("${config.healthmsg:'NO_DEFINE'}")
String healthMsg;
@GetMapping("whoami")
public String getSystemInfo() {
String returnMsg;
try {
returnMsg = new StringBuilder("HostName : ")
.append(InetAddress.getLocalHost().getHostName())
.append(", IP Address : ")
.append(InetAddress.getLocalHost().getHostAddress())
.toString();
}catch (Exception e) {
returnMsg = new StringBuilder("Error : ").append(e.getMessage()).toString();
}
return returnMsg;
}
@GetMapping("health")
public String checkHealth() {
return healthMsg;
}
}
Dockerfile에 USE_PROFILE ENV를 추가해서 default value로 'default'를 설정한 후 jar 파일을 실행시킬 때 spring.profiles.active 값으로 사용하게 <code 4>와 같이 수정하였다.
FROM openjdk:8-jdk-alpine
ENV USE_PROFILE default
COPY build/libs/*.jar dockerservice.jar
ENTRYPOINT ["java", "-Dspring.profiles.active=${USE_PROFILE}", "-jar","/dockerservice.jar"]
Update된 Code를 이용해 다시 build 한 후에, 위의 Dockerfilefmf 이용해 docker image를 새로 만든 후 실행하였다.
[browndwarf@ha-machine-1 dockerwebservice]$ ./gradlew clean build
...
[browndwarf@ha-machine-1 dockerwebservice]$ docker build -t dockerservice:latest .
...
[browndwarf@ha-machine-1 dockerwebservice]$ docker run -d --rm --name dockerservice -p 8080:8080 dockerservice:latest
d82d13030021b3417e1503e16d115c5023717775a87df01e7d180a95f0d009b9
# 별다른 설정값이 없을 떄 default profile이 적용됨
[browndwarf@ha-machine-1 dockerwebservice]$ curl localhost:8080/health
Healthy.
[browndwarf@ha-machine-1 dockerwebservice]$ docker stop dockerservice
dockerservice
...
# 'prod' profile을 적용
[browndwarf@ha-machine-1 dockerwebservice]$ docker run -d --rm --name dockerservice -p 8080:8080 -e USE_PROFILE=prod dockerservice:latest
062fa26690f0d9a6e3791fea047ad25e6e60fda3f83b847ea7e972478e043cb4
'prod' Profile의 값이 적용된 상태
[browndwarf@ha-machine-1 dockerwebservice]$ curl localhost:8080/health
I'm live.
위의 결과에서 보이는 바와 같이 GET /health를 호출했을 때 'default' profile에서 정의된 'Healthy'를 Response 하고 있다. 하지만 -e option을 통해 USE_PROFILE 값을 prod로 설정해서 실행시키면 GET /health의 response가 'I'm live'로 Response 한다. 즉, 외부에서 입력한 값을 통해 spring.profiles.active의 값이 의도한대로 설정되고 있음을 알 수 있다.
Case 2. 직접 Property Value 전달
여러 개의 Property들이 하나의 묶음으로 관리되야할 때는 위와 같이 profile에 따라 다른 Property Set이 적용되는 것이 좋지만, 1~2개의 Property를 추가할 때는 '-D' option을 사용해서 Spring Property나 System Property 모두 추가할 수 있다. 이를 Test 하기 위해 기존 Dockerfile을 'normal.prop'라는 Spring Application Property와 'system.prop'라는 System Property를 전달할 수 있게 <code 5>와 같이 수정하였다.
FROM openjdk:8-jdk-alpine
ENV USE_PROFILE default
COPY build/libs/*.jar dockerservice.jar
ENTRYPOINT ["java", "-Dspring.profiles.active=${USE_PROFILE}", "-Dnormal.prop=Normal_A", "-Dsystem.prop=System_B", "-jar","/dockerservice.jar"]
그리고 <code 6>과 같이 Application이 Ready Event를 처리하는 Handler를 만들어서 받아서 해당 Property들을 Log로 출력해 설정값들을 확인하는 Class를 추가하였다.
@Component
public class ApplicationReadyEventHandler implements ApplicationListener<ApplicationReadyEvent> {
@Value("${normal.prop:'NOT_EXIST_NORMAM_PROPERTY'}")
String normalProp;
@Value("#{systemProperties['system.prop']?: 'NOT_EXIST_SYSTEM_PROPERTY'}")
String systemProp;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
// TODO Auto-generated method stub
log.info(">>>>> Normal Property : {}", normalProp);
log.info(">>>>> System Property : {}", systemProp);
}
}
build한 후에 다시 Docker image를 만들어 실행해 보면 Dockerfile에서 정의했던 Application Property와 System Property가 Dockerfile에서 정의된 값으로 전달되었음을 확인할 수 있다.
# 새로 Build 한 후 Docker Image 생성
[browndwarf@ha-machine-1 dockerwebservice]$ ./gradlew clean build
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :test
2019-09-19 18:11:52.815 INFO 20188 --- [ Thread-5] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
BUILD SUCCESSFUL in 8s
6 actionable tasks: 6 executed
[browndwarf@ha-machine-1 dockerwebservice]$ docker build -t dockerservice:latest .
Sending build context to Docker daemon 28.56MB
Step 1/4 : FROM openjdk:8-jdk-alpine
---> a3562aa0b991
Step 2/4 : ENV USE_PROFILE default
---> Running in 395b3d2aeb9a
Removing intermediate container 395b3d2aeb9a
---> 93980c0f1b7a
Step 3/4 : COPY build/libs/*.jar dockerservice.jar
---> 75a9289f32b5
Step 4/4 : ENTRYPOINT ["java", "-Dspring.profiles.active=${USE_PROFILE}", "-Dnormal.prop=Normal_A", "-Dsystem.prop=System_B", "-jar","/dockerservice.jar"]
---> Running in 49d207f9e3da
Removing intermediate container 49d207f9e3da
---> 80b4bffbc46f
Successfully built 80b4bffbc46f
Successfully tagged dockerservice:latest
# 생성된 Docker Image 실행
[browndwarf@ha-machine-1 dockerwebservice]$ docker run --rm --name dockerservice -p 8080:8080 -e USE_PROFILE=prod dockerservice:latest
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.12.RELEASE)
2019-09-19 09:14:17.027 INFO 1 --- [ main] c.b.d.DockerwebserviceApplication : Starting DockerwebserviceApplication on 0aaf3e105b19 with PID 1 (/dockerservice.jar started by root in /)
2019-09-19 09:14:17.030 INFO 1 --- [ main] c.b.d.DockerwebserviceApplication : The following profiles are active: prod
2019-09-19 09:14:18.030 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-09-19 09:14:18.058 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-09-19 09:14:18.059 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.30]
2019-09-19 09:14:18.141 INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-09-19 09:14:18.141 INFO 1 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1072 ms
2019-09-19 09:14:18.402 INFO 1 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-19 09:14:18.734 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-19 09:14:18.736 INFO 1 --- [ main] c.b.d.DockerwebserviceApplication : Started DockerwebserviceApplication in 2.156 seconds (JVM running for 2.59)
2019-09-19 09:14:18.737 INFO 1 --- [ main] c.b.d.e.ApplicationReadyEventHandler : >>>>> Normal Property : Normal_A
2019-09-19 09:14:18.739 INFO 1 --- [ main] c.b.d.e.ApplicationReadyEventHandler : >>>>> System Property : System_B
위에서 추가했던 Application Property와 System Property를 'docker run' 실행할 때, 환경 변수로도 설정할 수 있는 지 확인해 보기 위해 Dockerfile을 <code 7>과 같이 수정하고 Docker image를 생성했다.
FROM openjdk:8-jdk-alpine
ENV USE_PROFILE default
ENV NORM_PROP=
ENV SYSTEM_PROP=
COPY build/libs/*.jar dockerservice.jar
ENTRYPOINT ["java", "-Dspring.profiles.active=${USE_PROFILE}", "-Dnormal.prop=${NORM_PROP}", "-Dsystem.prop=${SYSTEM_PROP}", "-jar","/dockerservice.jar"]
'docker run' command를 실행하면서 -e Option으로 NORM_PROP와 SYSTEM_PROP에 값을 설정해서 각각 Application Property와 System Property로 전달되는 지 확인해 보았다.
# 실행 시에 application property와 system property 적용
[browndwarf@ha-machine-1 dockerwebservice]$ docker run --rm --name dockerservice -p 8080:8080 -e USE_PROFILE=prod -e NORM_PROP=docker1 -e SYSTEM_PROP=docker2 dockerservice:latest
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.12.RELEASE)
2019-09-19 09:24:40.585 INFO 1 --- [ main] c.b.d.DockerwebserviceApplication : Starting DockerwebserviceApplication on 8d0cb9661c4c with PID 1 (/dockerservice.jar started by root in /)
2019-09-19 09:24:40.588 INFO 1 --- [ main] c.b.d.DockerwebserviceApplication : The following profiles are active: prod
2019-09-19 09:24:41.521 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-09-19 09:24:41.554 INFO 1 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-09-19 09:24:41.555 INFO 1 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.30]
2019-09-19 09:24:41.644 INFO 1 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-09-19 09:24:41.644 INFO 1 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1013 ms
2019-09-19 09:24:41.865 INFO 1 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-09-19 09:24:42.167 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-09-19 09:24:42.169 INFO 1 --- [ main] c.b.d.DockerwebserviceApplication : Started DockerwebserviceApplication in 2.03 seconds (JVM running for 2.458)
2019-09-19 09:24:42.170 INFO 1 --- [ main] c.b.d.e.ApplicationReadyEventHandler : >>>>> Normal Property : docker1
2019-09-19 09:24:42.172 INFO 1 --- [ main] c.b.d.e.ApplicationReadyEventHandler : >>>>> System Property : ${SYSTEM_PROP}
위에서 확인된 바와 같이 Application Property로 사용하는 'normal.prop'의 경우에는 -e option으로 전달한 값이 사용되고 있지만, System Property에는 전달되지 않고 Dockerfile에 있는 String이 System Property로 정의 되어 있다는 것을 알 수 있다. (이런 이유에 대해 좀 더 자세한 설명이 나와 있는 지 확인해 봤지만 찾을 수 없었다.)
Case 3. Property Override
application.properties(or application.yml)에서 정의했던 property를 override할 수 있는 지 알아보자. <code 8>과 같이 Dockerfile에서 DIRECT_MSG라는 환경변수로 추가하고 이를 위에서 사용했던 config.healthmsg에 전달하였다.
FROM openjdk:8-jdk-alpine
ENV USE_PROFILE default
ENV NORM_PROP=
ENV DIRECT_MSG=
COPY build/libs/*.jar dockerservice.jar
ENTRYPOINT ["java", "-Dspring.profiles.active=${USE_PROFILE}", "-Dnormal.prop=${NORM_PROP}", "-Dconfig.healthmsg=${DIRECT_MSG}", "-jar","/dockerservice.jar"]
위의 Dockerfile로 docker image를 만들어서 실행시킬 때 -e Option으로 DIRECT_MSG에 'hello'로 전달하면 prod profile에서 정의한 'I'm live' 값이 없어지고, 'hello'로 Override 되는 것을 확인할 수 있다.
# 실행 시에 DIRECT_MSG 설정으로 application property Override
[browndwarf@ha-machine-1 dockerwebservice]$ docker run --rm --name dockerservice -p 8080:8080 -e USE_PROFILE=prod -e NORM_PROP=docker1 -e DIRECT_MSG=hello dockerservice:latest
[browndwarf@ha-machine-1 ~]$ curl localhost:8080/health
hello