Spring Boot DevtoolsとJibを組み合わせた時に、コンテナ実行時にはDevtoolsを無効にしたくなるという話

# What's？

Spring BootでDockerイメージを作成する時は、Cloud Native Buildpacksを使うのが標準だと思いますが。

Jibを使って作ろうとした時に、依存関係にSpring Boot Devtoolsを含めているとちょっと困ったことになったのでその話をメモしておきます。

# 起こったことと解決方法

なにに困ったかというと、Spring Boot Devtoolsを依存関係に含めている状態でJibでDockerイメージを作って実行すると、Dockerコンテナの実行時にSpring Boot Devtoolsが有効になってしまうということです。

Spring Boot Devtools自体は、パッケージングしての実行時には無効になると書かれています。

> Developer tools are automatically disabled when running a fully packaged application. If your application is launched from java -jar or if it is started from a special classloader, then it is considered a “production application”.

[Developing with Spring Boot / Developer Tools](https://docs.spring.io/spring-boot/docs/2.7.0/reference/html/using.html#using.devtools)

そもそも、プロダクション環境ではDevtoolは有効にするべきではないともされています。

> This must not be done in a production environment where running devtools is a security risk.

ここで、JibのドキュメントにSpring Bootに特化した記述があるので見てみます。

[Frequently Asked Questions (FAQ) / Jib CLI / How does the jar command support Spring Boot JARs?](https://github.com/GoogleContainerTools/jib/blob/v3.2.1-maven/docs/faq.md#how-does-the-jar-command-support-spring-boot-jars)

推奨されているのは「Exploded Mode」というモードで、`java -cp /app org.springframework.boot.loader.JarLauncher`という形式で実行する、と書いています。  
※実際に動かすと、ちょっと違った感じでしたが…

すると、そのままだとSpring Boot Devtoolsが有効になってしまうという話です。

これを無効にするには、Dockerイメージを作成する際にSpring Boot Devtoolsのスコープを`provided`にしてパッケージングの際に含まれないようにすれば良さそうです。

https://github.com/GoogleContainerTools/jib/issues/2336

https://github.com/GoogleContainerTools/jib/issues/1254

もしくは、実行時に`-Dspring.devtools.restart.enabled=false`と指定するかですね。

できればDockerイメージ作成の際に外しておいた方が無難かな、と思います。

というわけで、実際の動作を確認してみましょう。

# 環境

今回の環境は、こちらです。

```shell
$ java --version
openjdk 11.0.15 2022-04-19
OpenJDK Runtime Environment (build 11.0.15+10-Ubuntu-0ubuntu0.20.04.1)
OpenJDK 64-Bit Server VM (build 11.0.15+10-Ubuntu-0ubuntu0.20.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: /home/charon/.sdkman/candidates/maven/current
Java version: 11.0.15, vendor: Private Build, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-120-generic", arch: "amd64", family: "unix"
```

```shell
$ docker version
Client: Docker Engine - Community
 Version:           20.10.17
 API version:       1.41
 Go version:        go1.17.11
 Git commit:        100c701
 Built:             Mon Jun  6 23:02:57 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.17
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.17.11
  Git commit:       a89b842
  Built:            Mon Jun  6 23:01:03 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.6
  GitCommit:        10c12954828e7c7c9b6e0ea9b0c02b01407d3ae1
 runc:
  Version:          1.1.2
  GitCommit:        v1.1.2-0-ga916309
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```

# Spring Bootプロジェクトを作成して、Jibを加える

まずはSpring Bootプロジェクトを作成します。依存関係には、`web`と`devtools`を含めておきました。

```shell
$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=2.7.0 \
  -d javaVersion=11 \
  -d name=devtools-with-jib \
  -d groupId=com.example \
  -d artifactId=devtools-with-jib \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=com.example.spring \
  -d dependencies=web,devtools \
  -d baseDir=devtools-with-jib | tar zxvf -
```

プロジェクト内に移動。

```shell
$ cd devtools-with-jib
```

依存関係等はこちらです。

```xml
        <properties>
                <java.version>11</java.version>
        </properties>
        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                </dependency>

                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-devtools</artifactId>
                        <scope>runtime</scope>
                        <optional>true</optional>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-test</artifactId>
                        <scope>test</scope>
                </dependency>
        </dependencies>

        <build>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>
```

ここに、JibのMavenプラグインを追加します。

```xml
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
			<plugin>
				<groupId>com.google.cloud.tools</groupId>
				<artifactId>jib-maven-plugin</artifactId>
				<version>3.2.1</version>
			</plugin>
		</plugins>
	</build>
```

あとは、ソースコードを作成しましょう。

いったん、生成されたソースコードは削除しておきます。

```shell
$ rm src/main/java/com/example/spring/DevtoolsWithJibApplication.java src/test/java/com/example/spring/DevtoolsWithJibApplicationTests.java
```

簡単なソースコードを作成。

```java:src/main/java/com/example/spring/App.java
package com.example.spring;

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

@SpringBootApplication
@RestController
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @GetMapping
    public String hello() {
        return "Hello World";
    }
}
```

# 確認してみる

では、Dockerイメージを作成してみます。

```shell
$ mvn package jib:dockerBuild

## 以下でも同じ
$ mvn compile jib:dockerBuild
```

実行。

```shell
$ docker container run -it --rm -p 8080:8080 --name app devtools-with-jib:0.0.1-SNAPSHOT
```

ログを見るとわかりますが、Spring Boot Devtoolsが有効になっています。

```shell
2022-06-22 15:25:21.171  INFO 1 --- [  restartedMain] com.example.spring.App                   : Starting App using Java 11.0.15 on 173c614374c4 with PID 1 (/app/classes started by root in /)
2022-06-22 15:25:21.177  INFO 1 --- [  restartedMain] com.example.spring.App                   : No active profile set, falling back to 1 default profile: "default"
2022-06-22 15:25:21.563  INFO 1 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2022-06-22 15:25:21.569  INFO 1 --- [  restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2022-06-22 15:25:26.686  INFO 1 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
```

スレッド名も、`restartedMain`ですからね。

コンテナ内の実行コマンドは、こんな感じです。

```shell
$ docker container exec -it app sh -c 'ps -ef | grep java | grep -v grep'
root           1       0 62 15:25 pts/0    00:00:39 java -cp @/app/jib-classpath-file com.example.spring.App
```

もちろん、ふつうに動いてはいますが。

```shell
$ curl localhost:8080
Hello World
```

というわけで、Spring Boot Devtoolsが含まれているのはちょっと嫌なので、これをなんとかしましょう。

# Spring Boot Devtoolsのスコープを変更する

まずはJibのissueにあったとおり、Spring Boot Devtoolsのスコープを変更してみましょう。

https://github.com/GoogleContainerTools/jib/issues/2336

`provided`にすれば、パッケージングの際に含まれなくなることを利用したものです。

スコープをプロパティとして定義して

```xml
	<properties>
		<java.version>11</java.version>
		<devtools.dependency.scope>runtime</devtools.dependency.scope>
	</properties>
```

Spring Boot Devtoolsの`scope`として設定。デフォルトは`runtime`とします。

```xml
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>${devtools.dependency.scope}</scope>
			<optional>true</optional>
		</dependency>
```

あとは、こんな感じでパッケージング時にスコープを指定。

```shell
$ mvn package jib:dockerBuild -Ddevtools.dependency.scope=provided
```

これで、Spring Boot Devtoolsが有効にならなくなりました。

```shell
2022-06-22 15:29:28.859  INFO 1 --- [           main] com.example.spring.App                   : Starting App using Java 11.0.15 on a1826ae55975 with PID 1 (/app/classes started by root in /)
2022-06-22 15:29:28.862  INFO 1 --- [           main] com.example.spring.App                   : No active profile set, falling back to 1 default profile: "default"
2022-06-22 15:29:30.187  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
```

# `spring.devtools.restart.enabled`プロパティを`false`にする

もうひとつの方法は、`spring.devtools.restart.enabled`プロパティを`false`にします。

今回は、JibのMavenプラグインの`jvmFlags`として指定。

```xml
			<plugin>
				<groupId>com.google.cloud.tools</groupId>
				<artifactId>jib-maven-plugin</artifactId>
				<version>3.2.1</version>
				<configuration>
					<container>
						<jvmFlags>
							<jvmFlag>-Dspring.devtools.restart.enabled=false</jvmFlag>
						</jvmFlags>
					</container>
				</configuration>
			</plugin>
```

コンテナイメージの作成。

```shell
$ mvn package jib:dockerBuild
```

こちらでも、先ほどと同様の結果になります。

```shell
2022-06-22 15:32:11.576  INFO 1 --- [           main] com.example.spring.App                   : Starting App using Java 11.0.15 on dfa3860ded97 with PID 1 (/app/classes started by root in /)
2022-06-22 15:32:11.578  INFO 1 --- [           main] com.example.spring.App                   : No active profile set, falling back to 1 default profile: "default"
2022-06-22 15:32:11.658  INFO 1 --- [           main] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2022-06-22 15:32:12.823  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
```