はじめに
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までを実施ます。
今回実装るすアプリは以下のような構成になっています。
ユーザが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"
続いてアプリケーションを以下のように修正します。
@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
にどのストアと繋ぐかを記載するようです。
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!