LoginSignup
5

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-21

はじめに

Microsoft Build2020を見てDistributed Application Runtime のDaprが中々面白そうでした。
チュートリアルがNode.jsだったので参考にしつつQuarkusからDaprを利用するサンプルを作ってみました。

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

Daprって?

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

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

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

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

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

$ 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でアクセスしてみます。

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

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

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

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

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

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

$ 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にアクセスしてみましょう。

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

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

Redisを使ったState管理を行う

アプリケーションの仕様

ではHello WorldのStep1からStep5までを実施ます。
今回実装るすアプリは以下のような構成になっています。

image.png

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

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

{
  "data": {
    "orderId": "42"
  } 
}

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

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

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

アプリケーションの実装

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

$ ./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になります。

[{
  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を起動します。

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

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

$ 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コマンドで以下のように書くこともできます。

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

まとめ

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

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

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

それではHappy Hacking!

参考リンク

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5