マイクロサービス開発を容易にするDaprをJavaで利用してみた

## はじめに

[Microsoft Build2020](https://koduki.hatenablog.com/entry/2020/05/21/154223)を見てDistributed Application Runtime のDaprが中々面白そうでした。
[チュートリアル](https://github.com/dapr/samples/tree/master/1.hello-world)がNode.jsだったので参考にしつつQuarkusからDaprを利用するサンプルを作ってみました。

コードは下記を参照
https://github.com/koduki/example-dapr/tree/v01/api-with-java

## Daprって？

Daprはサイドカー(Proxy)によりサービス間呼び出し、ステート管理、サービス間メッセージングなどの非機能要件を実現する事でマイクロサービスの実装を簡単にするマイクロソフトによって開発されているフレームワークです。
OSSで開発されているため下記より利用できます。

- https://dapr.io/
- https://github.com/dapr/dapr

サイドカーという事でIstioなどのサービスメッシュと同じものかと思っていたのですが、セッションを聴いていると少し違う感じで、ファイルやステート管理（データ永続化）、あるいはKafkaなどのキューイングを抽象化する役割もあるようでした。どちらかというとJavaEEのJNDIとかDataSourceやJCA(Java Connector Architecture)の類な感じがします。~~おじさんなら~~「これ進研ゼミでやったやつだ」っていうチャンスですね！

WeblogicなんかのJavaEEコンテナだとこの辺はそもそも同じメモリ空間にいたりT3で喋ってると思いますが、Daprと各アプリケーションはHTTPないしはgRPCで喋ります。


## とりあえずDaprを動かしてみる

実際のチュートリアルをやる前にとりあえず動かしてみましょう。今回はgRPCは面倒なのでRESTを実装します。とりあえずmvnコマンドでQuarkusのテンプレートを作って実行します。

```bash
$ mvn io.quarkus:quarkus-maven-plugin:1.4.2.Final:create \
    -DprojectGroupId=dev.nklab \
    -DprojectArtifactId=dapr-app \
    -DprojectVersion=1.0.0-SNAPSHOT \
    -DclassName="dev.nklab.example.dapr.HelloResource"
$ cd dapr-app
$ ./mvnw quarkus:dev 
```

別ターミナルからcurlでアクセスしてみます。

```bash
$  curl http://localhost:8080/hello                           
hello
```

アプリケーションの動作が確認できたところでDaprをインストールします。k8sは特になくても動作しますがDockerは事前に入れて置く必要があるようです。

```bash
$ curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | /bin/bash
$ dapr init
```

インストールはこれで完了です。以下のエラーが出たらたぶん`dapr init`を忘れています。

```bash
exec: "daprd": executable file not found in $PATH
```

続いて、先ほど作ったQuarkusアプリケーションを以下のコマンドでDaprでラッピングして実行します。

```bash
$ dapr run --app-id javaapp --app-port 8080 --port 3500 ./mvnw quarkus:dev 
...
ℹ️  Updating metadata for app command: ./mvnw quarkus:dev
✅  You're up and running! Both Dapr and your app logs will appear here.
```

`--app-port`はQuarkusのポート番号、`--port`はDaprのポート番号です。ではcurlでDaprにアクセスしてみましょう。

```bash
$ curl http://localhost:3500/v1.0/invoke/javaapp/method/hello                                
hello
```

Daprがサイドカー、すなわちProxyとして動作してるので`8080`ではなく`3500`で裏側のアプリにアクセスできたことがわかります。
`javaapp`の部分は先ほど実行時に指定した`aap-id`です。`invoke`で裏側のアプリに連携する処理になるようです。

## Redisを使ったState管理を行う

### アプリケーションの仕様

では[Hello World](https://github.com/dapr/samples/tree/master/1.hello-world)のStep1からStep5までを実施ます。
今回実装るすアプリは以下のような構成になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/54904/1a7eb3cb-bd1b-f945-3d92-74a954e38885.png)

ユーザがDapr経由でApplication(Java)にリクエストを送り、アプリケーションからDaprのAPIを叩きDaprを経由してRedisに書き込みます。アプリケーションはあくまでDaprとしか会話しないので、データの永続化にRedisを直接経由しないのは面白いですね。

仕様としてはユーザは以下のようなPOSTリクエストをApplicationに投げます。

```json
{
  "data": {
    "orderId": "42"
  } 
}
```

このデータはRedisに以下の形式で保存されます。

```json
[{
  key: "order",
  value: ここにorderIdを格納
}]
```

また、ApplicationにGETリクエストを投げると現在のorderIdを返します。

### アプリケーションの実装

では、アプリケーションを実装していきます。今回はJSONを使うのでQuarkusにライブラリを追加しておきます。

```bash
$ ./mvnw quarkus:add-extension -Dextensions="quarkus-resteasy-jsonb"
$ ./mvnw quarkus:add-extension -Dextensions="quarkus-resteasy-jackson"
```

続いてアプリケーションを以下のように修正します。

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

    @ConfigProperty(name = "daprapp.daprport")
    String daprPort;

    String stateStoreName = "statestore";

    @GET
    @Path("/order")
    public Map<String, Object> order() throws IOException, InterruptedException {
        return Map.of("orderId", get(stateUrl() + "/order").body());
    }

    @POST
    @Path("/neworder")
    public HttpResponse neworder(Map<String, Map<String, Object>> data) throws IOException, InterruptedException {
        System.out.println("orderId: " + data.get("data").get("orderId"));

        var items = List.of(Map.of("key", "order", "value", data.get("data").get("orderId")));
        return post(stateUrl(), items);
    }

    private String stateUrl() {
        return "http://localhost:" + daprPort + "/v1.0/state/" + stateStoreName;
    }

    private HttpResponse<String> post(String url, List<Map<String, Object>> items) throws IOException, InterruptedException, JsonProcessingException {
        var mapper = new ObjectMapper();
        var client = HttpClient.newHttpClient();
        var request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .POST(HttpRequest.BodyPublishers.ofString(mapper.writeValueAsString(items)))
                .setHeader("Content-Type", "application/json")
                .build();
        return client.send(request, HttpResponse.BodyHandlers.ofString());
    }

    private HttpResponse<String> get(String url) throws InterruptedException, IOException {
        var client = HttpClient.newHttpClient();
        var request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .GET()
                .setHeader("Content-Type", "application/json")
                .build();
        return client.send(request, HttpResponse.BodyHandlers.ofString());
    }
}
```

JAX-RSとして特別なことはしてないので詳細は省きますが、`neworder`が登録用のエンドポイント、`order`が参照用のエンドポイントです。

それぞれのメソッドの中で`http://localhost:3500/v1.0/state/statestore`にアクセスしています。これはDaprのステート管理APIのエンドポイントです。
今回はこのステート管理APIの実体がRedisになります。ステート管理APIのリクエストとレスポンスは前述したとおり以下のようなJSONになります。

```json
[{
  key: 値,
  value: 値
}]
```

### ステート管理の実装にRedisを設定する

続いてステート管理の実装にRedisを設定します。と言っても既に設定済みなので確認だけします。
実は先ほど`dapr run`をしたタイミングで`components`というディレクトリができています。こちらの`statestore.yaml`にどのストアと繋ぐかを記載するようです。

```components/statestore.yaml                                                           
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"
```

おそらくここをRDBなどに書き換えれば別の実装になるんではなかろうかと思われます。

### テスト

それでは実装は完了したので動作確認をします。まずはDaprを起動します。

```bash
$ dapr run --app-id javaapp --app-port 8080 --port 3500 ./mvnw quarkus:dev
```

続いてリクエストを投げます。

```bash
$ curl -X POST -H "Content-Type: application/json" -d '{"data": { "orderId": "41" } }' http://localhost:3500/v1.0/invoke/javaapp/method/neworder
{}
$ curl http://localhost:3500/v1.0/invoke/javaapp/method/order
{"orderId":"\"41\""}
```

投げたリクエストした値が格納されそれが取得できたのが確認できたかと思います。なお、POSTはDaprコマンドで以下のように書くこともできます。

```bash
$ dapr invoke --app-id javaapp --method neworder --payload '{"data": { "orderId": "41" } }'
```

## まとめ

とりあえずDaprをJavaから使ってみました。RESTなので特に問題なく実装できました。
DaprはIstio以上にJavaEE感がやはりあって、Daprそのものはともかくこの考え方自体は開発の容易性の観点で正しい方向の気がします。
DAOパターンを含めてサービスやデータの実体は隠蔽するのは基本的な考え方ですし、非機能はインフラ側に可能な限り溶けこました方がいいので。

一方で永続化層もProxyを経由するとなれば例えgRPCを使っても一定のオーバーヘッドは避けられないと思われます。この辺りを設計でどう対応していくかが今後求められていくのだと思います。

Dapr自体はまだ出来たばかりで荒削りなところも多そうですが、今後もう少し触っていきたいと思います。

それではHappy Hacking！

## 参考リンク

- [Distributed Application Runtime - Dapr](https://dapr.io/)
- [マイクロソフト、マイクロサービス開発を容易にする「Dapr」をオープンソースで公開。サービス間呼び出し、ステート管理、サービス間メッセージングなど提供](https://www.publickey1.jp/blog/19/dapr.html)
- [Dapr + JavaでHello worldしました。](https://cero-t.hatenadiary.jp/entry/2019/10/30/135155)