DevOps/Docker

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

갈색왜성 2019. 9. 20. 09:57
반응형

이번 포스팅에서는 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"]
<Code 1> Jar 파일을 바로 실행시키는 구조의 Dockerfile

 

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에 적용해 보았다.

  1. profile에 따라 다른 property set이 적용되는 경우
  2. 직접 property를 전달하는 경우 (application property, system property)
  3. 기존에 정의된 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.
<Code 2> default와 prod로 정의된 property 파일의 예

 

그리고 기존의 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;
	}
}
<Code 3> 수정된 Controller File

 

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"]
<Code 4> 수정된 Dockerfile

 

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 5> 2개의 설정값이 추가된 Dockerfile

 

그리고 <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);
	}

}
<Code 6> Application ready 상태일 때 Property를 Log로 출력하는 Code

 

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"]
<Code 7> Application Property와 ,System Property를 환경 변수로 받게 수정한 Dockerfile

 

'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"]
<Code 8> Application property Override를 위해 수정된 Dockerfile

 

위의 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


 

Reference