QuarkusとCloud RunではじめるServerless JavaEE 

はじめに
-------

先日のGoogle Cloud Next 19で[Cloud Run](https://cloud.google.com/run/)が公開されましたね。
これはHTTPで起動するDockerコンテナをk8s上でオートスケールさせる[Knative](https://www.knative.dev/docs/)をGoogleがGCP上でフルマネージドで提供するものです。
AWSの[Fargate](https://aws.amazon.com/jp/fargate/)や[Heroku](https://devcenter.heroku.com/articles/container-registry-and-runtime), あるいは僕たちが本当に欲しかった[GAE Flexible Environment](https://cloud.google.com/appengine/docs/flexible/)と言ったところでしょうか。

今回は、こちらにGraalでJavaをバイナリにコンパイルできる超音速/超軽量を謡うJavaEEコンテナの[Quarkus](https://quarkus.io/)を使うことで高速なServerless JavaEEアプリを作ってみます。
もう**「Javaだからスピンアップが遅い」**とか言わせない！

TL;DR
----

- [こちらのできたもの](https://github.com/koduki/example-serverless-javaee)をclone
- `Dockerfile.gcp`をビルドしてCloud Runにデプロイ
- Quarkus速い！ Cloud Run簡単！

Quarkusでサンプルアプリケーションを開発
----------------

### What is Quarkus?

QuarkusはRedhatによって作られた次世代JavaEEコンテナで「Supersonic Subatomic Java」とまで言うだけあって**10msと言う異次元の速度で起動する**と言う他者の追従を許さない特徴を持っています。
主な特徴は以下の通り。

- MicroProfileをサポートしているためJAX-RSやCDI/JPAと言った基本的なJavaEEの機能が利用できる
- GraalVMのnative-imageを使ってJVMではなくLinuxのバイナリとして実行しているためGo言語とかと同等の起動速度
- One configuration, Hot Reload, OpenAPI, Docker/Maven/Gradleのサポートなど単純にJavaEE環境としても開発もしやすい

詳しくは長くなったので別記事にまとめました。興味がある人はこちらも読んでみてください。
「[ブログなんだよもん: Serverless時代のJavaEEコンテナ - Quarkus](http://koduki.hatenablog.com/entry/2019/04/15/063038)」

### サンプルアプリの仕様

では、サンプルアプリケーションを作って行きます。なんでも良いのですが「**銀行API**」を今回は作ってみることにします。

機能としては以下とします。

- 口座を作成できる
- 口座を一覧を確認できる
- 口座に入金できる
- 口座から出金できる

なお、簡略化のためにユーザ管理はしないので誰でも入出金できますw
また口座情報はデータベースに格納することにします。

### プロジェクトの作成

プロジェクトはmavenまたはgradleで作成できます。今回はmavenを使用します。

```bash
% mvn io.quarkus:quarkus-maven-plugin:create
...
Set the project groupId [org.acme.quarkus.sample]: cn.orz.pascal.mybank
Set the project artifactId [my-quarkus-project]: my-bank
Set the project version [1.0-SNAPSHOT]: 
Do you want to create a REST resource? (y/n) [no]: y
Set the resource classname [cn.orz.pascal.mybank.HelloResource]: 
Set the resource path  [/hello]: 
...
[INFO] Finished at: 2019-04-14T17:51:48-07:00
[INFO] ------------------------------------------------------------------------
```

サンプルのエンドポイントとして`/hello`を追加しました。JAX-RSのコードは以下のようになっています。

```java
@Path("/hello")
public class HelloResource {
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}
```

なんの変哲も無いJAX-RSのコードですね。では実行してみましょう。開発モードで起動します。

```
$ mvn compile quarkus:dev
...
2019-04-14 17:52:13,685 INFO  [io.quarkus] (main) Quarkus 0.13.1 started in 2.042s. Listening on: http://[::]:8080
2019-04-14 17:52:13,686 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]
```

curlでアクセスしてみます。

```bash
$ curl http://localhost:8080/hello
hello 
```
無事、アクセスが確認できましたね。

### DB環境の準備

続いて口座を表すAccountテーブルの作成をします。
まずはDBを準備する必要があるのでDockerでpostgresを起動します。

```bash
$ docker run -it -p 5432:5432 -e POSTGRES_PASSWORD=mysecretpassword postgres        
...
2019-04-15 01:29:51.370 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2019-04-15 01:29:51.370 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2019-04-15 01:29:51.374 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2019-04-15 01:29:51.394 UTC [50] LOG:  database system was shut down at 2019-04-15 01:29:51 UTC
2019-04-15 01:29:51.404 UTC [1] LOG:  database system is ready to accept connections
```

### JPAの設定

続いてJPAの設定をします。まずは依存の追加。

```bash
# Extention名の確認
$ mvn quarkus:list-extensions|grep jdbc         
[INFO] 	 * JDBC Driver - H2 (io.quarkus:quarkus-jdbc-h2)
[INFO] 	 * JDBC Driver - MariaDB (io.quarkus:quarkus-jdbc-mariadb)
[INFO] 	 * JDBC Driver - PostgreSQL (io.quarkus:quarkus-jdbc-postgresql)
$ mvn quarkus:list-extensions|grep hibernate
[INFO] 	 * Hibernate ORM (io.quarkus:quarkus-hibernate-orm)
[INFO] 	 * Hibernate ORM with Panache (io.quarkus:quarkus-hibernate-orm-panache)
[INFO] 	 * Hibernate Validator (io.quarkus:quarkus-hibernate-validator)
# Extentionの追加
$ mvn quarkus:add-extension -Dextensions="io.quarkus:quarkus-jdbc-postgresql"
$ mvn quarkus:add-extension -Dextensions="io.quarkus:quarkus-hibernate-orm"

```

次は`src/main/resources/application.properties`にDB設定を記載します。基本的にはQuarkusではpersitance.xmlやlog4j.xmlなど別の設定ファイルを定義せずに全てこの`application.properties`に記載することが推奨されています。
また、このファイルは[microprofile-config](https://microprofile.io/project/eclipse/microprofile-config)が使われて居るので環境変数や引数で上書き可能です。
つまり、開発環境とSTGやProdとの差分はk8sのyaml側で定義することができるので、環境ごとにProfileで設定を切り替える、などのビルド手法が不要になり運用がシンプルにできます。

これは起動時/デプロイ時に環境変数を指定するDokcerやCloud RunあるいはHerokuのような[Twelve-Factor](https://12factor.net/ja/config)を満たした環境に非常にマッチしています。

`application.properties`に追加する設定内容はこちら。見ての通りのDB設定です。

```properties
# datasource account
quarkus.datasource.url: jdbc:postgresql://localhost:5432/postgres
quarkus.datasource.driver: org.postgresql.Driver
quarkus.datasource.username: postgres
quarkus.datasource.password: mysecretpassword

# database optional
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.log.sql: true
```

### 口座作成用のEntity/Service/Resourceの追加

続いて口座に相当する`Account`のEntityとServiceを作ります。
まずはEntity。当然ですが普通にJPAのEntityです。個人的な趣味でPKはUUIDを使っていますが別にシーケンスを使うことも問題なく可能です。

```java
@Entity
public class Account implements Serializable {
    private UUID id;
    private Long amount;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public UUID getId() {
        return id;
    }
    public void setId(UUID id) {
        this.id = id;
    }
    public Long getAmount() {
        return amount;
    }
    public void setAmount(Long amount) {
        this.amount = amount;
    }
}
```

続いてEntityを操作するServiceです。

```java
@ApplicationScoped
public class AccountService {
    @Inject
    EntityManager em;

    @Transactional
    public void create(long amount) {
        Account account = new Account();
        account.setAmount(amount);
        em.persist(account);
    }
}
```

最後にJAX-RSを記述するResourceです。

```java
@Path("/account")
public class AccountResource {
    @Inject
    AccountService accountService;

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/create")
    public void create() {
        accountService.create(0);
    }
}
```

では`/account/create`を叩いてみましょう。

```bash
$ curl -i -X POST -H "Content-Type: application/json" http://localhost:8080/account/create
HTTP/1.1 204 No Content
Date: Mon, 15 Apr 2019 02:56:30 GMT
```

正常終了しました。
また、サーバの標準出力に下記の通り実行したSQLが出ているかと思います。
`quarkus.hibernate-orm.log.sql`を`true`にしたのでSQLログが確認できます。本番では忘れず`false`にしましょう。

```bash
Hibernate: 
    insert 
    into
        Account
        (amount, id) 
    values
        (?, ?)
```

### 残りの口座作成用のAPIの追加

ではサクサクと残りのAPIを追加して行きましょう。`AccountService.java`に一覧と入出金の機能をつけます。

```java
@ApplicationScoped
public class AccountService {

    @Inject
    EntityManager em;

    @Transactional
    public void create(long amount) {
        Account account = new Account();
        account.setAmount(amount);
        em.persist(account);
    }

    @Transactional
    public List<Account> findAll() {
        return em.createQuery("SELECT a FROM Account a", Account.class)
                .setMaxResults(3)
                .getResultList();
    }

    @Transactional
    public Account deposit(UUID id, long amount) {
        em.createQuery("UPDATE Account SET amount = amount + :amount WHERE id=:id")
                .setParameter("id", id)
                .setParameter("amount", amount)
                .executeUpdate();
        return em.createQuery("SELECT a FROM Account a WHERE id=:id", Account.class)
                .setParameter("id", id)
                .getSingleResult();
    }

    @Transactional
    public Account withdraw(UUID id, long amount) {
        em.createQuery("UPDATE Account SET amount = amount - :amount WHERE id=:id")
                .setParameter("id", id)
                .setParameter("amount", amount)
                .executeUpdate();
        return em.createQuery("SELECT a FROM Account a WHERE id=:id", Account.class)
                .setParameter("id", id)
                .getSingleResult();
    }
}
```

続いて`AccountResource.java`を以下のように修正します。

```java
@Path("/account")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class AccountResource {

    @Inject
    AccountService accountService;

    @POST
    public void create() {
        accountService.create(0);
    }

    @GET
    public List<Account> list() {
        return accountService.findAll();
    }

    @POST
    @Path("/deposit/{id}/{amount}")
    public Account deposit(@PathParam("id") UUID id, @PathParam("amount") long amount) {
        System.out.println(id + ":" + amount);
        return accountService.deposit(id, amount);
    }

    @POST
    @Path("/withdraw/{id}/{amount}")
    public Account withdraw(@PathParam("id") UUID id, @PathParam("amount") long amount) {
        System.out.println(id + ":" + amount);
        return accountService.withdraw(id, amount);
    }
}
```

こちらを実行してみます。

```bash
$ curl -X POST -H "Content-Type: application/json" http://localhost:8080/account
$ curl -X GET -H "Content-Type: application/json" http://localhost:8080/account 
[{"amount":0,"id":"0687662d-5ac7-4951-bb11-c9ced6558a40"}]                                               
$ curl -X POST -H "Content-Type: application/json" http://localhost:8080/account/deposit/0687662d-5ac7-4951-bb11-c9ced6558a40/100 
{"amount":100,"id":"0687662d-5ac7-4951-bb11-c9ced6558a40"}                                              
$ curl -X POST -H "Content-Type: application/json" http://localhost:8080/account/deposit/0687662d-5ac7-4951-bb11-c9ced6558a40/100
{"amount":200,"id":"0687662d-5ac7-4951-bb11-c9ced6558a40"}                                                 
$ curl -X POST -H "Content-Type: application/json" http://localhost:8080/account/deposit/0687662d-5ac7-4951-bb11-c9ced6558a40/100
{"amount":300,"id":"0687662d-5ac7-4951-bb11-c9ced6558a40"}                                                  
$ curl -X POST -H "Content-Type: application/json" http://localhost:8080/account/withdraw/0687662d-5ac7-4951-bb11-c9ced6558a40/200
{"amount":100,"id":"0687662d-5ac7-4951-bb11-c9ced6558a40"} 
```

無事、入出金の機能も実装できました。

### ドキュメンテーション

さて、アプリ機能が出来たら次はドキュメンテーションですね。と言ってもQuarkusはOpenAPIとSwagger UIに対応してるのでサクりと出来ます。

まずはExtentionの追加。

```bash
$ mvn quarkus:list-extensions|grep openapi
[INFO] 	 * SmallRye OpenAPI (io.quarkus:quarkus-smallrye-openapi)
$ mvn quarkus:add-extension -Dextensions="io.quarkus:quarkus-smallrye-openapi"
```

Extentionを追加したら`quarkus:dev`を再実行します。そして`http://localhost:8080/openapi`にアクセスします。すると以下のようにJAX-RSから生成されたOpenAPIの定義ファイルが取得できます。

```yaml
openapi: 3.0.1
info:
  title: Generated API
  version: "1.0"
paths:
  /account:
    get:
      responses:
        200:
          description: OK
          content:
            application/json: {}
    post:
...
```

また、同じく`http://localhost:8080/swagger-ui/`にアクセスすれば以下のようなSwagger UIによるドキュメントにアクセスできます。
![004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54904/09afc482-0cdc-7501-64b5-e2de94f9cbc5.png)

Cloud Runにデプロイする
----------------

Quarkusによる爆速JavaEEアプリが出来たので、次はServerless環境である[Cloud Run](https://cloud.google.com/run/docs/)にデプロイしてみましょう。
冒頭でも書きましたがCloud Runは[Knative](https://cloud.google.com/knative/)をベースにしたGCPによるCaaS(Containers as a Service)環境です。
自前のGKE環境にデプロイする方法と手軽なGCPフルマネージド環境がありますが、今回は後者を使います。

### Cloud SQLの作成

今回はPostgreSQLを利用してるのでGCPにもRDBが必要です。というわけでCloud SQLを使ってマネージドなDBを作成します。

```bash
$ gcloud sql instances create myinstance --region us-central1 --cpu=2 --memory=7680MiB --database-version=POSTGRES_9_6
$ gcloud sql users set-password postgres --instance=myinstance --prompt-for-password
```
これで`myinstance`という名前のでDBを作成することができました。`gcloud sql instances list`で動作を確認することができます。

```bash
$ gcloud sql instances list
NAME        DATABASE_VERSION  LOCATION       TIER              PRIMARY_ADDRESS  PRIVATE_ADDRESS  STATUS
myinstance  POSTGRES_9_6      us-central1-b  db-custom-2-7680  xxx.xxx.xxx.xxx   -                RUNNABLE
```


### Cloud Run向けのDockerイメージの作成

続いてCloud Run向けのDockerイメージの作成をします。
本来的に`Cloud Run`として特殊な設定はいりません。Quarkusプロジェクトに含まれる`src/main/docker/Dockerfile.native|jvm`のいずれでビルドしたイメージもそのまま動きます。
ただし、GCPマネージドのCloud RunはVPCの中に入れる事ができません(Cloud Run on GKEは出来ます)。そのため、Cloud RunからCloud SQLへの接続はACLで直接接続するのでは無く`Cloud SQL Proxy`が基本となります。

#### エンドポイントスクリプトを作成

以下のようなProxyとQuarkusアプリの両方を動かす`src/main/script/run.sh`スクリプトを作成します。

```bash
#!/bin/sh

# Start the proxy
/usr/local/bin/cloud_sql_proxy -instances=$CLOUDSQL_INSTANCE=tcp:5432 -credential_file=$CLOUDSQL_CREDENTIALS &

# wait for the proxy to spin up
sleep 10

# Start the server
./application -Dquarkus.http.host=0.0.0.0 -Dquarkus.http.port=$PORT
```

Proxyの起動には多少の時間が必要なので10秒ほどSleepを入れています。当たり前ですがスピンアップに最低10秒かかるのでここはなんとかしたいところ...

#### Dockerfileの作成とビルド

続いてDockerfileの作成をします。基本はDockerfile.nativeと同じですが`run.sh`や`cloud_sql_proxy`を含んだ`src/main/docker/Dockerfile.gcp`を作成します。

```dockerfile
FROM registry.fedoraproject.org/fedora-minimal
WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work

ADD https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 /usr/local/bin/cloud_sql_proxy
RUN chmod +x /usr/local/bin/cloud_sql_proxy
COPY src/main/script/run.sh /work/run.sh
RUN chmod +x /work/run.sh

EXPOSE $PORT
CMD ["./run.sh"]
```

続いて`.dockerignore`に`Dockerfile.gcp`を追加します。Quarkusプロジェクトでは特定のファイル以外はビルド時に見えなくしてあるので、改修しないと`docker build`時に対象ファイルがないとしてエラーになります。
改修差分は以下の通り。

```bash
$ diff .dockerignore.bak .dockerignore
4a5
> !src/main/script/run.s
```

いよいよ準備ができたのでビルドを行います。
フルマネージド版の`Cloud Run`ではデプロイ対象はCloud Registryに配置してある必要があるようなので、タグを`gcr.io/プロジェクト名/イメージ名`にしておきます。

```bash
$ export PRJ_NAME="ここにプロジェクト名"
$ ./mvnw clean package -Pnative -DskipTests=true -Dnative-image.container-runtime=docker
$ docker build -f src/main/docker/Dockerfile.gcp -t gcr.io/${PRJ_NAME}/mybank .
```

GraalVMのnative-imageのビルドは依存関係を確認してネイティブイメージに変換するためとても重いです。
ザクっと5分から10分かかるので懐かしのコーヒータイムを楽しんでください。

#### ローカルでの動作確認

ローカルでの動作確認には「IAMと管理」よりSQLクライアントにアクセスできるアカウントを作成しJSON形式の鍵を作成します。
こちらを`credentials.json`という名前で適当なローカルの場所に配置してボリュームでコンテナの見えるところに置いた上でファイルのパスを環境変数`CLOUDSQL_CREDENTIALS`に指定してローカルでの動作確認が出来ます。

```bash
$ export SQL_CONNECTION_NAME=$(gcloud sql instances describe myinstance|grep connectionName|cut -d" " -f2)
$ docker run -it -p 8080:8080 -v `pwd`:/key/ \
-e CLOUDSQL_INSTANCE=${SQL_CONNECTION_NAME} \
-e CLOUDSQL_CREDENTIALS=/key/credentials.json \
-e QUARKUS_DATASOURCE_URL=jdbc:postgresql://localhost:5432/postgres \
-e QUARKUS_DATASOURCE_PASSWORD="ここにパスワード" \
gcr.io/${PRJ_NAME}/mybank
```

#### Cloud RunのサービスアカウントにSQLクライアント権限を追加

ローカルでの動作確認が出来たので`Cloud Run`向けの設定をします。
鍵ファイルをコンテナに入れれば上記のままでも動きますがそれはセキュリティ的に微妙なので、Cloud Runのサービスアカウント`Google Cloud Run Service Agent(xxxx@serverless-robot-prod.iam.gserviceaccount.com)`にSQLクライアント権限を追加します。

「IAMと管理」 -> 「IAM」で上記のCloud Run Service Agentを指定して`Cloud SQLクライアント`の役割を追加すればOKです。
これでCloud Runからは鍵ファイルなしで接続が可能になります。

#### Cloud Runへのデプロイ

それではいよいよCloud Runにデプロイを行います。
まずは先ほど作成したイメージをCloud RegistryにPushします。

```bash
docker push gcr.io/${PRJ_NAME}/mybank
```

続いてデプロイ。

```bash
$ export SQL_CONNECTION_NAME=$(gcloud sql instances describe myinstance|grep connectionName|cut -d" " -f2)
$ gcloud beta run deploy mybank \ 
--image gcr.io/${PRJ_NAME}/mybank \
--set-env-vars \
QUARKUS_DATASOURCE_URL=jdbc:postgresql://localhost:5432/postgres,\
QUARKUS_DATASOURCE_PASSWORD={DBパスワード},\
CLOUDSQL_INSTANCE=$SQL_CONNECTION_NAME
```

`set-env-vars`で環境変数を指定できます。必要に応じてモジュールに手を加える事なくログレベルやDB設定の変更ができるのが便利ですね。

```bash
$ gcloud beta run services list
   SERVICE         REGION       LATEST REVISION     SERVING REVISION    LAST DEPLOYED BY    LAST DEPLOYED AT
✔  mybank          us-central1  mybank-00017        mybank-00017        xxxx@gmail.com      2019-04-25T08:50:28.872Z
```

デプロイできたのが確認できるかと思います。接続先のURLを確認してみましょう。

```bash
$ gcloud beta run services describe mybank|grep hostname
hostname: https://mybank-xxx-uc.a.run.app
```

curlで動作確認をしてみます。

```bash
[$ curl -X POST -H "Content-Type: application/json" https://mybank-xxx-uc.a.run.app/account
$ curl https://mybank-xxx-uc.a.run.app/account
[{"amount":0,"id":"b9efbb84-3b4d-4152-be6b-2cc68bfcbe71"}]
```

初回のスピンアップは10秒程度かかりますが、それ以降は数百ms前後で動作してる事がわかります。DBアクセスもバッチリですね！

まとめ
-------

QuarkusとCloud Runを使ってServerlessなJavaEE環境を作ってみました。
GAEはロックインするしJavaEEだとスピンアップがなー、という所にちょうど良い感じのプロダクトが揃ってきましたが如何でしょうか？

普通にJAX-RS/CDIやJPAといったJavaEEお馴染みの書き方をしつつ、msオーダーで起動するのは面白いですよね？　
この特徴は「リクエスト毎にプロセスを立ち上げる」というServerlessのアーキテクチャにもとても合っています。
Fast CGIに舞い戻ったのが不思議な感じがしますが長時間稼働も原則しないのでJavaで頭を悩ませるGCも大きな影響がないのは面白い特徴だと思います。

どちらも出たばかりで不安定ですし、ブレイクスルーなので運用面をどうしていくかは考える必要がありますが今後も追っていきたいと思います。
まずはDB周りをなんとかしないとせっかくmsオーダーで動くのにDB接続のためにスピンアップが重い...

それではHappy Hacking!

参考
-------

- [Quarkus Official](https://quarkus.io/)
- [Cloud Run Official](https://cloud.google.com/run/)
- [ブログなんだよもん:Serverless時代のJavaEEコンテナ - Quarkus](http://koduki.hatenablog.com/entry/2019/04/15/063038)
- [Quarkus: コンテナ上で Java アプリを高速起動する新しい手法のご紹介](https://yoshio3.com/2019/03/11/try-quarkus/)
- [QUARKUS AND THINJARS--AIRHACKS.FM PODCAST](http://adambien.blog/roller/abien/entry/quarkus_airhacks_fm_podcast)
- [Knative - Google Cloud Platform Podcast](https://www.gcppodcast.com/categories/knative/)
- [Connecting to Cloud SQL from Cloud Run](https://medium.com/@petomalina/connecting-to-cloud-sql-from-cloud-run-dcff2e20152a)