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などの多言語を、統合された一つのランタイム上で実行させる汎用ランタイムです。

<img width="836" alt="architecture.png" src="https://qiita-image-store.s3.amazonaws.com/0/136020/593e4a39-6f65-3522-a578-71104ac5edaa.png">

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

Javaアプリケーションの開発者にとっては、新しい Just-In-Time コンパイルテクノロジにより、Javaアプリケーションをより高速に実行することができるようになります。
また、GraalVMにはNativeバイナリを作成する機能があり、静的解析を使用して到達可能なコードを見つけて 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
 * @Dependent, @ApplicationScoped, @Singleton and @RequestScoped
* Interceptors
 * Business method interceptors: @AroundInvoke
 * Interceptors for lifecycle event callbacks: @PostConstruct, @PreDestroy, @AroundConstruct
* 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](https://qiita-image-store.s3.amazonaws.com/0/136020/77f9a0cc-eacd-cfe7-d783-93dcc9a86db9.png)


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

* Java 8 以上
* Maven 3.5.3 以上 (Gradleも利用可能)
* GraalVM
* Docker Engine
* Kubernetes (とりあえず動かしてみる程度ならminikubeでも良い)


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

```sh
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 があらかじめ設定されています。

```xml: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ファイルが自動でコンパイルされ変更後のアプリケーションを確認できるようになります。

```console
$ mvn compile quarkus:dev
```

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

```console
$ 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";
    }
}
```

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

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


## LinuxのNativeバイナリの作成
### Nativeバイナリを生成しよう
次に、GraalVMを使ってLinuxのNativeバイナリを作成するのですが、自動生成された pom.xml には profile に executions があらかじめ設定されています。
以下の設定により、Mavenコマンドを実行することで、LinuxのNativeバイナリを作成できるようになります。

```xml: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バイナリを作成します。

```console
$ 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](https://qiita-image-store.s3.amazonaws.com/0/136020/029ba435-d646-84cf-5a51-9bce006a2539.png)

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

```Dockerfile 
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イメージを作成してください。

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

作成されたコンテナイメージのサイズを確認してみましょう。
ベースイメージが "fedra-minimal" となっているため125MBなっていますが、Quarkusのサンプル JAX-RS アプリケーションを動かすだけなら「20MB程度」ということが差分から分かると思います。
JDKをインストールするだけで数百MB必要だった頃を考えると、このコンテナイメージサイズはだいぶ小さく見えますね。

```console
$ 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コンテナを起動してみます。

```console
$ 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 しか利用していないことが分かります。

```console
$ 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 リソースを作成します。

```console
$ 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 リソースを作成します。

```console
$ kubectl expose deployment quarkus-quickstart --type=NodePort
service/quarkus-quickstart exposed
```

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

```console
$ 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アプリケーションの動作確認ができます。

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


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

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

また、QuarkusはRed Hatがリリースしたプロダクトらしく、OSS(オープンソースソフトウェア)として開発されています。
リリースされたばかりであり、今後も機能追加・機能改善していくと思いますので、引き続きウォッチを継続して行きたいと考えています。


# 参考情報
* [Quarkus](https://quarkus.io/)
* [GraalVM](https://www.graalvm.org/)
* [Javaフレームワーク「Quarkus」登場。Javaコードからネイティブバイナリを生成し瞬時にJavaアプリが起動、コンテナへの最適化を実現。Red Hatがリリース](https://www.publickey1.jp/blog/19/javaquarkusjavajavared_hat.html)