Docker 環境で Spring Boot Webサービスを起動する:GraalVM ネイティブイメージ
こんにちは、@studio_meowtoon です。今回は、WSL Ubuntu の Docker 環境で、GraalVM ネイティブイメージ Spring Boot Web アプリケーションをコンテナとして起動する方法を紹介します。
目的
Windows 11 の Linux でクラウド開発します。
こちらから記事の一覧がご覧いただけます。
実現すること
ローカル環境の WSL Ubuntu の Docker 環境で、Dockerfile からビルドした GraalVM ネイティブイメージ Spring Boot Web サービスのカスタムコンテナイメージを起動します。
ネイティブイメージ形式のアプリをコンテナとして起動
実行環境
要素 | 概要 |
---|---|
terminal | ターミナル |
Ubuntu | OS |
Docker | コンテナ実行環境 |
Web サービス コンテナ
要素 | 概要 |
---|---|
app-hello-spring-boot-native | カスタムコンテナ |
app | ネイティブイメージ アプリケーション |
tomcat | Web サーバー |
技術トピック
Java におけるネイティブイメージビルドとは?
こちらを展開してご覧いただけます。
キーワード | 内容 |
---|---|
ネイティブイメージビルド | Java コードをネイティブマシンコードにコンパイルすることです。通常、Java コードは Java バイトコードと呼ばれる中間言語にコンパイルされ、Java 仮想マシン (JVM) で実行されます。しかし、ネイティブイメージビルドは、Java コードを JVM を介さずに直接実行可能なネイティブマシンコードに変換することで、より高速な実行速度とより低いメモリ使用量を実現することができます。 |
ネイティブイメージビルドは、以下のようなニーズから求められています。
キーワード | 内容 |
---|---|
パフォーマンスの向上 | Java は一般的に高水準のプログラミング言語であり、JVM によって実行されるため、実行速度が遅いとされることがあります。ネイティブイメージビルドにより、高速な実行速度を実現することができます。 |
メモリの最適化 | JVM による Java コードの実行には、多くのメモリが必要となることがあります。ネイティブイメージビルドにより、より少ないメモリ使用量でプログラムを実行できるようになります。 |
ネイティブプログラムの統合 | Java は、C言語や C++ などの他のプログラミング言語で書かれたネイティブプログラムと統合することができます。しかし、統合するためには、ネイティブコードが必要になります。ネイティブイメージビルドにより、これらのネイティブプログラムと Java コードをシームレスに統合することができます。 |
Dockerfile とは?
こちらを展開してご覧いただけます。
Dockerfile
Dockerfile は、Docker コンテナを構築するためのテキストファイルです。Docker コンテナはアプリケーションやサービスを実行するための環境を含む軽量でポータブルな仮想化ユニットです。
キーワード | 内容 |
---|---|
スクリプト形式 | Dockerfile はシンプルなスクリプト形式で記述されるため、コンテナのビルドプロセスを自動化することが容易です。 |
レイヤー構造 | Docker イメージは Dockerfile の各命令が実行される際にレイヤーとして生成され、再利用やキャッシュが可能な構造となっています。 |
バージョン管理 | Dockerfile はテキストベースであるため、コードと同様にバージョン管理システムで管理しやすいです。 |
ポータビリティ | Dockerfile により、アプリケーションとその依存関係が1つのコンテナイメージにパッケージ化されるため、異なる環境間での移植性が高まります。 |
自動化と効率化 | Dockerfile を使用することで、アプリケーションのビルドや環境構築を自動化できます。これにより、手動での作業時間やヒューマンエラーが減り、開発・デプロイプロセスが効率的になります。 |
再現性 | Dockerfile はビルド手順を完全に定義するため、異なる環境で同じアプリケーションを再現できます。これにより、開発、テスト、本番環境間での一貫性が確保されます。 |
環境の分離 | Docker コンテナはホストシステムから分離されるため、アプリケーションの依存関係やライブラリの衝突を回避し、より安全な環境で実行できます。 |
拡張性 | Dockerfile を使用することで、カスタムイメージを作成することができます。このため、特定のニーズに合わせてカスタマイズされたコンテナイメージを容易に作成できます。 |
開発環境
- Windows 11 Home 22H2 を使用しています。
WSL の Ubuntu を操作していきますので macOS の方も参考にして頂けます。
WSL (Microsoft Store アプリ版) ※ こちらの関連記事からインストール方法をご確認いただけます
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
Ubuntu ※ こちらの関連記事からインストール方法をご確認いただけます
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 22.04.1 LTS
Release: 22.04
Java JDK ※ こちらの関連記事からインストール方法をご確認いただけます
$ 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 ※ こちらの関連記事からインストール方法をご確認いただけます
$ 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 ※ こちらの関連記事からインストール方法をご確認いただけます
$ docker --version
Docker version 23.0.1, build a5ee5b1
この記事では基本的に Ubuntu のターミナルで操作を行います。Vim を使用してコピペする方法を初めて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
作成する Web アプリケーションの仕様
No | エンドポイント | HTTPメソッド | MIME タイプ |
---|---|---|---|
1 | /api/data | GET | application/json |
/api/data というエンドポイントに対して HTTP GET リクエストを送信すると、JSON データがレスポンスされるシンプルな Web サービスを実装します。
{"message":"Hello World!"}
Hello World を表示する手順
GraalVM の環境設定
こちらの関連記事で手順がご確認いただけます。
JDK のバージョン確認
Java のバージョンを切り替えます。
$ sudo update-alternatives --config java
$ sudo update-alternatives --config javac
GraalVM が選択されているか確認してください。
$ 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 サービスの作成
こちらの関連記事で手順がご確認いただけます。
プロジェクトフォルダに移動
プロジェクトフォルダに移動します。
※ ~/tmp/hello-spring-boot をプロジェクトフォルダとします。
$ cd ~/tmp/hello-spring-boot
構成を単純にする為に、全ての要素をアプリケーションクラス記述した例
コントローラークラスを削除して、アプリケーションクラスを修正します。
$ rm src/main/java/com/example/springboot/controller/HelloController.java
$ vim src/main/java/com/example/springboot/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;
}
}
pom.xml の修正
pom.xml ファイルを作成します。
$ vim 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 ネイティブイメージが作成されます。
$ mvn -Pnative native:compile
ここまでの手順で、ローカル環境の Ubuntu にアプリのネイティブイメージをビルドすることができました。
ネイティブイメージのコンテナイメージをビルド
Dockerfile を編集します。
$ vim 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 デーモンを起動します。
$ sudo service docker start
* Starting Docker: docker [ OK ]
Docker 環境をお持ちでない場合は、以下の関連記事から Docker Engine のインストール手順をご確認いただけます。
コンテナイメージをビルドします。
$ docker build \
--no-cache \
--tag app-hello-spring-boot-native:latest .
コンテナイメージを確認します。
$ docker images | grep app-hello-spring-boot-native
app-hello-spring-boot-native latest f0794df4d28c 23 minutes ago 149MB
ここまでの手順で、ローカル環境の Docker にアプリのネイティブイメージのカスタムコンテナイメージをビルドすることができました。
コンテナを起動
ローカルでコンテナを起動します。
※ コンテナを停止するときは ctrl + C を押します。
$ docker run --rm \
--publish 8080:8080 \
--name app-local \
app-hello-spring-boot-native:latest
従来の JVM のコンテナと比べ起動時間がとても速くなっています。
2023-03-14T11:09:55.330Z INFO 1 --- [main] com.example.springboot.Application : Started Application in 0.073 seconds (process running for 0.08)
詳細な出力を表示します。
$ 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)
ここまでの手順で、ローカル環境の Docker でアプリのネイティブイメージのカスタムコンテナを起動することができました。
コンテナの動作確認
別ターミナルから curl コマンドで確認します。
$ curl -v http://localhost:8080/api/data -w '\n'
出力
* 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!"}
ここまでの手順で、ターミナルに {"message":"Hello World!"} と表示され、JSON データを取得することが出来ました。
コンテナに接続
別ターミナルからコンテナに接続します。
$ 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 コマンドで状況を確認します。
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
Spring Boot として動作していますが、java コマンドが起動していないことが確認できます。つまり JVM を必要としません。
コンテナの情報を表示してみます。
# 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
このコンテナは Ubuntu をベースに作成されています。つまり、Ubuntu と同じように扱うことができます。
まとめ
WSL Ubuntu の Docker 環境で、Dockerfile からビルドした GraalVM ネイティブイメージ Spring Boot Web アプリのカスタムコンテナを起動することができました。
Dockerfile の理解は重要です。自動ビルドツールもありますが、手動で書く必要があるケースもあります。Ubuntu を使うと Linux の知識も身に付きます。最初は難しく感じるかもしれませんが、徐々に進めていけば自信を持って書けるようになります。
どうでしたか? WSL Ubuntu で、GraalVM ネイティブイメージ Spring Boot Web アプリケーションを、Docker 環境でコンテナとして手軽に起動することができます。ぜひお試しください。今後も Java の開発環境などを紹介していきますので、ぜひお楽しみにしてください。
推奨コンテンツ
関連記事
通常の JAR ファイル形式のビルドと比較してみましょう!