Azure Container Service (Kubernetes)와 DevOps 구현 사례 #2 – 로컬 개발환경

Azure Container Service (Kubernetes)와 DevOps 구현 사례 #1 에서는 Kubernetes 운영환경을 만들었다. 여기서는 개발자들이 로컬 개발환경에서 소스를 수정 후에 빌드를 하고 Docker 이미지를 만드는 과정을 알아보자. 여러 개의 서비스로 구성된 마이크로서비스 아키텍쳐라면 서비스들이 한꺼번에 실행되어야 테스트를 할 수 있는데 Docker Compose로 테스트 하는 방법도 알아보자.

Docker 설치

먼저 Docker 가 로컬 개발환경에 설치되어 있어야 한다. Docker 설치 방법은 웹에 문서가 많다. 검색해서 참조해서 설치하면 된다. 맥의 경우 dmg 이미지로 쉽게 설치할 수 있다.

개발환경

Java + Spring boot 로 개발이 되어있고 이클립스를 사용했다. 메이븐(maven)을 사용하여 빌드를 관리한다. 설명에 사용한 소스코드는 github에 api-demo, view-demo 라는 프로젝트로 공개되어 있다. 소스코드에서 Dockerfile, pom.xml, docker-compose.yml 파일을 참고 바란다.

pom.xml

Maven을 사용하면 빌드와 도커 이미지 생성을 동시에 할 수 있다. spotify 에서 만든 Maven Plugin인 spring-boot-maven-plugin을 사용하면 쉽게 설정 할 수 있다. pom.xml 에 plugin 설정을 보면 Maven의 Goal은 package로 설정되어 있고 $ mvn package 명령하나로 소스빌드와 도커이미지 설정이 한번에 수행된다. repository 을 보면 이미지의 이름과 태그가 opencloudregistry.azurecr.io/clouddemo/view-demo:1 으로 설정되어 있는 걸 볼 수 있다.

이클립스에서 Maven Build를 실행하거나 터미널에서 $ mvn package 명령으로 실행할 수 있다. pom.xml에 기술되어 있는 것처럼 소스를 빌드하고 war 패키지를 만들고 마지막으로 도커 이미지를 만든다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.aircuve</groupId>
	<artifactId>view-demo</artifactId>
	<version>0.0.1</version>
	<packaging>war</packaging>

	<name>view-demo</name>
	<description>Saas view demo for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.4.7.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

	(중간생략)	...

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
	            <groupId>com.spotify</groupId>
	            <artifactId>dockerfile-maven-plugin</artifactId>
	            <version>1.3.4</version>
				<executions>
				    <execution>
                        <id>build-image</id>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>      
				  </executions>
				  
	            <configuration>
	           	 <repository>opencloudregistry.azurecr.io/clouddemo/view-demo</repository>
    				 <tag>1</tag>
    				<googleContainerRegistryEnabled>false</googleContainerRegistryEnabled>
	            </configuration>
        	</plugin>
		</plugins>
	</build>
</project>

Dockerfile

FROM java:8
VOLUME /tmp
ADD target/view-demo-0.0.1.war app.war
ENV JAVA_OPTS=""
RUN bash -c 'touch /app.war'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.war"]

Maven이 도커 이미지를 만들기 위해서는 Dockerfile이 필요하다. Dockerfile에 어떻게 도커 이미지를 만들것인지에 대한 명령이 들어있다. 여기서는 Base 이미지를 java:8을 사용했다. Docker Hub에서 이미지를 다운받아서 빌드할 때 만들어 놓은 war 파일을 복사한다. 마지막으로 도커 이미지가 로드되서 실행할 애플리케이션을 지정했다.

메이븐 빌드 로그

이클립스에서 빌드한 결과 로그를 보면 어떻게 최종 빌드 이미지를 만드는지 볼 수 있다.

[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building view-demo 0.0.1
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ view-demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource
[INFO] Copying 120 resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ view-demo ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ view-demo ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/Kevin/source/opencloud/view-demo/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ view-demo ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-surefire-plugin:2.18.1:test (default-test) @ view-demo ---
[INFO] Surefire report directory: /Users/Kevin/source/opencloud/view-demo/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
2017-11-22 15:52:30.555  INFO   --- [           main] .b.t.c.SpringBootTestContextBootstrapper : Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.aircuve.view.ViewPackageApplicationTests], using SpringBootContextLoader
2017-11-22 15:52:30.575  INFO   --- [           main] o.s.t.c.support.AbstractContextLoader    : Could not detect default resource locations for test class [com.aircuve.view.ViewPackageApplicationTests]: no resource found for suffixes {-context.xml, Context.groovy}.

(중간생략 ...)

org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@5ae50ce6]

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.4.7.RELEASE)

2017-11-22 15:52:31.290  INFO 8060 --- [           main] c.a.view.ViewPackageApplicationTests     : Starting ViewPackageApplicationTests on KevinMac.local with PID 8060 (started by Kevin in /Users/Kevin/source/opencloud/view-demo)

(중간생략 ...)

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.84 sec - in com.aircuve.view.ViewPackageApplicationTests
2017-11-22 15:52:33.576  INFO 8060 --- [       Thread-2] o.s.w.c.s.GenericWebApplicationContext   : Closing org.springframework.web.context.support.GenericWebApplicationContext@6853425f: startup date [Wed Nov 22 15:52:31 KST 2017]; root of context hierarchy

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] 
[INFO] --- maven-war-plugin:2.6:war (default-war) @ view-demo ---
[INFO] Packaging webapp
[INFO] Assembling webapp [view-demo] in [/Users/Kevin/source/opencloud/view-demo/target/view-demo-0.0.1]
[INFO] Processing war project
[INFO] Copying webapp resources [/Users/Kevin/source/opencloud/view-demo/src/main/webapp]
[INFO] Webapp assembled in [361 msecs]
[INFO] Building war: /Users/Kevin/source/opencloud/view-demo/target/view-demo-0.0.1.war
[INFO] 
[INFO] --- spring-boot-maven-plugin:1.4.7.RELEASE:repackage (default) @ view-demo ---
[INFO] 
[INFO] --- dockerfile-maven-plugin:1.3.4:build (build-image) @ view-demo ---
[INFO] Google Container Registry support is disabled
[INFO] Building Docker context /Users/Kevin/source/opencloud/view-demo
[INFO] 
[INFO] Image will be built as opencloudregistry.azurecr.io/clouddemo/view-demo:2
[INFO] 
[INFO] Step 1/6 : FROM java:8
[INFO] Pulling from library/java
[INFO] Digest: sha256:c1ff613e8ba25833d2e1940da0940c3824f03f802c449f3d1815a66b7f8c0e9d
[INFO] Status: Image is up to date for java:8
[INFO]  ---> d23bdf5b1b1b
[INFO] Step 2/6 : VOLUME /tmp
[INFO]  ---> Using cache
[INFO]  ---> d7662a1cee77
[INFO] Step 3/6 : ADD target/view-demo-0.0.1.war app.war
[INFO]  ---> fb5ba0dda896
[INFO] Step 4/6 : ENV JAVA_OPTS ""
[INFO]  ---> Running in 29ff6523682b
[INFO]  ---> 7ff1eed623c5
[INFO] Removing intermediate container 29ff6523682b
[INFO] Step 5/6 : RUN bash -c 'touch /app.war'
[INFO]  ---> Running in 1d0725dc2a29
[INFO]  ---> 229230cd53ba
[INFO] Removing intermediate container 1d0725dc2a29
[INFO] Step 6/6 : ENTRYPOINT java -Djava.security.egd=file:/dev/./urandom -jar /app.war
[INFO]  ---> Running in dbff4c32c614
[INFO]  ---> dbdf6c3ac7fc
[INFO] Removing intermediate container dbff4c32c614
[INFO] Successfully built dbdf6c3ac7fc
[INFO] Successfully tagged opencloudregistry.azurecr.io/clouddemo/view-demo:2
[INFO] 
[INFO] Detected build of image with id dbdf6c3ac7fc
[INFO] Building jar: /Users/Kevin/source/opencloud/view-demo/target/view-demo-0.0.1-docker-info.jar
[INFO] Successfully built opencloudregistry.azurecr.io/clouddemo/view-demo:2
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 25.745 s
[INFO] Finished at: 2017-11-22T15:52:53+09:00
[INFO] Final Memory: 33M/384M
[INFO] ------------------------------------------------------------------------

Docker Compose

마이크로서비스 아키텍쳐의 경우 테스트를 하려면 관련된 서비스가 모두 실행이 되어야 한다. 즉 연관된 도커 이미지가 한번에 실행되어야 내가 수정한 소스코드가 정상적으로 작동하는지 알 수 있다. docker 명령으로 하나씩 올려서 테스트 할 수 도 있지만 Docker Compose를 이용하면 쉽게 테스트 환경을 만들 수 있다. 여기에서는 view-demo 와 api-demo 두 개의 서비스를 한번에 올려서 테스트 하도록 Docker Compose를 구성했다. docker-compose.yml 파일의 내용은 아래와 같다.

version: "3.3"
services:
    view-demo:
      image: opencloudregistry.azurecr.io/clouddemo/view-demo:1
      container_name: view-demo
      ports:
        - "443:8443"
    api-demo:
      image: opencloudregistry.azurecr.io/clouddemo/api-demo:1
      container_name: api-demo

$ docker-compose up 명령을 실행해서 https://localost로 접속해서 테스트 할 수 있다.

$ docker-compose up
Starting view-demo ... 
Starting api-demo ... 
Starting api-demo
Starting api-demo ... done
Attaching to view-demo, api-demo
api-demo     | 
api-demo     |   .   ____          _            __ _ _
api-demo     |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
api-demo     | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
api-demo     |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
api-demo     |   '  |____| .__|_| |_|_| |_\__, | / / / /
api-demo     |  =========|_|==============|___/=/_/_/_/
api-demo     |  :: Spring Boot ::        (v1.4.7.RELEASE)
api-demo     | 
view-demo    | 
view-demo    |   .   ____          _            __ _ _
view-demo    |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
view-demo    | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
view-demo    |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
view-demo    |   '  |____| .__|_| |_|_| |_\__, | / / / /
view-demo    |  =========|_|==============|___/=/_/_/_/
view-demo    |  :: Spring Boot ::        (v1.4.7.RELEASE)
view-demo    | 
view-demo    | 2017-11-22 07:00:07.288  INFO 1 --- [           main] com.aircuve.ViewPackageApplication       : Starting ViewPackageApplication v0.0.1 on a7a23f97ce7c with PID 1 (/app.war started by root in /)
view-demo    | 2017-11-22 07:00:07.309 DEBUG 1 --- [           main] com.aircuve.ViewPackageApplication       : Running with Spring Boot v1.4.7.RELEASE, Spring v4.3.9.RELEASE
view-demo    | 2017-11-22 07:00:07.310  INFO 1 --- [           main] com.aircuve.ViewPackageApplication       : No active profile set, falling back to default profiles: default
api-demo     | 2017-11-22 07:00:07.357  INFO 1 --- [           main] com.aircuve.ApiPackageApplication        : Starting ApiPackageApplication v0.0.1 on 16b394d7ca29 with PID 1 (/app.war started by root in /)
api-demo     | 2017-11-22 07:00:07.396  INFO 1 --- [           main] com.aircuve.ApiPackageApplication        : No active profile set, falling back to default profiles: default
view-demo    | 2017-11-22 07:00:07.424  INFO 1 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@14899482: startup date [Wed Nov 22 07:00:07 UTC 2017]; root of context hierarchy

(중간생략 ...)

view-demo    | 2017-11-22 07:00:14.622  INFO 1 --- [           main] com.aircuve.ViewPackageApplication       : Started ViewPackageApplication in 8.298 seconds (JVM running for 9.362)
api-demo     | 2017-11-22 07:00:14.713  INFO 1 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
api-demo     | 2017-11-22 07:00:14.842  INFO 1 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
api-demo     | 2017-11-22 07:00:14.850  INFO 1 --- [           main] com.aircuve.ApiPackageApplication        : Started ApiPackageApplication in 8.51 seconds (JVM running for 9.59)