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

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

こんにちは、@studio_meowtoon です。今回は、WSL Ubuntu の Docker 環境で、GraalVM ネイティブイメージ Java Quarkus Web アプリケーションをコンテナとして起動する方法を紹介します。
![quarkus_on_docker.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/d7d7be58-f78d-ea0e-a4da-972d584b802b.png)

## 目的

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

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

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

## 実現すること

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

**ネイティブイメージ形式のアプリをコンテナとして起動**
```mermaid
flowchart LR
    terminal --> app[app-runner]
    subgraph Ubuntu
        subgraph Docker
            subgraph container[app-hello-quarkus-native]
                subgraph app[app-runner]
                    netty
                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 netty fill:#FFFFDE, stroke:#B4AE43
```

**実行環境**

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

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

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

:::note alert
**Quarkus** では、**コマンド**で[**簡単にコンテナイメージを作成できる機能**](https://ja.quarkus.io/guides/container-image#docker)があります。しかし、この記事では Dockerfile を使用してコンテナイメージをビルドすることで、Docker の基礎的な使い方を学習することを目的としています。ご注意ください。
:::

## 技術トピック 

### 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.7" 2023-04-18
OpenJDK Runtime Environment (build 17.0.7+7-Ubuntu-0ubuntu122.04.2)
OpenJDK 64-Bit Server VM (build 17.0.7+7-Ubuntu-0ubuntu122.04.2, 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 を表示する手順 

### Java Quarkus Web サービスの作成  

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

https://qiita.com/studio_meowtoon/items/695b6b5bc31adee105c9

#### アプリケーションのメインコード

```java:HelloResource.java
package com.example.quarkus;

import java.util.Map;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/api")
public class HelloResource {

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

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

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

```sh
$ cd ~/tmp/hello-quarkus
```

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

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

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

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

```sh
$ mvn clean package -Pnative
```

:::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-runner app-runner

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

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

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

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

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

```
$ docker images | grep app-hello-quarkus-native
app-hello-quarkus-native    latest    40e57c48641e    9 seconds ago    122MB
```

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

### コンテナを起動

コンテナを起動します。  

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

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

```sh
__  ____  __  _____   ___  __ ____  ______
 --/ __ \/ / / / _ | / _ \/ //_/ / / / __/
 -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2023-07-31 11:55:35,941 INFO  [io.quarkus] (main) hello-quarkus 1.0 native (powered by Quarkus 2.16.8.Final) started in 0.011s. Listening on: http://0.0.0.0:8080
2023-07-31 11:55:35,942 INFO  [io.quarkus] (main) Profile prod activated.
2023-07-31 11:55:35,942 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy-reactive, resteasy-reactive-jackson, smallrye-context-propagation, vertx]
```

</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 OK
< content-length: 26
< Content-Type: application/json;charset=UTF-8
<
* 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 42M
drwxr-xr-x 1 root root 4.0K Jul 31 12:12 .
drwxr-xr-x 1 root root 4.0K Jul 31 12:12 ..
-rwxr-xr-x 1 root root  42M Jul 31 12:12 app-runner
```

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

```sh
top - 12:15:13 up  6:25,  0 users,  load average: 0.32, 1.85, 1.42
Tasks:   3 total,   1 running,   2 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.4 us,  0.3 sy,  0.0 ni, 99.2 id,  0.0 wa,  0.0 hi,  0.1 si,  0.0 st
MiB Mem :   7897.1 total,   4321.1 free,   1969.0 used,   1607.0 buff/cache
MiB Swap:   2048.0 total,   2018.3 free,     29.7 used.   5663.8 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
    1 root      20   0 1353552  62468  31756 S   0.0   0.8   0:00.03 app-runner
   25 root      20   0    4624   3768   3200 S   0.0   0.0   0:00.02 bash
   35 root      20   0    7312   3048   2720 R   0.0   0.0   0:00.00 top
```

:::note warn
**Quarkus として動作**していますが、**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 ネイティブイメージ Java Quarkus Web アプリのカスタムコンテナを起動することができました。

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

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

## 推奨コンテンツ

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

## 関連記事

#### Java Spring Boot：GraalVM ネイティブイメージ

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