谷本 心 in せろ部屋

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

Dapr Advent Calendar 21日目 - Daprの開発環境

こんにちは Dapr Advent Calendar 21日目です。今回からは実践編として、実際に開発する上で役立つノウハウやTipsを紹介したいと思います。

Daprの開発環境

ここまでDaprの機能を説明してきて、なんだかよさげには見えるけど、実際に効率良く開発できるのかについてはまだ疑問があるかも知れません。今回は開発の効率に直結する「開発環境」や「デバッグ」について話したいと思います。

DaprのIDE Support

DaprのIDE対応について、公式ドキュメントにはVisual Studio CodeIntelliJについて記載されています。

docs.dapr.io

Visual Studio CodeIntelliJのどっちを使うと良いですかって聞かれれば、そりゃJava開発なんだから今はIntelliJでしょう、としか答えられないのですが、Visual Studio Code対応がどのようになっているのかを見ておきます。

Visual Studio Code + Dapr extensionの所感

Visual Studio Codeの拡張としてDapr extensionが提供されている。このextensionでできることは次の通りです。

  • componentファイルのscaffold作成(pubsub、statestore、zipkinの設定ファイルの作成)
  • dapr起動コマンドのscaffold作成
  • 起動中のDaprアプリケーション一覧の表示
  • 起動中のDaprアプリケーションに対するinvokeとpublish

設定ファイルのscaffoldは ~/.dapr/components と同じファイルを生成するだけなので大したことはありません。デモ用ですかね。

dapr起動コマンドのscaffold作成は、Visual Studio Codeのlaunch.jsondapr run でアプリケーションを起動するconfigurationを追加するというものです。dapr run コマンドを手打ちしなくて済むようになります。

Daprアプリケーション一覧は、次のように表示されます。

f:id:cero-t:20211221005228p:plain:w300
Dapr extensionで見るアプリケーション一覧

起動中のアプリケーション一覧が表示されます。dapr list コマンドで取得できるものと同様ですね。ちなみにk8s上にデプロイしたDaprアプリケーションは見えませんでした。

これらのアプリケーションを選択し、invokeやpublishを実行することができます。invokeする時にわざわざcurlコマンドを叩いたり、ちょっと長めのURLを打ったりしなくて済みます。

Dapr extensionが提供する機能はこのくらいなので、そんなに高機能というわけでも生産性がすごく上がるというわけでもないですが、Daprアプリケーションの簡易UIとして使うのも悪くないという印象でした。

IntelliJによるデバッグ

続いて、IntelliJによるDaprアプリケーションのデバッグ方法についても説明しておきます。

対象のソースコードGitHubにあるHello Worldアプリケーションを利用します。

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

ここにある「hello」モジュールを利用します。

Daprの単独起動

まずはDaprを単独で起動します。公式ドキュメントではIntelliJのExternal Toolsを利用していましたが、わざわざ使う必要もないので次のコマンドでDaprを起動します。

dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080

これでDaprが単独で起動します。まだアプリケーションは起動していません。

IntelliJ側でアプリケーションを起動

続いて、IntelliJhello モジュールの HelloApplicationデバッグ起動します。HelloApplication を右クリックし、Debug 'HelloApplition' を選択するなどで起動できます。

f:id:cero-t:20211221010340p:plain
IntelliJデバッグ起動

アプリケーションを起動したら HelloControllerreturn Map.of("message", "Hello, world!");ブレークポイントを張っておいてください。

アプリケーションへのアクセス

続いて、次のコマンドでアプリケーションにアクセスします。

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

このコマンドを実行すると、IntelliJ側で指定したブレークポイントで止まっているはずです。

つまり、Daprを単体起動し、IntelliJからDaprで指定した app-port のアプリケーションを起動すればDapr経由でアプリケーションにアクセスできる状態になるのです。この方法で起動すれば、IntelliJ側でデバッグができるということです。仕組みが分かってしまえば簡単な話ですね!

いつどのツールを使って開発するのか

これまで半年くらい業務でDaprを使って開発し、実運用も始めているのですが、正味の話こんな風にデバッグをできることは、いま初めて知りました。

なぜこれまで知らなかったというと、ローカル環境で開発する時には(担当範囲にもよるのですが)Daprを使うことはあまりないためです。その辺りについて説明したいと思います。

Daprアプリケーションの開発の実際

例として、運用するシステムが Amazon EKS + Dapr + RabbitMQ + PostgreSQL + Spring Boot という構成だったとします。

その際、開発フェーズごとに使うツールは次のようになります。

  • 開発時
  • Dapr経由での動作検証時
    • Java + Docker + Dapr
  • k8sでの動作検証時
    • Java + Docker + Dapr + Minikube + kubectl
  • Amazon EKSでの動作検証時
    • Java + Docker + kubectl

この辺りを順番に説明していきます。

開発時 (Java + Docker)

アプリケーションを開発する際には、Spring Bootで開発するためにJavaJDK)を利用し、PostgreSQLを利用するためにDockerを利用します。ここでDaprは使いません。

これまでのAdvent Calendarでも少し説明していましたが、ローカル環境で開発する際にはアプリケーション同士を直接呼び出しています。

Invoke APIを使わず直接呼び出し

たとえばInvoke APIを使ったアプリケーションのコードは次のようになります。

@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);
}

ここで baseUrl としてローカル環境で開発する際には http://localhost:8080 を指定します。そうすれば、ポート8080番で起動している別のアプリケーションにアクセスできますし、あくまでもSpring Bootだけの世界に閉じて開発ができます。

そしてDaprを利用する際には http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method というInvoke APIのURLに差し替えるのです。

このようにすればローカルではDaprなし、検証や運用はDaprあり、と切り替えることができます。

Pub/sub APIを使わず直接呼び出し

HTTPで呼び出す同期処理はそれで良いとして、非同期処理はどうなるでしょうか。

たとえばPub/sub APIを使ったアプリケーションのコードは次のようになります。

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

@PostMapping("/publish")
public void publish(@RequestBody MyMessage message) {
    restTemplate.postForObject(pubsubUrl, message, Void.class);
}

そしてメッセージを受け取るsubscribe側は次のように実装します。

@PostMapping("/subscribe")
public void subscribe(@RequestBody CloudEvent<MyMessage> cloudEventMessage) {
    doSubscribe(cloudEventMessage.data);
}

@PostMapping("/doSubscribe")
public void doSubscribe(@RequestBody MyMessage message) {
    System.out.println(message);
}

上がDaprから呼ばれる際にCloudEventのエンベロープがついたクラスを引数にしたメソッドです、下がその中のメッセージを使って実処理を行うメソッドです。

このような構成にして、ローカルの開発時には pubsubUrllocalhost:8084/doSubscribe などを指定して直接同期呼び出しをするようにします。そうすればDaprのことを気にせず、Spring Bootの世界に閉じてビジネスロジックに注力して開発ができます。しかも呼び出しが同期となるため、JUnitなどの自動テストの際に非同期処理が終わることを1秒待つ、みたいなことをしなくても、処理が終わればすぐにassertに進むことができます。

そしてDapr利用時には pubsubUrlhttp://localhost:${DAPR_HTTP_PORT}/v1.0/publish/pubsub/my-message などのPub/sub APIのURLにするのです。このようにすればローカルではDaprなし、検証や運用はDaprあり、と切り替えることができます。

そんな切り替えをして、Daprを使ったときに問題が起きることはないか? と疑問に思うかも知れませんが、もちろん何か問題が起きることはあるかも知れません。ただ基本的にはごく少しのコード修正だけで済むものです。

このような方針にするためにも、Dapr Java SDKは使わず、HTTP通信をするためのクライアントだけを利用しています。

Dapr経由での動作検証時 (Java + Docker + Dapr)

Daprのpub/sub機能が正しく動くかどうか試したい場合や、Daprの分散トレーシングで渡される traceparent ヘッダを利用した処理を行いたい場合には、上の構成に加えてDaprをインストールして動作検証を行います。

やや極端ですが、この時点ではDaprのAPIさえ利用すれば利用するミドルウェアは何でも構いません。たとえば、実際にRabbitMQを用いたpub/sub機能を使って開発をしていたメンバにヒアリングした所、開発時にはDaprが勝手にインストールするRedisを使っていたと話していました。開発時にRedisを使い、検証時からRabbitMQを使っていても、設定ファイル以外は変える必要がなく、何の問題もなくアプリケーションは動作していました。

もちろんRabbitMQ固有の機能、たとえばDead Letter Exchange (DLX)などを用いた処理の開発や検証を行うのであればDocker上にRabbitMQをデプロイして検証する必要がありますが、あくまでもpub/subの主機能の検証を行いたいだけであればRedisを使っても構わないのです。

別にそのような方針を推奨するわけでも何でもないですが、Daprがミドルウェアを抽象化するメリットがこういう所で活きてきます。

k8sでの動作検証時 (Java + Docker + Dapr + Minikube + kubectl)

アプリケーションの開発が終わり、Amazon EKSなどにデプロイする前には、k8sでの検証が必要となります。その場合にはMinikubeが必要となります。インフラ構築を担当する2〜3名ほどだけがローカルにMinikubeをインストールして検証をしていました。もちろんkubectlなどの関連ツールも使います。

逆に言えば、インフラを触らないアプリケーション開発者はMinikubeのインストールすらしていません。k8sに難しいイメージを持っていて、「k8sを使わなければならない」というだけで抵抗があるエンジニアにとっては、k8sを使わずに開発できるという方針にしたほうがハッピーでしょう。

Amazon EKSでの動作検証時 (Java + Docker + kubectl)

Minikube上での検証が終われば、また必要なツールは減ります。イメージを作成してEKSにデプロイするだけですから、イメージのビルドのためのJavaとDocker、またデプロイするためのkubectlが必要となるだけです。DaprやMinikubeは必要ありません。

ただしEKS上で問題が起きた際にログなどを確認できるよう、開発者全員がkubectlを利用できるようにすべきです。ログやメトリクスをCloudWatchやDataDogに集約していれば、開発者がkubectlを利用する機会は減るでしょう。

別にDevとOpsを分離することが正しいのだと主張するつもりはないですが、僕は「それぞれのフェーズにおいて注力すべき部分に注力する」というプロセスを大事にしており、それを実現できるという点で、Daprをとても気に入っています。

まとめ

  • Visual Studio CodeのDapr extensionを使えば、起動中のDaprアプリケーション一覧を表示したりinvoke/publishなどを容易に行えます
  • Daprを単体起動すれば、自分の好きなIDEを使ってアプリケーションをデバッグ起動し、Daprを使った処理のデバッグができます
  • 開発のフェーズごとに、注力すべきことに注力するという構成を取りやすいです
  • 開発時にはDaprもMinikubeもなしでビジネスロジックの開発に注力することができます
  • 検証時以降はそれぞれに必要なツールを少しずつ増やして環境を構築するという方針が良いでしょう

こんな風にしてDaprのアプリケーション開発を進めていました。もちろんこれから変わる所もあるでしょうし、今後も方法論を磨いていきたいと思います。

それでは、また明日!