谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

Dapr Advent Calendar 3日目 - Daprでサービスを呼ぶ

こんにちは、Dapr Advent Calendar 3日目です。ついに3日坊主達成です!

別のサービスを呼んでみよう

前回のエントリー ではDaprを使ってHello Worldアプリケーションを呼び出しましたが、今回はひとつのWebアプリケーションから、Dapr経由で別のWebアプリケーションを呼び出してみます。

f:id:cero-t:20211203033040p:plain
今回作るアプリケーション

ソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021

このソースコードの例ではMavenのマルチモジュール構成で、次のように作ってあります。

  • dapr-advent-2021
    • mvnw
    • pom.xml
    • hello (前回説明したもの)
    • invoke (今回新たに作るもの)

Daprを使わないWebアプリケーションの作成

まずはDaprを使わずに、呼び出される側と呼び出す側のWebアプリケーションをそれぞれ作成します。

呼び出される側のWebアプリケーションは、前回のHello World(helloモジュール)をそのまま使います。

(hello) HelloController.java

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Map<String, String> hello() {
        return Map.of("message", "Hello, world!");
    }
}

そして新たに別のWebアプリケーション(invokeモジュール)を作成します。上で作ったHello Worldのアプリケーションを呼び出すためのアプリケーションです。

(invoke) InvokeController.java

@RestController
public class InvokeController {
    private RestTemplate restTemplate;

    public InvokeController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/invokeHello")
    public Map<String, ?> invokeHello() {
        Map<?, ?> result = restTemplate.getForObject("http://localhost:8080/hello", Map.class);
        return Map.of("remoteMessage", result);
    }
}

helloアプリケーションの /hello を呼び出すだけの簡単な処理です。

RestTemplateを使うため、ApplicationクラスでBean定義をしておきます。

(invoke) InvokeApplication.java

@SpringBootApplication
public class InvokeApplication {
    public static void main(String[] args) {
        SpringApplication.run(InvokeApplication.class, args);
    }

    @Bean
    RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
}

Spring Bootの使い方みたいなところの説明は割愛しますね。

また、このアプリケーションをポート番号8081で起動するよう、設定ファイルでポート番号を指定します。

(invoke) application.properties

server.port=8081

これでアプリケーションは完成です。

まずはそれぞれのWebアプリケーションを別々のコンソールで起動します。

cd hello
../mvnw spring-boot:run
cd invoke
../mvnw spring-boot:run

アプリケーションが起動したらcurlコマンドでアクセスします。

curl localhost:8081/invokeHello

無事に実行結果が表示されました。

{"remoteMessage":{"message":"Hello, world!"}}

結果を見やすいようjqコマンドなどで整形するとこんな感じです(以降も基本的にjqで整形した結果を表示します)

{
  "remoteMessage": {
    "message": "Hello, world!"
  }
}

これで別サービスの呼び出しができました。

f:id:cero-t:20211203034815p:plain
ここまでで作ったアプリケーション

ただ、ソースコード内に相手側のアドレスやポート番号を書かなくてはならないという問題がありますね。これをDaprで解決しましょう。

URLを設定ファイルに移動させる

Daprを使ってアプリケーション起動する前に、ソースコードのURLの一部を設定ファイルに移動させます。

(invoke) InvokeController.java(抜粋)

@Value("${baseUrl}")
private String baseUrl;

@GetMapping("/invokeHello")
public Map<String, ?> invokeHello() {
    Map<?, ?> result = restTemplate.getForObject(baseUrl + "/hello", Map.class);
    return Map.of("baseUrl", baseUrl, "remoteMessage", result);
}

URL部分をbaseUrlの値で動的に変えられるようにしました。また、利用したbaseUrlの値を確認するために、レスポンスにbaseUrlを入れるようにしました。

baseUrlは application.properties で定義します。

(invoke) application.properties

server.port=8081
baseUrl=http://localhost:8080

これでURLの一部を設定ファイルに移動させることができました。このURLをDapr向けに書き換えた設定ファイルも別に作っておきます。

(invoke) application-dapr.properties

baseUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method

DAPR_HTTP_PORT は、Daprのポート番号を示しています。Dapr経由でアプリケーションを起動すると、この環境変数にDapr自身のポート番号が入ります。アプリケーションはこのポートを経由して、自身のサイドカーであるDaprプロセスのAPIを利用できるようになるのです。このポート番号を使い、前回説明したInvoke APIを使ってアプリケーションにアクセスするように記述しました。

念のため、この時点でアプリケーションの動作確認をしておきましょう。invokeアプリケーションをいったん停止してから再起動します。

../mvnw spring-boot:run

curlコマンドでアクセスします。

curl localhost:8081/invokeHello 

実行結果にbaseUrlが入るようになりました。

{
  "baseUrl": "http://localhost:8080",
  "remoteMessage": {
    "message": "Hello, world!"
  }
}

これで準備が整いました。次はDaprを使ってアプリケーションを起動してみましょう。

Daprとともにアプリケーションを起動

まずはDaprを使ってhelloアプリケーションを起動します。

helloアプリケーションを停止させた後、次のコマンドで起動します。

dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080 ../mvnw spring-boot:run

続いて、invokeアプリケーションも同じように停止させたあと、次のコマンドで起動します。

dapr run --app-id invoke-app --app-port 8081 --dapr-http-port 18081 -- ../mvnw spring-boot:run -Dspring-boot.run.profiles=dapr

mvnwコマンドに -Dspring-boot.run.profiles=dapr という引数を追加して、application-dapr.properties の設定が有効になるようにしました。また引数を追加するためmvnwコマンドの前に -- を追加しました。

それではDapr経由でWebアプリケーションにアクセスしてみましょう。curlコマンドでアクセスします。

curl localhost:18081/v1.0/invoke/invoke-app/method/invokeHello

次のような結果が表示されるはずです。

{
  "remoteMessage": {
    "message": "Hello, world!"
  },
  "baseUrl": "http://localhost:18081/v1.0/invoke/hello-app/method"
}

ふつうにアクセスした時と同じメッセージが取得でき、baseUrlはDaprのAPI形式になりました。

なおinvokeアプリケーションは、別にDaprを経由せずに呼び出しても構いません。

curl localhost:8081/invokeHello

上と同じ実行結果が得られます。

{
  "remoteMessage": {
    "message": "Hello, world!"
  },
  "baseUrl": "http://localhost:18081/v1.0/invoke/hello-app/method"
}

これで、Dapr経由で2つのWebアプリケーションが連携できました。

あくまでも自身のサイドカーであるDaprと通信するだけで、別のDapr経由で起動しているアプリケーションに対してアクセスができた、ということです。

f:id:cero-t:20211203035529p:plain
Daprを経由したアクセス

やったことの解説

今回は2つのアプリケーションを起動して、Dapr経由でアクセスすることができました。Daprがちょうどサービスディスカバリーやルーティングのような機能を有していると言えます。Spring CloudでいうEurekaやDiscovery Clientに相当する機能です。

この機能に関するドキュメントはこちらになります。

docs.dapr.io

f:id:cero-t:20211203034219p:plain

Dapr同士が協調して、Invoke APIに含まれるapp-id(今回は hello-app)を名前解決して相手のアプリケーションを発見し、その相手のアプリケーションのサイドカーを経由してアプリケーションにアクセスする形となっています。

この名前解決には、ローカル環境ではマルチキャストDNSが利用され、k8s上ではk8sの名前解決が利用されるほか、Consulを利用することもできます。

この名前解決の機能を利用すれば、たとえばこのようなアクセスも可能です。

curl http://localhost:18080/v1.0/invoke/hello-app/method/hello
curl http://localhost:18081/v1.0/invoke/hello-app/method/hello

Daprのポート番号 1808018081 で異なっていますが、どちらのURLでアクセスしても、同じ結果が返ってきます。

{
  "message": "Hello, world!"
}

どのDaprにアクセスしても hello-app という名前からアクセスすべきアプリケーションを探してアクセスを行うのです。Daprを使う時には「どのDaprにアクセスするか」はあまり関係なく、Daprネットワークにアクセスしている、という感覚が近いと言えるでしょう。

環境への依存を下げる

今回説明したサービス呼び出しに関連して、もう少し環境周りのパラメータを減らす方法を説明しておきます。

Daprのポート番号を指定せずに起動する

Daprの理解を深めるために、Daprのポート番号を指定せずにアプリケーションを起動するようにします。

helloアプリケーションを一度停止し、次のコマンドで起動します。--dapr-http-port を指定しない形です。

dapr run --app-id hello-app --app-port 8080 ../mvnw spring-boot:run

invokeアプリケーションも一度停止して、同じように --dapr-http-port を指定せずに起動します。

dapr run --app-id remote-call-app --app-port 8081 -- ../mvnw spring-boot:run -Dspring-boot.run.profiles=dapr

そしてcurlコマンドで直接invokeアプリケーションにアクセスします。

curl localhost:8081/invoke

これでも正常に実行結果が得られました。

{
  "remoteMessage": {
    "message": "Hello, world!"
  },
  "baseUrl": "http://localhost:65275/v1.0/invoke/hello-app/method"
}

DaprのHTTPポート番号がランダムに決まったたとしても、環境変数 DAPR_HTTP_PORT を使っているため自分のサイドカーとして起動しているDaprにアクセスできます。空いているポート番号を適当に使っても構わなくなるのは楽で良いですよね。

Daprの設定ファイルを使わずに起動する

ここまでの例では application-dapr.properties を使っていましたが、これはアプリケーション側がDaprに依存している形にも見えます。このファイルを使わずに、コマンド引数で接続先を指定する形も試してみましょう。

次のコマンドで起動します。

dapr run --app-id remote-call-app --app-port 8081 -- ../mvnw spring-boot:run -Dspring-boot.run.arguments='--baseUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method'

baseUrl をコマンド引数として渡すようにしました。このコマンド実行時点で ${DAPR_HTTP_PORT} が解釈されてしまわないよう、引数はシングルクォートで囲っています。

これで起動したアプリケーションにcurlコマンドでアクセスすると

curl localhost:8081/invoke

正常に実行結果が得られました。

{
  "remoteMessage": {
    "message": "Hello, world!"
  },
  "baseUrl": "http://localhost:51939/v1.0/invoke/hello-app/method"
}

このようにすればアプリケーション側のソースコードや設定ファイルからDaprに対する依存を避けることができます。Spring Bootが環境変数に応じて処理を切り替えやすい構造になっているおかげとも言えます。

もちろんコマンドライン引数でわざわざこんな長いURLを指定するのは面倒ですが、たとえばk8sのマニュフェストファイルで環境変数に指定する、というような状況であればこの形で指定することも妥当なやり方の一つになると思います。

まとめ

それでは今回の内容を簡単に振り返りましょう。

  • Dapr経由でアプリケーションを複数起動すると、お互いに相手の名前を指定して通信することができます
  • ローカル環境ではマルチキャストを使ってDapr同士が連携し、他のDaprを探しに行きます
  • アプリケーションは DAPR_HTTP_PORT という環境変数で自分のサイドカーであるDaprのポート番号を認識します
  • 環境変数で処理を切り替えやすい構造にしていれば、より環境への依存を下げることができます

正直、僕はこの機能を見ただけでもDaprを使ってみようという気持ちになりました。他のサービスディスカバリーのサーバなしで名前解決できるのって、何気に嬉しいですよね。

それでは!