Docker 環境で Spring Boot Webサービスを起動する：GraalVM ネイティブイメージ

# Docker 環境で Spring Boot Webサービスを起動する：GraalVM ネイティブイメージ

こんにちは、@studio_meowtoon です。今回は、WSL Ubuntu の Docker 環境で、GraalVM ネイティブイメージ Spring Boot Web アプリケーションをコンテナとして起動する方法を紹介します。

![spring-boot_graalvm_on_docker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/e385a4bb-6482-4604-b89b-b34149957583.png)

## 目的

Windows 11 の Linux でクラウド開発します。

こちらから記事の一覧がご覧いただけます。

https://qiita.com/studio_meowtoon/items/6119ab05d8b13527efd8

## 実現すること

ローカル環境の WSL Ubuntu の Docker 環境で、Dockerfile からビルドした GraalVM ネイティブイメージ Spring Boot Web サービスのカスタムコンテナイメージを起動します。

**ネイティブイメージ形式のアプリをコンテナとして起動**
```mermaid
flowchart LR
    terminal --> app[app]
    subgraph Ubuntu
        subgraph Docker
            subgraph container[app-hello-spring-boot-native]
                subgraph app[app]
                    tomcat
                end
            end
        end
    end
style Ubuntu fill:#FFBCA4, stroke:#E0460F
style Docker fill:#EEFDFF, stroke:#74B9DC
style container fill:#FFD7E4, stroke:#E13C73
style app fill:#ECECFF, stroke:#9674DC
style tomcat fill:#FFFFDE, stroke:#B4AE43
```

**実行環境**

|要素|概要|
|:--|:--|
|terminal|ターミナル|
|Ubuntu|OS|
|Docker|コンテナ実行環境|

**Web サービス コンテナ**

|要素|概要|
|:--|:--|
|app-hello-spring-boot-native|カスタムコンテナ|
|app|ネイティブイメージ アプリケーション|
|tomcat|Web サーバー|

## 技術トピック

### Java におけるネイティブイメージビルドとは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

:::note info
|キーワード|内容|
|:--|:--|
|ネイティブイメージビルド|Java コードをネイティブマシンコードにコンパイルすることです。通常、Java コードは Java バイトコードと呼ばれる中間言語にコンパイルされ、Java 仮想マシン (JVM) で実行されます。しかし、**ネイティブイメージビルド**は、Java コードを **JVM を介さず**に直接実行可能な**ネイティブマシンコードに変換**することで、**より高速な実行速度**と**より低いメモリ使用量**を実現することができます。|

ネイティブイメージビルドは、以下のようなニーズから求められています。  

|キーワード|内容|
|:--|:--|
|パフォーマンスの向上|Java は一般的に高水準のプログラミング言語であり、JVM によって実行されるため、実行速度が遅いとされることがあります。ネイティブイメージビルドにより、高速な実行速度を実現することができます。|
|メモリの最適化|JVM による Java コードの実行には、多くのメモリが必要となることがあります。ネイティブイメージビルドにより、より少ないメモリ使用量でプログラムを実行できるようになります。|
|ネイティブプログラムの統合|Java は、C言語や C++ などの他のプログラミング言語で書かれたネイティブプログラムと統合することができます。しかし、統合するためには、ネイティブコードが必要になります。ネイティブイメージビルドにより、これらのネイティブプログラムと Java コードをシームレスに統合することができます。|
:::

</details>

### Dockerfile とは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### [Dockerfile](https://docs.docker.jp/engine/reference/builder.html#dockerfile)

:::note info
Dockerfile は、Docker コンテナを構築するためのテキストファイルです。Docker コンテナはアプリケーションやサービスを実行するための環境を含む軽量でポータブルな仮想化ユニットです。

|キーワード|内容|
|:--|:--|
|スクリプト形式|Dockerfile はシンプルなスクリプト形式で記述されるため、コンテナのビルドプロセスを自動化することが容易です。|
|レイヤー構造|Docker イメージは Dockerfile の各命令が実行される際にレイヤーとして生成され、再利用やキャッシュが可能な構造となっています。|
|バージョン管理|Dockerfile はテキストベースであるため、コードと同様にバージョン管理システムで管理しやすいです。|
|ポータビリティ|Dockerfile により、アプリケーションとその依存関係が1つのコンテナイメージにパッケージ化されるため、異なる環境間での移植性が高まります。|
|自動化と効率化|Dockerfile を使用することで、アプリケーションのビルドや環境構築を自動化できます。これにより、手動での作業時間やヒューマンエラーが減り、開発・デプロイプロセスが効率的になります。|
|再現性|Dockerfile はビルド手順を完全に定義するため、異なる環境で同じアプリケーションを再現できます。これにより、開発、テスト、本番環境間での一貫性が確保されます。|
|環境の分離|Docker コンテナはホストシステムから分離されるため、アプリケーションの依存関係やライブラリの衝突を回避し、より安全な環境で実行できます。|
|拡張性|Dockerfile を使用することで、カスタムイメージを作成することができます。このため、特定のニーズに合わせてカスタマイズされたコンテナイメージを容易に作成できます。|
:::

</details>

## 開発環境

+ Windows 11 Home 22H2 を使用しています。  

:::note info
[WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作していきますので macOS の方も参考にして頂けます。  
:::

<details>

WSL (Microsoft Store アプリ版) [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
```

Ubuntu [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
```

Java JDK [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/4d11e94a2389758759cd)

```sh
$ java -version
openjdk version "17.0.6" 2023-01-17
OpenJDK Runtime Environment GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13, mixed mode, sharing)
```

Maven [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/f4cdc4039158153365fb)

```sh
$ mvn -version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.18, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
```

Docker [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/3bceee25ac67cc73ebbe)

```sh
$ docker --version
Docker version 23.0.1, build a5ee5b1
```

</details>

:::note warn
この記事では基本的に **Ubuntu のターミナルで操作**を行います。**Vim を使用してコピペする方法**を初めて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
:::

https://qiita.com/studio_meowtoon/items/7771ad48676926e41463

## 作成する Web アプリケーションの仕様

|No|エンドポイント|HTTPメソッド|MIME タイプ|
|:--|:--|:--|:--|
|1|/api/data|GET|application/json|

:::note info
**/api/data** というエンドポイントに対して HTTP GET リクエストを送信すると、JSON データがレスポンスされるシンプルな Web サービスを実装します。
**{"message":"Hello World!"}**
:::

## Hello World を表示する手順 

### GraalVM の環境設定

こちらの関連記事で手順がご確認いただけます。

https://qiita.com/studio_meowtoon/items/ab5c7758874683367540

### JDK のバージョン確認  

Java のバージョンを切り替えます。

```sh
$ sudo update-alternatives --config java
$ sudo update-alternatives --config javac
```

:::note alert
**GraalVM** が選択されているか確認してください。  
:::

```sh
$ java -version
openjdk version "17.0.6" 2023-01-17
OpenJDK Runtime Environment GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13)
OpenJDK 64-Bit Server VM GraalVM CE 22.3.1 (build 17.0.6+10-jvmci-22.3-b13, mixed mode, sharing)
```

### Spring Boot Web サービスの作成  

こちらの関連記事で手順がご確認いただけます。

https://qiita.com/studio_meowtoon/items/37ac0082a3228e4de570

### プロジェクトフォルダに移動

プロジェクトフォルダに移動します。  
※ ~/tmp/hello-spring-boot をプロジェクトフォルダとします。    

```sh
$ cd ~/tmp/hello-spring-boot
```

<details>
<summary>構成を単純にする為に、全ての要素をアプリケーションクラス記述した例</summary>

コントローラークラスを削除して、アプリケーションクラスを修正します。

```sh
$ rm src/main/java/com/example/springboot/controller/HelloController.java
$ vim src/main/java/com/example/springboot/Application.java
```

ファイルの内容

```java:Application.java
package com.example.springboot;

import java.util.Map;

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

@RestController
@SpringBootApplication
@RequestMapping("/api")
public class Application {

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

    @GetMapping("/data")
    public Map<String, String> getData() {
        Map<String, String> map = Map.of("message", "Hello World!");
        return map;
    }
}
```

</details>

### pom.xml の修正

pom.xml ファイルを作成します。

```sh
$ vim pom.xml
```

```xml:pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>hello-spring-boot</artifactId>
    <version>1.0</version>
    <name>hello-spring-boot</name>

    <properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

    <build>
        <finalName>app</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <configuration>
                    <imageName>app</imageName>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
```

### ネイティブイメージをビルド

ネイティブイメージをビルドします。  
※ target/app ネイティブイメージが作成されます。

```sh
$ mvn -Pnative native:compile
```

:::note info
ここまでの手順で、ローカル環境の **Ubuntu** にアプリの**ネイティブイメージ**をビルドすることができました。
:::

### ネイティブイメージのコンテナイメージをビルド

Dockerfile を編集します。

```sh
$ vim Dockerfile
```

ファイルの内容

```Dockerfile:Dockerfile
# because the native image app was built by Ubuntu 22.04.
FROM ubuntu:22.04

# create a work dir.
WORKDIR /app

# copy a native image app.
COPY target/app app

# open port 8080 for a native image app.
EXPOSE 8080

# startup a native image app.
ENTRYPOINT ["./app"]
```

Docker デーモンを起動します。

```sh
$ sudo service docker start
 * Starting Docker: docker    [ OK ]
```

:::note warn
Docker 環境をお持ちでない場合は、以下の関連記事から Docker Engine のインストール手順をご確認いただけます。
:::

https://qiita.com/studio_meowtoon/items/3bceee25ac67cc73ebbe

コンテナイメージをビルドします。

```sh
$ docker build \
    --no-cache \
    --tag app-hello-spring-boot-native:latest .
```

コンテナイメージを確認します。

```sh
$ docker images | grep app-hello-spring-boot-native
app-hello-spring-boot-native   latest     f0794df4d28c   23 minutes ago   149MB
```

:::note info
ここまでの手順で、ローカル環境の **Docker** にアプリの**ネイティブイメージ**の**カスタムコンテナイメージ**をビルドすることができました。
:::

### コンテナを起動

ローカルでコンテナを起動します。
※ コンテナを停止するときは ctrl + C を押します。

```sh
$ docker run --rm \
    --publish 8080:8080 \
    --name app-local \
    app-hello-spring-boot-native:latest
```

従来の JVM のコンテナと比べ起動時間がとても速くなっています。

```sh
2023-03-14T11:09:55.330Z  INFO 1 --- [main] com.example.springboot.Application : Started Application in 0.073 seconds (process running for 0.08)
```

<details>
<summary>詳細な出力を表示します。</summary>

```sh
$ docker run --rm --name app-local -p 8080:8080 app-hello-spring-boot-native:latest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.4)

2023-03-14T11:09:55.275Z  INFO 1 --- [           main] com.example.springboot.Application       : Starting AOT-processed Application using Java 17.0.6 with PID 1 (/workspace/com.example.springboot.Application started by cnb in /workspace)
2023-03-14T11:09:55.275Z  INFO 1 --- [           main] com.example.springboot.Application       : No active profile set, falling back to 1 default profile: "default"
2023-03-14T11:09:55.290Z  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-03-14T11:09:55.291Z  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-03-14T11:09:55.291Z  INFO 1 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.5]
2023-03-14T11:09:55.298Z  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-03-14T11:09:55.298Z  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 22 ms
2023-03-14T11:09:55.329Z  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-03-14T11:09:55.330Z  INFO 1 --- [           main] com.example.springboot.Application       : Started Application in 0.073 seconds (process running for 0.08)
```

</details>

:::note info
ここまでの手順で、ローカル環境の **Docker** でアプリの**ネイティブイメージ**の**カスタムコンテナ**を起動することができました。
:::

### コンテナの動作確認

別ターミナルから curl コマンドで確認します。

```sh
$ curl -v http://localhost:8080/api/data -w '\n'
```

出力

```sh
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/data HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Content-Type: application/json
< Transfer-Encoding: chunked
< Date: Fri, 04 Aug 2023 01:52:30 GMT
<
* Connection #0 to host localhost left intact
{"message":"Hello World!"}
```

:::note info
ここまでの手順で、ターミナルに **{"message":"Hello World!"}** と表示され、**JSON データ**を取得することが出来ました。 
:::

### コンテナに接続

別ターミナルからコンテナに接続します。

```sh
$ docker exec -it app-local /bin/bash
```

コンテナに接続後にディレクトリを確認します。 
※ コンテナから出るときは ctrl + D を押します。  

```
# pwd
/app
# ls -lah
total 68M
drwxr-xr-x 1 root root 4.0K Mar 17 14:53 .
drwxr-xr-x 1 root root 4.0K Mar 17 14:53 ..
-rwxr-xr-x 1 root root  68M Mar 17 14:29 app
```

top コマンドで状況を確認します。

```sh
top - 14:58:43 up 10:17,  0 users,  load average: 0.00, 0.00, 0.22
Tasks:   3 total,   1 running,   2 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.4 us,  0.2 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   7763.1 total,   4489.9 free,   1395.1 used,   1878.1 buff/cache
MiB Swap:   2048.0 total,   2046.5 free,      1.5 used.   6110.2 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0 1478396 108644  48388 S   0.0   1.4   0:00.21 app
   35 root      20   0    4624   3748   3236 S   0.0   0.0   0:00.02 bash
   43 root      20   0    7312   3424   2860 R   0.0   0.0   0:00.02 top
```

:::note warn
**Spring Boot として動作**していますが、**java コマンドが起動していない**ことが確認できます。つまり **JVM を必要としません**。
:::

コンテナの情報を表示してみます。

```cs
# cat /etc/*-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.2 LTS"
PRETTY_NAME="Ubuntu 22.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.2 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
```

:::note warn
このコンテナは Ubuntu をベースに作成されています。つまり、Ubuntu と同じように扱うことができます。
:::

## まとめ

WSL Ubuntu の Docker 環境で、Dockerfile からビルドした GraalVM ネイティブイメージ Spring Boot Web アプリのカスタムコンテナを起動することができました。

:::note warn
**Dockerfile の理解**は重要です。自動ビルドツールもありますが、手動で書く必要があるケースもあります。**Ubuntu** を使うと **Linux の知識**も身に付きます。最初は難しく感じるかもしれませんが、徐々に進めていけば自信を持って書けるようになります。
:::

どうでしたか？ WSL Ubuntu で、GraalVM ネイティブイメージ Spring Boot Web アプリケーションを、Docker 環境でコンテナとして手軽に起動することができます。ぜひお試しください。今後も Java の開発環境などを紹介していきますので、ぜひお楽しみにしてください。

## 推奨コンテンツ

https://qiita.com/studio_meowtoon/items/98074a014ad75291a6d8

## 関連記事

通常の JAR ファイル形式のビルドと比較してみましょう！

https://qiita.com/studio_meowtoon/items/9c07e20b4124d8c5f972