Java
kubernetes
Quarkus

KubernetesネイティブなJavaフレームワーク Quarkus について調査してみた件


はじめに

2019/3/7にRed HatがKubernetesネイティブなJavaフレームワーク「Quarkus(くぉーかす)」を発表しました。

Introducing Quarkus: a next-generation Kubernetes native Java framework

https://developers.redhat.com/blog/2019/03/07/quarkus-next-generation-kubernetes-native-java-framework/

Kubernetesの利用やマイクロサービス・サーバレス化が進み、アプリケーションのコンテナ化が一般的になりつつある現在、他の言語に比べて起動が遅いことはJavaにとって大きなデメリットでした。

これに対して、Quarkusでは "Supersonic Subatomic Java (超音速・原子より小さいJava)" というキャッチコピーが付いているように、GraalVMを用いて作成したLinuxのNativeバイナリをコンテナ上で起動することにより、Javaの起動時間を劇的に短縮しています。

Quarkusについての紹介は既に様々な方がブログを書かれていると思うので、本記事では「Quarkus登場の背景」や「Quarkusを使ったアプリケーションのKubernetesへのデプロイ」などについても書こうと思います。


Quarkus 登場の背景

Docker・Kubernetesの流行やマイクロサービス・サーバレス化の流れなどから、アプリケーションのコンテナ化は一般的になりつつあります。

これらのコンテナアプリケーションの開発には、GolangやRubyなど軽量で起動時間が短くメモリ使用量の少ない言語が採用されることが多く、起動時間が遅くメモリ使用量の大きなJavaの利用は敬遠されてきていました。

しかし、Javaのエコシステムは20年以上に渡り続いており、2019年現在でも多くの開発者に利用されているポピュラーな言語の一つと言えると思います。

Java開発者がこれまで培ってきた知識・スキルを活かしつつ、KubernetesネイティブなコンテナアプリケーションをJavaを使ってデプロイするため登場したのが「Quarkus」です。

Quarkusは、起動時間の大幅な短縮・メモリ使用量の削減・小さなサイズのコンテナイメージを実現することができます。

これにより、多くのJava開発者をKubernetesのエコシステムに取り込み、コンテナアプリケーションをより一般的に、Kubernetesのエコシステムをより大きくしようとするためリリースされたのが、「Quarkus」であると考えています。


GraalVM とは

GraalVMは、Oracleがオープンソースとして2018年4月に公開した、Java・JavaScript・Ruby・Pythonなどの多言語を、統合された一つのランタイム上で実行させる汎用ランタイムです。

architecture.png

引用: https://www.graalvm.org/docs/img/architecture.png

Javaアプリケーションの開発者にとっては、新しい Just-In-Time コンパイルテクノロジにより、Javaアプリケーションをより高速に実行することができるようになります。

また、GraalVMにはNativeバイナリを作成する機能があり、静的解析を使用して到達可能なコードを見つけて Ahead-Of-Time (AOT) コンパイルを実行することにより、即時実行可能なマシンコードをNativeバイナリに含めることができます。


2019年3月時点の制約事項


CDI実装のサポート/未サポート状況

Quarkusでは、CDI(Contexts and Dependency Injection)を利用することができますが、すべてのCDI仕様を実装できているわけではありません。

サポートしている機能と未サポートの機能は以下の通りです。


サポートしている機能


  • Programming model


    • Managed beans implemented by a Java class


    • @PostConstruct and @PreDestroy lifecycle callbacks

    • Producer methods and fields, disposers

    • Qualifiers

    • Alternatives

    • Stereotypes



  • Dependency injection and lookup


    • Field, constructor and initializer/setter injection

    • Type-safe resolution

    • Programmatic lookup via javax.enterprise.inject.Instance

    • Client proxies

    • Injection point metadata [1]



  • Scopes and contexts



  • Interceptors



  • Events and observers, including asynchronous events


制約事項



  • @SessionScoped and @ConversationScoped are not supported

  • Custom scopes and contexts are not supported

  • Decorators are not supported

  • Portable Extensions are not supported

  • BeanManager - only the following methods are implemented: getBeans(), createCreationalContext(), getReference(), resolve(), getContext(), getEvent() and createInstance()

  • Specialization is not supported

  • beans.xml descriptor content is ignored

  • Passivation and passivating scopes are not supported

  • Transitive interceptor bindings and interceptor methods on superclasses are not implemented yet

参考: https://quarkus.io/guides/cdi-reference.html


ビルド時間

Quarkusでは、GraalVMを使ってLinuxのNativeバイナリを作成していますが、自分の環境ではMavenコマンドを実行してから完了するまで、簡単なアプリにも関わらず「9分」程度の時間が掛かってしまいました。

ホットデプロイを実現する"開発モード"が提供されていますので、ローカル環境で開発する場合にはコンテナ化までは行わず、当面は"開発モード"を使って開発する形になると思います。


Quarkus を使ってアプリケーションを Kubernetes にデプロイしてみよう

ここでは、以下のURLで紹介されている Get Started に従って、簡単な JAX-RS アプリケーションを作成してみます。

QUARKUS - GET STARTED

https://quarkus.io/get-started/

getting-started-architecture.png


事前準備

Quarkusを使ってアプリケーションをKubernetesへデプロイするには、事前に以下の環境を準備する必要があります。


  • Java 8 以上

  • Maven 3.5.3 以上 (Gradleも利用可能)

  • GraalVM

  • Docker Engine

  • Kubernetes (とりあえず動かしてみる程度ならminikubeでも良い)


最初のアプリケーションの作成


Mavenプロジェクトを作成してみよう

Quarkusを使ってアプリケーション開発をするには、以下のコマンド例を用いてMavenプロジェクトを作成してください。

mvn io.quarkus:quarkus-maven-plugin:0.11.0:create \

-DprojectGroupId=org.acme \
-DprojectArtifactId=getting-started \
-DclassName="org.acme.quickstart.GreetingResource" \
-Dpath="/hello"

上記のコマンドを実行すると、以下のようなディレクトリ・ファイルが作成されます。

ここでは、src/main/docker ディレクトリ配下に、Dockerfile のサンプルも自動的に作成されます。

$ tree getting-started/

getting-started/
├── pom.xml
└── src
├── main
│   ├── docker
│   │   └── Dockerfile
│   ├── java
│   │   └── org
│   │   └── acme
│   │   └── quickstart
│   │   └── GreetingResource.java
│   └── resources
│   └── META-INF
│   ├── microprofile-config.properties
│   └── resources
│   └── index.html
└── test
└── java
└── org
└── acme
└── quickstart
├── GreetingResourceTest.java
└── NativeGreetingResourceIT.java

自動生成された pom.xml には、以下のように Quarkus BOM と quarkus-maven-plugin があらかじめ設定されています。


pom.xml

<dependencyManagement>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>



アプリケーションのコードを確認してみよう

上記のコマンドによりMavenプロジェクトを作成すると、以下のサンプルコードが生成されます。


src/main/java/org/acme/quickstart/GreetingResource.java

package org.acme.quickstart;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello")
public class GreetingResource {

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}



開発モードで起動してみよう

次に"開発モード"を利用してアプリケーションの起動を行ってみます。

"開発モード"では、バックグラウンドコンパイルによるホットデプロイが可能となり、Javaファイルを更新してブラウザを更新すると、Javaファイルが自動でコンパイルされ変更後のアプリケーションを確認できるようになります。

$ mvn compile quarkus:dev

サンプルの JAX-RS アプリケーションは、以下のコマンドを実行することで動作確認ができます。

$ curl http://localhost:8080/hello

hello


CDI(Contexts and Dependency Injection)を試してみよう

次に、自動生成されたサンプルアプリケーションのカスタマイズを実施してみます。

ここでは、サービスクラスを新規作成し、自動生成された GreetingResource.java に対してサービスクラスの呼び出しを追加します。

以下のサービスクラスを新規追加します。


src/main/java/org/acme/quickstart/GreetingService.java

package org.acme.quickstart;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class GreetingService {

public String greeting(String name) {
return "hello " + name;
}

}


次に、自動生成された GreetingResource.java を修正します。


src/main/java/org/acme/quickstart/GreetingResource.java

package org.acme.quickstart;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.inject.Inject;
import javax.ws.rs.PathParam;

@Path("/hello")
public class GreetingResource {

@Inject
GreetingService service;

@GET
@Produces(MediaType.TEXT_PLAIN)
@Path("/greeting/{name}")
public String greeting(@PathParam("name") String name) {
return service.greeting(name);
}

@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "hello";
}
}


以下のコマンドにより動作確認を行います。

$ curl http://localhost:8080/hello/greeting/quarkus

hello quarkus


LinuxのNativeバイナリの作成


Nativeバイナリを生成しよう

次に、GraalVMを使ってLinuxのNativeバイナリを作成するのですが、自動生成された pom.xml には profile に executions があらかじめ設定されています。

以下の設定により、Mavenコマンドを実行することで、LinuxのNativeバイナリを作成できるようになります。


pom.xml

<profiles>

<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<configuration>
<enableHttpUrlHandler>true</enableHttpUrlHandler>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

以下のMavenコマンドにより、LinuxのNativeバイナリを作成します。

$ mvn package -Pnative -Dnative-image.docker-build=true

ここで、-Pnative オプションはNativeで実行可能なバイナリを生成するためのオプションであり、-Dnative-image.docker-build=true オプションはDocker上のOSに合わせたNativeバイナリの生成を明示するために必要なオプションです。

ちなみに、自分の環境で実行してみたところ、Mavenコマンドの完了までに「約9分」掛かってしまいました。

docker buildも実行していない状態でこれだけ時間が掛かってしまうと頻繁にビルドができなくなってしまうので、将来的な機能改善を期待したいと考えています。

[INFO] ------------------------------------------------------------------------

[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 08:48 min
[INFO] Finished at: 2019-03-18T12:16:35+09:00
[INFO] ------------------------------------------------------------------------


Dockerをビルドしよう

containerization-process.png

Mavenプロジェクトを作成すると、自動的にサンプルの Dockerfile が生成されるため、このファイルをベースにDockerイメージを作成します。

FROM registry.fedoraproject.org/fedora-minimal

WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

以下のコマンドを使って、Dockerイメージを作成してください。

$ docker build -f src/main/docker/Dockerfile -t quarkus-quickstart/quickstart .

作成されたコンテナイメージのサイズを確認してみましょう。

ベースイメージが "fedra-minimal" となっているため125MBなっていますが、Quarkusのサンプル JAX-RS アプリケーションを動かすだけなら「20MB程度」ということが差分から分かると思います。

JDKをインストールするだけで数百MB必要だった頃を考えると、このコンテナイメージサイズはだいぶ小さく見えますね。

$ docker images

REPOSITORY TAG IMAGE ID CREATED SIZE
quarkus-quickstart/quickstart latest d93d89bde0bb About a minute ago 125MB
registry.fedoraproject.org/fedora-minimal latest f0c38118c459 3 weeks ago 105MB


動作確認をしてみよう

次に、Dockerコンテナを起動してみます。

$ docker run -i --rm -p 8080:8080 quarkus-quickstart/quickstart

2019-03-18 03:21:00,052 INFO [io.quarkus] (main) Quarkus 0.11.0 started in 0.012s. Listening on: http://0.0.0.0:8080
2019-03-18 03:21:00,054 INFO [io.quarkus] (main) Installed features: [cdi, resteasy]

自分の環境では、上記の例のように「0.012秒」という、従来のJavaアプリケーションでは考えられないような劇的に短い時間で起動できました。

以下のコマンドにより動作確認もできます。

curl http://localhost:8080/hello

hello

また、メモリ使用量は以下の通りであり、サンプルの JAX-RS アプリケーションでは 1.2MB しか利用していないことが分かります。

$ docker stats

CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
b4d39bbe3159 eloquent_bohr 0.00% 1.281MiB / 1.952GiB 0.06% 1.04kB / 0B 0B / 0B 5


Kubernetesへのデプロイ

ここまでで、JavaソースコードからMavenコマンドによりLinuxのNativeバイナリを作成し、そのLinuxバイナリをコンテナとして起動することができました。

次に、Quarkusを使ったアプリケーションを、Kuberntetes(minikube)上にデプロイしてみたいと思います。

以下のコマンドにより、先程作成したDockerイメージから Deployment・ReplicaSet・Pod リソースを作成します。

$ kubectl run quarkus-quickstart --image=quarkus-quickstart/quickstart:latest --port=8080 --image-pull-policy=IfNotPresent

kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/quarkus-quickstart created

次に、動作確認をできるようにするため、kubectl expose コマンドにより Service リソースを作成します。

$ kubectl expose deployment quarkus-quickstart --type=NodePort

service/quarkus-quickstart exposed

上記の2つのコマンドにより作成されるKubernetesリソースは以下の通りです。

$ kubectl get all

NAME READY STATUS RESTARTS AGE
pod/quarkus-quickstart-7986c7cd95-44k9n 1/1 Running 1 1m

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5m
service/quarkus-quickstart NodePort 10.103.25.16 <none> 8080:32179/TCP 27s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/quarkus-quickstart 1/1 1 1 1m

NAME DESIRED CURRENT READY AGE
replicaset.apps/quarkus-quickstart-7986c7cd95 1 1 1 1m

以下のコマンドにより、Kubernetes(minikube)上にデプロイしたQuarkusアプリケーションの動作確認ができます。

$ curl $(minikube service quarkus-quickstart --url)/hello/greeting/quarkus

hello quarkus


さいごに

本記事では、「Quarkus登場の背景」や「Quarkusを使ったアプリケーションのKubernetesへのデプロイ」などについて書きました。

これまでに記載してきたように、Quarkusにより起動が劇的に早くなっただけでなく、小さなイメージサイズとメモリ使用量といったコンテナアプリケーションに必須の要素が、エンタープライズをはじめ開発者が非常に多いJavaで実現できるようになったという意味で非常に重要なリリースであったと考えています。

しかし、まだリリースされたばかりであることから、CDIに様々な制約が存在することやビルドに長い時間が掛かってしまうことも分かりました。

また、QuarkusはRed Hatがリリースしたプロダクトらしく、OSS(オープンソースソフトウェア)として開発されています。

リリースされたばかりであり、今後も機能追加・機能改善していくと思いますので、引き続きウォッチを継続して行きたいと考えています。


参考情報