GradleでSpringBoot + Dockerイメージをビルドする

# TL;DR

- Gradleを利用してDockerイメージを作成してみる
- SpringBootをjar形式にビルドし、java + jarファイルを含めたDockerイメージを作成する
- 基本的には[Spring公式](https://spring.io/guides/gs/spring-boot-docker/)のドキュメントに沿って試してみるだけ

# 実行環境構築

## 環境

以下の検証項目は、macOSで実施しています
利用しているライブラリなどのバージョンは以下になります

- JDK (1.8.0_121)
- Spring Boot (1.5.8)
- Gradle (3.3)
- Docker Version (17.06.2-ce-mac27)

## プロジェクトを作成する

[SPRING INITIALIZR](https://start.spring.io/)を利用してプロジェクトを選択します

Generate a 「Gradle Project」 としてGradleでビルドするようにして、DependenciesにWebを含めてみます

![screenshot_spring_init.png](https://qiita-image-store.s3.amazonaws.com/0/20943/93d3f96d-41e4-44bd-26f0-0bad6fcb313d.png)


出来上がった`demo.zip`を展開して、gradleでプロジェクトをビルドして実行してみましょう

```
$ unzip demo.jar
$ cd demo
$ ./gradlew build
$ java -jar build/libs/demo-0.0.1-SNAPSHOT.jar
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.8.RELEASE)

2017-11-10 10:04:09.107  INFO 41308 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication on mac0137.local.furyu.jp with PID 41308 (/Users/moriokanaoki/Downloads/demo/build/libs/demo-0.0.1-SNAPSHOT.jar started by moriokanaoki in /Users/moriokanaoki/Downloads/demo)
2017-11-10 10:04:09.111  INFO 41308 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2017-11-10 10:04:09.193  INFO 41308 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@45283ce2: startup date [Fri Nov 10 10:04:09 JST 2017]; root of context hierarchy
2017-11-10 10:04:10.577  INFO 41308 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2017-11-10 10:04:10.591  INFO 41308 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2017-11-10 10:04:10.592  INFO 41308 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.23
2017-11-10 10:04:10.676  INFO 41308 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2017-11-10 10:04:10.676  INFO 41308 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1486 ms
2017-11-10 10:04:10.804  INFO 41308 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2017-11-10 10:04:10.809  INFO 41308 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-11-10 10:04:10.810  INFO 41308 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-11-10 10:04:10.810  INFO 41308 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-11-10 10:04:10.810  INFO 41308 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2017-11-10 10:04:11.191  INFO 41308 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@45283ce2: startup date [Fri Nov 10 10:04:09 JST 2017]; root of context hierarchy
2017-11-10 10:04:11.299  INFO 41308 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-11-10 10:04:11.300  INFO 41308 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-11-10 10:04:11.332  INFO 41308 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-11-10 10:04:11.333  INFO 41308 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-11-10 10:04:11.372  INFO 41308 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-11-10 10:04:11.509  INFO 41308 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-11-10 10:04:11.578  INFO 41308 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-11-10 10:04:11.583  INFO 41308 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.98 seconds (JVM running for 3.412)
```

動いてますが、分かりにくいのでHelloWorldくらい出力させてみときます

```java:src/main/java/com/example/demo/DemoApplication.java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class DemoApplication {

        public static void main(String[] args) {
                SpringApplication.run(DemoApplication.class, args);
        }

        @RequestMapping(value = "/")
        String sayHello() {
                return "Hello World!";
        }
}
```

再度 `demo.jar` を起動させてlocalhostの8080番ポートにアクセスすると "Hello World!" が表示されていると思います。

## Dockerfileを用意する

上記 `java -jar` を実行するDockerイメージを作成するために、プロジェクト直下にDockerfileを用意します
demo.jarの作成などは後述のbuild.gradle変更と連携しています

```txt:Dockerfile
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD target/demo.jar app.jar
ENV JAVA_OPTS=""
ENTRYPOINT exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar
```

## gradle-dockerプラグインを追加

Dockerイメージ生成をGradleにて行うためにbuild.gradleを修正します。

```build.gradle
buildscript {
	ext {
		springBootVersion = '1.5.8.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
		classpath "se.transmode.gradle:gradle-docker:1.2"
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'docker'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
	mavenCentral()
}

dependencies {
	compile('org.springframework.boot:spring-boot-starter-web')
	testCompile('org.springframework.boot:spring-boot-starter-test')
}

jar {
    baseName = 'demo'
    archiveName = "${baseName}.jar"
}

task buildDocker(type: Docker, dependsOn: build) {
    applicationName = jar.baseName
    dockerfile = file('Dockerfile')
    doFirst {
        copy {
            from jar
            into "${stageDir}/target"
        }
    }
}
```

## ビルド

早速ビルドしてみましょう。
ここでmacOSで `Dockerを起動しておくこと` を忘れずに

```
$ ./gradlew build buildDocker
Starting a Gradle Daemon, 1 incompatible Daemon could not be reused, use --status for details
:compileJava
:processResources UP-TO-DATE
:classes
:findMainClass
:jar
:bootRepackage
:assemble
:compileTestJava
:processTestResources NO-SOURCE
:testClasses
:test
2017-11-10 22:27:45.286  INFO 43631 --- [       Thread-5] o.s.w.c.s.GenericWebApplicationContext   : Closing org.springframework.web.context.support.GenericWebApplicationContext@697f2062: startup date [Fri Nov 10 22:27:43 JST 2017]; root of context hierarchy
:check
:build
:buildDocker
Sending build context to Docker daemon   14.5MB
Step 1/5 : FROM openjdk:8-jdk-alpine
 ---> xxxxxxxxxxxx
Step 2/5 : VOLUME /tmp
 ---> Using cache
 ---> xxxxxxxxxxxx
Step 3/5 : ADD target/demo.jar app.jar
 ---> xxxxxxxxxxxx
Removing intermediate container xxxxxxxxxxxx
Step 4/5 : ENV JAVA_OPTS ""
 ---> Running in xxxxxxxxxxxx
 ---> xxxxxxxxxxxx
Removing intermediate container xxxxxxxxxxxx
Step 5/5 : ENTRYPOINT exec java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app.jar
 ---> Running in xxxxxxxxxxxx
 ---> xxxxxxxxxxx
Removing intermediate container xxxxxxxxxxxx
Successfully built xxxxxxxxxxxx
Successfully tagged com.example/demo:0.0.1-SNAPSHOT

BUILD SUCCESSFUL

$ docker images
REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
com.example/demo                0.0.1-SNAPSHOT      xxxxxxxxxxxx        1 minutes ago       116MB
```
うまく出来上がったようです

# DockerによるSpringBootの起動

```
$ docker run -p 8080:8080 -d com.example/demo:0.0.1-SNAPSHOT
xxxxxxxxxxxxxxxcf54172bd224611a7a184d3d454ee06a289143c191041706
$ docker ps
CONTAINER ID        IMAGE                             COMMAND                  CREATED             STATUS              PORTS                    NAMES
xxxxxxxxxxxx        com.example/demo:0.0.1-SNAPSHOT   "/bin/sh -c 'exec ..."   36 seconds ago      Up 10 seconds       0.0.0.0:8080->8080/tcp   friendly_murdock
```
起動しているようです。
localhostの8080番ポートにアクセスすると "Hello World!" が再び表示されています

# まとめ

今回はイメージ名やタグについては調整してません
また、Dockerイメージも雑にローカルに配備しました

Gradleでjarファイルを作成するのと同じような感覚でDockerイメージまで作成出来ました
Java9やOpenJDKの採用も今後進んでいくと考えられるので、SpringBootを動かすのはDockerが第一候補みたいな未来もあるかもですね

# 参考文献

- https://spring.io/guides/gs/spring-boot-docker/