谷本 心 in せろ部屋

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

Dapr Advent Calendar 25日目 - Daprを使うべきかどうか

こんにちは Dapr Advent Calendar 25日目です。ついにファイナルを迎えました。私たちの挑戦を応援し続けてくれた世界中のファンの皆さん、そして、Dapr Advent Calendarに集まってくださった皆さん、本当にありがとうございます!

Daprを使うべきかどうか

一部のオタクにしか分からない挨拶から始めて申し訳ありません、いよいよDapr Advent Calendarも最終回です。最後は「Daprを使うべきかどうか」について論じたいと思います。

なぜDaprを使うのか

まずはDaprのメリットや、Daprが解決する課題について説明します。

この3つを順に説明します。

1. アプリとインフラを疎結合にする

Daprの主目的であり、このAdvent Calendarでも何度となく触れてきたことですが、Daprを使うことでアプリケーションとインフラ層を分離することができます。そのおかげで開発時にインフラ関連のツールは必要ありませんし、インフラのことを気にせずビジネスロジック(とWeb API)の開発に注力することができます。

その辺りはこの記事に書いていました。

cero-t.hatenadiary.jp

また、Spring Cloudのようにアプリケーションとインフラ層の結合がより密接しているフレームワークでは、新旧バージョンの混在が難しいことがありましたが、アプリケーションとインフラ層が分離されているDaprでは、アプリケーションだけのバージョンアップ、Daprやk8sだけのバージョンアップが行いやすいというメリットがあります。

その辺りについては、この記事にも書いた通りです。

cero-t.hatenadiary.jp

このアプリケーションとインフラ層を分離できるというのが、Daprの1つ目のメリットとなります。

2. 言語やフレームワークを選ばない

2つ目のメリットが、Daprがサイドカーという方式を採っているため、言語やフレームワークを選ばないことです。

僕はJava + Spring Bootで開発していますが、僕が大好きだったSpring CloudはJVM言語でしか開発ができません。Daprであればその縛りはなく、HTTP通信さえできれば好きな言語や好きなフレームワークでの開発ができます。

仮に組織の中で様々な言語やフレームワークを使う人たちがいたとしても、組織の共通技術基盤としてDaprを採用することができ、皆のノウハウを集めて育てることができます。

その気になれば、一つのシステムを別々の言語やフレームワークを混在させて構築することもできるでしょう。マイクロサービスの一要素としてポリグロット(多言語)が挙げられることがありますが、まさにそれを実現するための基盤としてDaprを利用することができます。

そんな好き放題に言語やフレームワークが使われることはあまりないとは思いますが、システムが非常に大きくてサブシステムごとに開発する組織が分かれる場合や、たとえばフロントエンドに近い部分はNode.jsで作り、コアに近い部分はJVM言語で作る、という程度のポリグロットなら現実にもあり得るでしょう。Daprはそうした状況でも共通的に使えるのです。

3. マルチクラウド

そしてメリットの最後が、マルチクラウドです。

たとえばAWS向けに開発していたシステムを、AWSの障害に備えてAzureやGCPでも運用したい時には、それぞれのマネージドサービスやミドルウェアを抽象化するDaprを利用できます。

そんな案件ほとんどないでしょ、せいぜいマルチリージョンまででしょ、と思っていたのですが、意外とそういうリクエストが "なくはない" という感じで、実際にマルチクラウドを目指している案件で、Daprを採用するかべきかどうかを一緒に検討して欲しいと言われています。

もちろんDaprなど使わなくても、各クラウド固有のマネージドサービスではなくPostgreSQLMySQL、Redis、またたKafkaやRabbitMQなどを使うようにすれば十分だという面もあるでしょう。しかしDaprを使えばマネージドサービスのメリットを引き出しながら、共通のAPIで利用できるというメリットがあるというのも確かです。

ネットワーク関連の設定などはDaprの範疇外ですし、マルチクラウドは簡単でもありませんが、せめてアプリケーションとその周辺のインフラ層においてはDaprを使うことで複雑さを排除できる可能性があります。

この辺りは実際に取り組む機会などあれば、また改めてお伝えしたいと思います。

Daprを使う上での課題

もちろん、Daprを使えば順風満帆というわけでも、全幅の信頼が置けるというわけでもありません。Daprのデメリットや課題についても目を向けてみましょう。

  • 導入事例が極めて少ない
  • 機能が少ない
  • メンテナンス期間の短さ

この3つです。ほぼ説明不要な気もしますが、順に説明します。

1. 導入事例が極めて少ない

まず一つ目、Daprは導入事例がほとんどありません。人によっては「cero-tさんがやってるやつ」みたいな印象があると耳にしたことすらあります。それくらい日本でも、おそらく世界でも、導入事例は多くありません。

全く枯れていない技術ですから、大きな問題があったり、情報が少なかったりするかも知れません。あるいは全く流行らなくてメンテナンスされなくなってフェードアウトしてしまうリスクだってあります。

新しいプロダクトを使うというのは、そういうリスクと戦うことになります。

2. 機能が少ない

Daprは世にアナウンスされから約2年、バージョン1.0がリリースされてから約1年くらいの新しいプロダクトですから、そこまで機能が多くありません。このDapr Advent Caledndarだけであらかたの機能を網羅できてしまう程度です。

Spring Cloudなどと比較すると、提供する機能も、対応するコンポーネント(マネージドサービス、ミドルウェア)も少ないですし、いくつかのコンポーネントはまだAlpha版やBeta版だったりします。Alpha版やBeta版の機能でも十分に使えるので僕は使っていますが、いつかAPIの仕様が変わるかも知れないというリスクを受け入れざるを得ません。

機能が少ない部分は自分でカバーしたり、クラウドの機能を直接使ったりする必要があるため、すべてがDaprだけで完結するわけでもありません。足りないところは自分で補う必要があるのです。

3. メンテナンス期間の短さ

DaprはMicrosoft社が中心に開発しているプロダクトですが、まだビジネス的に十分回っているわけではありませんから、サポートなどのエコシステムもそんなに整ってはいません。バージョン1.0からは「Production Ready」な品質として、少し古いバージョンでもパッチが提供されるようになりました。ただ、そのパッチの提供期間も長くはありません。

Daprはおおむね2ヶ月に1度程度、マイナーバージョンアップ(1.4→1.5、1.5→1.6など)が行われます。またバグフィックスや軽微な機能追加のためのパッチ(1.4.1→1.4.2、1.5.0→1.5.1など)が提供されます。このパッチの提供は「現行バージョンと、その一つ前のバージョン」のみが対象となっています。

docs.dapr.io

2021-12-25現在の最新バージョンは 1.5.1 で、その前バージョンである 1.4 はパッチ提供の範囲内なので 1.4.4 までリリースされていますが、バージョン 1.3 以前にはパッチは提供されません。

おおむね2ヶ月に一度バージョンが上がることを踏まえると、特定バージョンのメンテナンス期間は4ヶ月程度しかありません。もちろん最新バージョン(1.x.0)がリリースされてすぐには使わず、パッチが1つ2つリリースされてから使うことも多いでしょうから、実質的には長くとも2ヶ月に一度くらいにはDaprのバージョンを上げる必要があります。

次のドキュメントにあるように1コマンドでバージョンアップできるとは言え、その頻度には抵抗がある組織だってあるかも知れません。

docs.dapr.io

このサポートの短さは課題の一つと言えます。

Daprの課題を覆す

僕は上に書いたようなリスクを受け入れてDaprを使っているのですが、もちろん僕の「Daprを使いましょう」という提案を受け入れてくれたお客様も相当な勇気(あるいは無謀さ)があったように思います。*1

ただ、Daprに問題があった時に心中するつもりで使っているわけでもありません。Daprにどうしても受け入れられない問題があると分かったときには「Daprを捨てて自分で作る」つもりでいます。

DaprはHTTP通信で使うサイドカーですから、同様の機能を自分で作り込み、URLの向き先を自分で作った機能にすれば、Daprを使わずに同様の機能を提供できます。Daprを使っている以上はアプリケーションとインフラ層が疎結合になっているので、別のものに差し替えることだって不可能ではないのです。

もちろん口で言うほど容易なことではないですし、そんな状況が訪れることは願ってないですが、「Daprを使って開発すれば、最悪、自分で作り直すことだってできる」という感覚ではいるのです。そのため、リスクを受け入れることができました。

サイドカーコンテナ

ところで、Daprのようなサイドカーを、単独プロセスではなくコンテナとして提供するパターンもあります。

qiita.com

ここで記載されているサイドカーパターンやアンバサダーパターンを使えば、アプリケーション開発はビジネスロジックに注力し、サイドカーやアンバサダーがインフラとの連携に注力するという責任分解もやりやすくなるでしょう。Daprで提供されているような主要機能を、自分で好きなように実装して提供することもできるはずです。

ではなぜサイドカーコンテナパターンにしなかったのかと言うと、単純に、自分で実装するのが面倒だったためです。そこにDaprがあるんだから、Daprを使おう、ダメだった時には自分で作ろう、というくらいの感覚です。

そうやってダメな場合の撤退パターンも考えたうえで、僕はDaprを使うことに決めたのです。

まとめ

  • なぜDaprを使うのか
  • Daprを使う上での課題
    • 導入事例が極めて少ない
    • 機能が少ない
    • メンテナンス期間の短さ
  • 最悪、自分で作り直すという覚悟を持って使い始めた

最後になりますが、僕はDaprというプロダクトに魅力を感じているのもそうですが、それ以上にDaprという「アプリケーション寄りのサイドカーサービス」というアーキテクチャそのものに魅力を感じて使っているとも言えます。そのアーキテクチャに利があるという確信があるため、最悪は自分で作れば良いという覚悟ができましたし、他にもっと良いものが出てきたら乗り換えれば良い、きっと乗り換えやすいはずだと考えています。

それがいまの僕の考え方なんだ、ということをお伝えして、Dapr Advent Calendarを締めくくりたいと思います。

それでは、またの機会に!

See you!

*1:ちなみにこのお客様は5〜6年くらい前に「マイクロサービスで作ってみましょう」という提案も受け入れてくれました

Dapr Advent Calendar 23日目 - Daprのロードマップ

こんにちは Dapr Advent Calendar 23日目です。これまでのAdvent CalendarではDaprの機能や使い方を紹介してきましたが、今後、Daprはどういう方向が強化されるのでしょうか。そのロードマップを少し眺めてみたいと思います。

Daprのロードマップを見てみよう

Daprのロードマップは、Github Projectsで管理されています。

github.com

ロードマップにはいくつかのレーンが設けられています。

Backlog は対応予定だけど優先順位が確定していないもの。👍をつけていけば優先度を上げられるようです。

Planned (Committed) はだいたいマイルストーンが決まって設計を検討しているもの。

In Progress (Development) は開発中のもの。

DoneReleased はそれぞれ開発完了のものとリリース済みのものです。

もちろんすべてのIssueは紹介できませんので、In Progress、Planned、Backlogの中から、主要なものや面白そうなものをピックアップして紹介します。

In Progress

現在開発中の In Progress には、2021-12-23時点で8個のIssueがあります。4つが現在AlphaやBetaのコンポーネントをGAにするとか、作り直そうというものです。

残りの4つを簡単に紹介します。

Planned

今後対応予定の Planned には、2021-12-23時点で26個のIssueがあります。

品質周りのIssueが多く、現在AlphaやBetaのコンポーネントをGAにするとか、DaprコンポーネントのE2Eテストや結合テストを強化するなど、内部の品質を高めるissueが全体の半数を超えています。

残りの半数のうちから、興味深いものをいくつか紹介します。

Backlog

まだマイルストーンなど決まっていない Backlog には、2021-12-23時点で25個のIssueがあります。いつ出てくるかは分かりませんが、逆に言えば少し規模や影響の大きなIssueも多いです。

Pub/sub関連

分散トレーシング関連

ネットワーク関連

その他

また、ロードマップにはないのですが components-contribのissue で、次のIssueが重要なものとしてピン止めされています。

ECS上でDaprを動かせるようにしようという提案。以前にもこのAdvent Calendarの中で触れたことがありましたが、ECSで使えるようになればユーザーも増えるんじゃないかと期待しています。

まとめに代えて

全体を眺めた感じ、品質向上のテーマや既存機能の改善がほとんどで、とんでもない大物が待っているような気配はありません。まずはアーリーアダプターが使って満足できるように足場を固めているところ、という感じでしょうかね。利用者が増えてきたり、Azureと絡めて予算がつくようになってくれば、また傾向が変わるかも知れません。

ところで、「とんでもない大物」と言えば、ロードマップにはありませんが、こんなIssueがありました。

github.com

SQLを発行するためのAPIを作ろう、トランザクションも有効にしよう、というものです。さすがにそれは考えないようにしてるのかな、と思ってたのですが、少なくとも提案はされているようですね。ちょっと注目しておきたいIssueです。

そんなわけで、今回はここまでにしたいと思います。

明日のAdvent Calendarは、@backpaper0さんがDaprのActorについて書いてくれる予定です。Actorはよく分からなくて飛ばしたところなので、楽しみですね!

それでは!

Dapr Advent Calendar 22日目 - Dapr vs Spring Cloud

こんにちは Dapr Advent Calendar 22日目です。おとといから合宿をしていたため今日は更新が遅くなってしまいました。今回はDaprをSpring Cloudと比べてみたいと思います。

Spring Cloud vs Dapr

僕はもともとSpring BootとSpring Cloudを使って、いわゆるマイクロサービスを開発してました。

Spring CloudはいわゆるCloud Nativeな分散アプリケーションに必要となる要素をフルスタックで提供するプロダクトで、サービスディスカバリー、メッセージング/ストリーミング処理、分散トレーシング、サーキットブレイカーなどの機能をSpring Bootから扱いやすい形で提供しています。こんな機能をSpring Cloud以上に使いやすく提供しているプロダクトは、おそらく今のところ他にはないでしょう*1

一時期、僕はJava以外の言語、たとえばGoなどでマイクロサービスの開発をしようかと考えたこともあったのですが、結局はSpring Cloudを使いたいからSpring Boot、そのためにJavaJVM言語)を使うという選択をしていました。そうやって言語選択に影響するくらい、Spring Cloudは分散アプリケーション開発における重要なプロダクトでした。

そんなSpring Cloudと、Daprを、次の3つの機能で比べたいと思います。

  • サービス呼び出し
  • 非同期メッセージング
  • 分散トレーシング

この3つが、分散アプリケーションの柱ですよね。

1. サービス呼び出し

まずはアプリケーションから別アプリケーションを呼び出す機能を比較します。

DaprのInvoke API

Daprのサービス呼び出しはInvoke APIを利用します。このAdvent Calendarでも何度となく触れてきました。

cero-t.hatenadiary.jp

ソースコードは次のようになります。

@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:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method というDaprのInvoke APIを指定することで、Dapr経由で対象のアプリケーションを呼ぶことができます。

Daprはアプリケーション名から対象サービスが動いているホストを発見するために、ローカル環境ではmDNS(マルチキャストDNS)を利用し、k8sではk8s自身が持つ名前解決機能を利用します。いずれも利用できない環境では、現時点ではConsulを利用する必要があります。

たとえば、Amazon EC2上でもConsulを利用すればDaprを動かすことができます。

cero-t.hatenadiary.jp

そのような場合を除けば、Daprは基本的にはサービスレジストリなしで運用できるところが強みですね。

Spring Cloud Service Discovery

Spring Cloudのサービス呼び出しは、名前解決のためにNetflix Eurekaを利用します。

spring.pleiades.io

名前解決のためのサーバとなるEureka Server(上で言うConsulに相当します)を立て、それぞれのアプリケーションではEureka Discovery Clientを利用して自分自身をEureka Serverに登録したり、他のアプリケーションの情報をEureka Serverから取得してアクセスするという仕組みになっています。

サーバ側であるEureka Serverのコードは次のようになります。

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

@EnableEurekaServer アノテーションをつけたアプリケーションを起動するだけで、難しいことはありません。もちろん設定ファイルを追加して細かな設定をしたり、クラスタを組んだりすることができます。

クライアント側のソースコードは次のようになります。

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

Dapr側のソースコードと同じです。

baseUrlhttp://hello-app というアプリケーション名さえ指定すれば、RestTemplateが自動的にEureka Discovery Clientを利用して対象のアプリケーションにアクセスします。URLのシンプルさで言えば、Daprを使うよりも分かりやすいですね。

どちらが良いか?

サービス呼び出しについて、DaprとSpring Cloud、どちらが良いでしょうか。

DNSを別に立てる必要がないDaprのほうが使いやすいですが、Spring Cloudはローカル環境でもEureka Serverを立てる必要があります。ただ、立てることは大して難しくないので、そこまでのデメリットとも言えません。

また、呼び出す際のURLはSpring Cloudのほうが分かりやすく、DaprのInvoke APIはどうしても長くなってしまいます。しかし、これもそこまで大きなデメリットと言えません。

判定としては「引き分け」というか、大差ないというのが正味の所ですね。

2. 非同期メッセージング

続いて、非同期メッセージングについて比較します。

DaprのPus/sub APIとSubscription

DaprではメッセージングのPub/sub APIを用いてメッセージを送信し、Subscriptionの設定ファイルとWeb APIを作るだけでメッセージを受信できます。

cero-t.hatenadiary.jp

メッセージを送る側のソースコードは次のようになります。

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

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

pubsubUrlhttp://localhost:${DAPR_HTTP_PORT}/v1.0/publish/rabbitmq-pubsub/my-message というDaprのPub/sub APIを指定して、メッセージブローカーにメッセージを送ることができます。

続いて、メッセージを受け取る側のソースコードです。次のようになります。

@PostMapping("/subscribe")
public void subscribe(@RequestBody CloudEvent<MyMessage> cloudEventMessage) {
    System.out.println("subscriber is called");
    System.out.println(message);
}

メッセージを受け取るWeb APIを作成するだけです。

このWeb APIを利用するために、次のような設定ファイルを作成します。

apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
  name: subscription
spec:
  pubsubname: rabbitmq-pubsub
  topic: my-message
  route: /subscribe
scopes:
  - subscribe-app

詳細な説明は割愛しますが、メッセージブローカーからメッセージを受け取ると /subscribe という上で作ったWeb APIにアクセスするという設定を書いています。

シンプルに、やりたいことを実現できる感じです。

Spring Cloud Stream

Spring Cloudでメッセージングを行うためには、Spring Cloud Streamを利用します。

spring.pleiades.io

Spring Cloud Stream 2.xから3.xの間でAPIに大きな変化があり、よりストリーム処理に特化したような形になりました。ここでは3.xの記法で紹介します。

メッセージを送る側は次のようなソースコードになります。

@PostMapping("/publish")
public void publish(@RequestBody MyMessage message) {
    streamBridge.send("my-message-0", message);
}

StreamBridge クラスを使って、メッセージを送ります。

また、ソースコード内で指定したメッセージのキーに対して、どのメッセージブローカーに送るかを設定ファイルに書きます。

spring.cloud.stream.bindings.my-message-0.destination=my-message

ここで指定した値がRabbitMQのExchangeの名前として使われます。

続いて、メッセージを受け取る側のソースコードです。

@Bean
public Consumer<MyMessage> subscribe() {
    return (map) -> {
        System.out.println("subscriber is called");
        System.out.println(map);
    };
}

DaprのようなWeb APIではなく、java.util.funciton パッケージの ConsumerFunction などを使って実装します。

そして、メッセージを受け取った際にこのメソッド(Bean)を呼び出すよう設定ファイルを作成します。

spring.cloud.stream.bindings.subscribe-in-0.destination=my-message
spring.cloud.stream.bindings.subscribe-in-0.group=my-message-subscribe

上の値がExchangeの名前、下の値がQueueの名前として使われます。設定に若干クセがあって微妙に分かりづらいですね。

ただ、subscribe側を「API」などではなく「Function」と捉えて実装させるというのは納得感もあります。このバージョンのSpring Cloud Streamのソースコードを読んだことはないので推測になりますが、おそらくこの前段の処理は、メッセージブローカーからメッセージを1つずつ受け取って処理するのではなく、WebFluxを使ってノンブロッキングに複数の呼び出しをまとめているのでしょう。その方が性能的なメリットがありますからね。

どちらが良いか?

サービス呼び出しについては、DaprとSpring Cloud、どちらが良いでしょうか。

設定ファイルも含めた構成としては、Daprの方が分かりやすいです。しかし性能やメモリ使用量の少なさに関しては(計測したことはないですが)Spring Cloud Streamの方が上になると推測できます。特に大量のメッセージを同時に捌くような必要がある際には差が出そうです。

またDaprのほうはHTTPを使ってメッセージングをしていますが、Spring Cloud Streamでは独自クラスを使ってメッセージングをしています。そのためDaprの方がテスト時に別の処理に置き換えたり、curlコマンドでのテストがやりやすい一方で、HTTPを経由することのオーバーヘッドがあるとも言えます。

取り回しやすさではDaprに軍配が上がり、性能についてはSpring Cloudに軍配があがる、と言えるでしょうか。

3. 分散トレーシング

最後に、分散トレーシングについて比較します。

Daprの分散トレーシング対応

Daprでは設定ファイルを書くだけで分散トレーシングが有効になります。

cero-t.hatenadiary.jp

設定ファイルは次のような内容です。

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: daprConfig
spec:
  tracing:
    samplingRate: "1"
    zipkin:
      endpointAddress: http://localhost:9411/api/v2/spans

分散トレーシングのサンプリングレートやzipkinサーバのアドレスを指定するだけで、分散トレーシングが有効になります。

ただしトレースIDを自分で伝播させる必要があるため、次のようなコードを書く必要があります。

@GetMapping("/invokeHello")
public Map<String, ?> invokeHello(@RequestHeader("traceparent") String traceparent) {
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.set("traceparent", traceparent);
    HttpEntity<?> request = new HttpEntity<>(httpHeaders);

    Map<?, ?> result = restTemplate.exchange(helloUrl + "/hello", HttpMethod.GET, request, Map.class).getBody();
    return Map.of("baseUrl", helloUrl, "remoteMessage", result);
}

HTTPヘッダで受け取った traceparent というヘッダの値を、次のリクエストのHTTPヘッダに渡しています。これをすべてのエンドポイントの処理に書くことは現実的ではないですから、何か共通的な処理にする必要があるでしょう。その辺りは自分で用意しなければなりません。

Spring Cloud Sleuth

Spring Cloudで分散トレーシングを行うには、Spring Cloud Sleuthを利用します。

spring.pleiades.io

Spring Cloud Sleuthをdependenciesに追加して次のように設定ファイルを作成します。

spring.sleuth.sampler.rate=100
spring.zipkin.sender.type=web
spring.zipkin.baseUrl=http://localhost:9411

このように設定すれば、分散トレーシングが有効になりZipkinにトレース情報が送られます。

分散トレーシングは、RestTemplateやWebClientによるHTTP通信や、Spring Cloud Streamによるメッセージングなどすべてが対象になりますし、Daprのほうで問題となったトレースIDの伝播も自動的に行われます。

それだけでなく、トレースIDなどの情報がロガーのMDC(Mapped Diagnostic Context)に保存されるため、ログの設定を書けばログにトレースIDなどを出力することができ、トレースIDを起点としたログ検索がやりやすくなります。

このような事が何の実装もなく実現でき、トレースIDの伝播も自動で行われるのは、Spring Cloudがフルスタックで提供されていることが引き出すメリットだと言えるでしょうね。

どちらが良いか?

分散トレーシングについては、トレースIDの伝播も自動的に行うSpring Cloudの方が便利だと言えます。

また、Spring Cloud Sleuthではキュー経由でトレース情報を送ることでトレーシングの余計な待ち時間を減らしたり、Zipkin以外の様々な分散トレーシングツールと連携したりすることも可能です。

さすがは5年以上の歴史あるプロダクトだと言ったところでしょうか。僕が分散トレーシングという言葉を知ったのもこのSpring Cloud Sleuthがキッカケでしたし、他のプロダクトに先駆けてフルスタックで機能を提供してきた筋の良いプロダクトだと思います。

総合的な比較

ここまでDaprとSpring Cloudの3機能を比べてきましたが、総合的に見て、どちらの方が良いのでしょうか。

分かりやすさやHTTPによる疎結合という点ではDaprの方が上ですが、非同期メッセージングの性能や分散トレーシングの機能はSpring Cloudに強みがあります。歴史の浅いプロダクトと、歴史あるプロダクトの差、と言い換えることができるかも知れません。またこの3機能だけではなく他の機能を含めて、あるいは世の中にある情報量の差なども考えれば、Spring Cloudの方が上だと言えるでしょう。

バージョンアップに伴う苦痛

それではなぜ、僕はSpring CloudをやめてDaprを選んだのでしょうか。一言でいうと「バージョンの互換性やバージョンアップに伴う問題」が要因でした。

たとえば古いバージョンのJavaとSpring Bootで運用しているシステムがあり、新システムではより新しいバージョンのJavaとSpring Bootで開発しようとした場合、それぞれのシステムでSpring Cloudを使おうとすると、それぞれのSpring Bootに対応したSpring Cloudのバージョンが異なってしまうため、そこで互換性が失われていることがありました。たとえばSpring Cloudのバージョンアップに伴って、内部で使っているEurekaのバージョンが上がった際にプロトコルが変わってしまったことがありましたし、また上にも述べたとおりSpring Cloud Streamは2.xと3.xで大きくAPIの形が変わっています。

であれば常にJava、Spring Boot、Spring Cloudを最新バージョンに更新し続ければ良いのかも知れません。ただ、Spring Cloudはバージョンアップに伴う作業が大がかりになりがちでした。これはCloud Nativeという分野が比較的新しいため、対応するプロダクトやトレンドが変わり、それにSpring Cloudが追従しようとした際に互換性を失わざるを得ないという面があったのだと思います。

またもう少しマイナーな問題としては、Spring Bootの提供スピードとSpring Cloudの提供スピードの違いや依存性の複雑さなどにより、Spring Bootのバージョンを上げようとすると、Spring Cloudがまだ対応していないとか、参照しているライブラリのバージョンが異なってエラーになることがありました。

その辺りの痛みを経験したことで、Spring Cloudが悪いというよりは、「Web APIを提供するアプリケーションと、その下支えをするインフラとの境界になるレイヤーは、もっと疎結合にして、それぞれ個別にバージョンアップできるようにしたほうが良い」と考えるようになったのです。

それがSpring CloudではなくDaprに使うに至った経緯です。

まとめ

  • サービス呼び出しについては、DaprもSpring Cloudもあまり変わらない
  • メッセージングについては、Daprの方がシンプルだがSpring Cloudのほうがおそらく性能が良い
  • 分散トレーシングについては、DaprよりもSpring Cloudの方が高機能だし使いやすい
  • Spring Cloudはバージョンアップに伴う苦痛が大きかった

ところで、最近しばらくSpring Cloudをあまり追っていなかったためドキュメントなどを見直しながら今回のブログを書いたのですが、Spring Cloudのドキュメントはどこに何があるのか少し分かりづらくなっていて、どうしちゃったのかなという気持ちになりました。歴史あるプロダクトに様々な機能がついて複雑になっていく中で、ドキュメントも複雑になっていくということがあるのかも知れません。もちろん、その辺りを補うようなガイドや、ブログ、発表資料なども多いのが、Springの良いところなのですけどね。

いずれにせよ、SpringはJavaのCloud Nativeアプリケーションの開発を牽引する立場として、今後も応援したいと思います。

それでは、また明日!

*1:もしあればこっそり教えてください

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のアプリケーション開発を進めていました。もちろんこれから変わる所もあるでしょうし、今後も方法論を磨いていきたいと思います。

それでは、また明日!

Dapr Advent Calendar 20日目 - DaprをAmazon EC2で使う

こんにちは Dapr Advent Calendar 20日目です。錦鯉、M-1優勝おめでとうございます! うるさい漫才は好きではないんですが、錦鯉は、なんでしょうか、キャラのせいなんですかね、わりと好きなんです。優勝したペアが涙を流すことはあっても、審査員までもらい泣きしているのはなかなか見ないなと思いましたね!

DNSにConsulを使ってEC2でDaprを運用する

さて、以前 Dapr Advent Calendar 12日目 - Daprをk8s以外の分散環境で使う で、Amazon EC2のようにIPマルチキャストが使えない環境では、Daprを動かすためにConsulが必要ということを書きました。それについて、@kencharosさんからコメントを貰いました。

Consulはdevモードであれば立てるのがそんなに難しくないようなので、今回はこれで試してみましょう。

EKSで動かした翌日にEC2で動かした話を書くなんて、なんか技術スタックが巻き戻ってる気もするのですが、k8sを使うことに抵抗がある人も多いでしょうから、参考にしてもらえると嬉しいです。

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

EC2にインスタンスを作成

まずはAmazon EC2インスタンスを3つ作ります。1台はConsulサーバ、残り2台はアプリケーションをデプロイするサーバとして利用します。

EC2の使い方や手順などについては割愛しますが、Amazon Linux 2のt2.microインスタンスを3つ作りました。

EC2にConsul環境を構築

まずはEC2インスタンスの1台にConsulの環境を構築しましょう。

次のコマンドを実行してConsulのCLIツールをインストールします。

curl -LO https://releases.hashicorp.com/consul/1.11.1/consul_1.11.1_linux_amd64.zip
unzip consul_1.11.1_linux_amd64.zip
sudo mv consul /usr/local/bin/

続いて、Consulをサーバとして起動します。

consul agent -dev -client=0.0.0.0

開発用に1インスタンスで起動するだけなので -dev オプションをつけ、別のインスタンスからアクセスできるよう -client=0.0.0.0 オプションをつけました。

これでConsulサーバの構築は完了です。とても簡単でしたね。

アプリケーションのDapr実行環境を構築

続いて、残り2台のインスタンスにアプリケーションを実行するための環境を構築しましょう。

まずはJava 11 (corretto)をインストールします。

sudo yum install -y java-11-amazon-corretto-headless

続いて、Dapr CLIをインストールして初期化します。

wget -q https://raw.githubusercontent.com/dapr/cli/master/install/install.sh -O - | /bin/bash
dapr init --slim

Daprは --slim オプションを付けて、Dockerや設定ファイルを作らないスタンドアロンモードで初期化しました。

これでアプリケーションの実行環境が構築できました。

アプリケーションのデプロイ

ソースコード

続いてアプリケーションの準備です。サンプルアプリケーションのソースコードGitHubに置いてあります。

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

このうち「hello」と「invoke」を使います。何度も使っているアプリケーションなので、ソースコードの説明は省略します。

名前解決にConsulを使う設定ファイルを作成

Daprが連携するために使う名前解決にConsulを利用できるよう設定ファイルを作成します。ComponentではなくConfigurationとして、次のように設定します。

consul-config.yaml

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: consul-config
spec:
  nameResolution:
    component: "consul"
    configuration:
      selfRegister: true
      client:
        address: "172.*.*.*:8500"

spec.nameResolution に名前解決の設定をします。

今回はConsulを利用するため componentconsul を指定しました。

そして、起動時にアプリケーションが自動登録されるよう configuration.selfRegistertrue とします。

また configuration.client.address にConsulサーバのプライベートIPアドレスを指定します。後ほど、アプリケーションを実行するインスタンス上でこの値を書き換えます。

この設定ファイルを使うことで、Daprが名前解決としてConsulを利用できるようになります。

ソースコードのダウンロードと書き換え

それでは、アプリケーションを実行する2台のインスタンスでそれぞれアプリケーションのソースコードをダウンロードしして解凍します。

curl -LO https://github.com/cero-t/dapr-advent-2021/archive/refs/heads/main.zip
unzip main.zip

続いて、Consulサーバのアドレスを書き換えましょう。

vi dapr-advent-2021-main/consul-config.yaml

一番最後の行にある 172.*.*.*:8500 の部分に、Consulサーバとして起動したインスタンスのプライベートIPを指定してください。この作業も2台のインスタンスでそれぞれ行います。

アプリケーションの起動

それではアプリケーションを起動しましょう。

1台目のインスタンスでhelloアプリケーションを起動します。

cd dapr-advent-2021-main/hello
dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080 --config ../consul-config.yaml ../mvnw spring-boot:run

--config ../consul-config.yaml オプションをつけて、設定ファイルが有効化されるようにしました。

2台目のインスタンスも同様にしてinvokeアプリケーションを起動します。

cd dapr-advent-2021-main/invoke
dapr run --app-id invoke-app --app-port 8081 --dapr-http-port 18081 --config ../consul-config.yaml -- ../mvnw spring-boot:run -Dspring-boot.run.profiles=dapr

オプションは上と同様です。また -Dspring-boot.run.profiles=dapr をつけて application-dapr.yaml の設定が有効になるようにしています。

これでアプリケーションの起動が完了しました。

アプリケーションにアクセス

そして、別のコンソールから任意のインスタンス上で次のコマンドでアクセスします。

curl (invokeアプリケーションを起動したインスタンスのプライベートIPアドレス):8081/invokeHello

次のような結果が表示されます。

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

問題なくinvokeアプリケーションからhelloアプリケーションにアクセスできましたね!

AWSではマルチキャストDNSが使えないため、名前解決の設定なしでは通信することができませんでした、Consulを利用することで無事にDapr上で動くアプリケーション同士が連携して通信することができました。

f:id:cero-t:20211220002142p:plain
Consulを使った名前解決

EC2かEKSか

Daprのことを話題にすると、HTTP通信だけですべてが解決できるとか、別のサーバなどなしで開発ができるところをメリットと思ってもらえるのですが、「ただ、k8sはちょっと・・・」というところで尻込みされることが何度となくありました。minikubeやEKSを使ってみて、そこまで難しいとは感じませんでしたが、とは言えやはりこれまで使ってきた技術スタックとは異なるものですし、Daprという新しい技術に加えてk8sまで使うとなると、抵抗があるというのも理解できます。

今回はConsulを使い、EC2アプリケーション上で動くDaprアプリケーション同士が通信できることを確認しました。この方法を使えばk8sを使わなくともDaprアプリケーションを運用することができます。もちろんConsulのサーバを立てて運用する必要がありますが、その辺りはSpring CloudのEurekaなども大きな差はないでしょう。欲を言えば、AWSがマネージドConsulサーバサービスを提供してくれると嬉しいところです。

いずれにせよk8sだけでなく、EC2のような仮想サーバに直接デプロイするとか、ECSのようなコンテナサービスにデプロイするとかが気軽にできるようになれば、またDaprを利用する人も増えるんじゃないかなと思います。名前解決の簡単さはDaprの強みとなるので、今後のアップデートでより洗練されることを期待しています。

まとめ

  • Consulの開発モードを使ってEC2上にConsulサーバを立てました
  • Daprの設定でConsulサーバを名前解決に使うよう設定しました
  • Consulを使えば問題なくEC2でも名前解決ができ、通信することができました

これを12日目のエントリーの時点で書けていれば、もっと良かったのですけどね🙄

ちょっと行き当たりばったりになってしまうところも、毎日ブログを書いてる醍醐味ひとつということで。

それでは、また!

Dapr Advent Calendar 19日目 - DaprをAmazon EKSで使う

こんにちは Dapr Advent Calendar 19日目です。ついに最後の1週間に突入しました。3日目くらいの時に「ちょっとこれ無理かな」とか思ってたんですけど、人間、やってみれば意外といけるもんですね!

DaprをAmazon EKSで運用する

さて、これまでk8s + Daprのアプリケーションをローカルのminikube上で動かしてきましたが、今回はAmazon EKS上で動かしてみます。Daprはマイクロソフトが中心に開発しているOSSなので、Azureのほうがサポートされているミドルウェアが多かったりドキュメントが多かったりするのですが、AWSでも特に問題なく運用できています。

f:id:cero-t:20211219103727p:plain
今回動かすアプリケーション

環境設定

事前準備

まず前提として、次のことは完了済みとします。

  • AWSアカウントの作成
  • AWS CLIのインストール
  • aws configureが済んでいて、awsコマンドが実行できる(~/.aws/credentialsに必要な情報を記載済み)

AWSを日常的に利用している人は、この辺りは既に終わっていると思いますので、説明は割愛します。

また、これまでのAdvent Calendarで利用してきた次のツールもインストール済みとします。

  • Dapr CLI
  • Docker
  • kubectl

もしまだインストールしていなければ、インストールしておいてください。

eksctlのインストール

上に記載したツール群に加えて、eksctlというCLIツールをインストールしてください。これはAmazon EKSをコマンドラインで操作するもので、ちょうどminikubeのCLIに近いものです。

https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html

これで準備は完了です。

EKS環境の構築

EKSクラスタの作成

まず、eksctlコマンドを使ってEKSクラスタを作成します。

eksctl create cluster \
  --name=dapr-test \
  --version 1.21 \
  --region ap-northeast-1 \
  --zones ap-northeast-1a,ap-northeast-1c

僕のアカウントでは --zones オプションをつけずに実行したら「ap-northeast-1bにキャパシティがないよ」というエラーが起きたので、AZとして -1a-1c を指定するようにしました。

なお、クラスタの作成に失敗すると基本的にはロールバックされるのですが、状況次第ではCloudFormationが削除されないことがあるので、残っているようなら手で削除してください。

ちなみに作成したEKSクラスタを放置すると(m5.largeのEC2インスタンス2つも合わせて)月2万円くらい掛かるので、不要になればすぐにEKSクラスタを削除して想定外の課金にならないよう気をつけてくださいね。

kubectlからEKSクラスタにアクセスする

eksctlでk8sクラスタを作成すると、kubectlの向き先が作成したEKSクラスタになっているはずです。念のため確認しておきましょう。

kubectl config current-context

次のように表示されるはずです。

(AWSアカウント名)@dapr-test.ap-northeast-1.eksctl.io

もしminikubeなど別のクラスタを参照している場合は、kubectl config get-contexts でEKSのクラスタ名を確認したうえで

kubectl config use-context (AWSアカウント名)@dapr-test.ap-northeast-1.eksctl.io

などでkubectlの向き先がEKSにするようにしてください。

EKSクラスタを他の人(アカウント)が作成した場合など、手元のコンテキスト一覧にEKSクラスタがない場合は、次のコマンドでコンテキストを取得してください。

aws eks --region ap-northeast-1 update-kubeconfig --name dapr-test

これでkubectlからEKSにアクセスできるようになるはずです。

クラスタ情報を確認しておきましょう。

kubectl cluster-info

次のように表示されます。

Kubernetes control plane is running at https://(アカウントID).gr7.ap-northeast-1.eks.amazonaws.com
CoreDNS is running at https://(アカウントID).gr7.ap-northeast-1.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

これでkubectlからEKSにアクセスしていることを確認できました。

Dapr環境の構築

それではEKS上にDapr環境を構築しましょう。

dapr init -k

オプションの -kk8sを対象とします。kubectlが向いているk8s上にDaprの環境が構築されます。

初期化が終わったら、podの一覧を見てみましょう。

kubectl get pods -A

次のように表示されるはずです。

NAMESPACE     NAME                                     READY   STATUS    RESTARTS   AGE
dapr-system   dapr-dashboard-57b4db56fc-vxmcs          1/1     Running   0          89s
dapr-system   dapr-operator-5b4b68b5c5-cgh28           1/1     Running   0          89s
dapr-system   dapr-placement-server-0                  1/1     Running   0          89s
dapr-system   dapr-sentry-c6b746cdf-pxgdx              1/1     Running   0          89s
dapr-system   dapr-sidecar-injector-6f749dbf87-qcg7r   1/1     Running   0          89s
kube-system   aws-node-fp8m2                           1/1     Running   0          15m
kube-system   aws-node-rp5qq                           1/1     Running   0          15m
kube-system   coredns-76f4967988-7psgm                 1/1     Running   0          24m
kube-system   coredns-76f4967988-fjlrb                 1/1     Running   0          24m
kube-system   kube-proxy-xk8wm                         1/1     Running   0          15m
kube-system   kube-proxy-ztkr8                         1/1     Running   0          15m

Daprやk8s関係のpodが起動していることが分かります。

これでEKS上でDaprを動かす準備ができました。

アプリケーションのデプロイ

続いて、Hello Worldアプリケーションと、それを呼び出すInvokeアプリケーションをEKS上で動かしてみましょう。ソースコードGitHubにあります。

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

この中にある「hello」と「invoke」を利用します。

作成したアプリケーションのおさらい

helloアプリケーション

helloアプリケーションの HelloController をおさらいしておきましょう。

(hello) HelloController.java

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

/hello というエンドポイントでメッセージを返すだけのアプリケーションです。

invokeアプリケーション

invokeアプリケーションの InvokeController もおさらいしておきましょう。

(invoke) InvokeController.java

@RestController
public class InvokeController {
    private RestTemplate restTemplate;

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

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

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

/invokeHello というエンドポイントで、上の /hello を呼ぶ処理を行うアプリケーションです。

baseurlapplication.yamlapplication-dapr.yaml に定義されており、Springのプロファイルが dapr の時は次の設定が利用されます。

(invoke) application-dapr.yaml

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

DaprのInvoke APIを利用したURLです。これを使ってhelloアプリケーションを呼び出します。

アプリケーションのイメージ作成

イメージの置き場所となるECRの作成

続いて、アプリケーションのイメージを作成したいのですが、もちろんEKSからローカルPCにあるイメージレジストリを参照できるわけではありません。EKSを使う場合には、Docker HubやAmazon ECRなどを使うことになります。ここではEKSと距離の近いECRにイメージを置くことにしましょう。

次のコマンドでECRにリポジトリを作成します。リポジトリはイメージごとに作成する必要があるため helloinvoke という名前のリポジトリをそれぞれ作成します。

export AWS_REGION=ap-northeast-1
aws ecr create-repository --repository-name hello --region ap-northeast-1
aws ecr create-repository --repository-name invoke --region ap-northeast-1

これでリポジトリの作成は完了です。

イメージの作成

続いてイメージを作成します。上で作ったECRのリポジトリにpushできるよう、イメージ名をECRリポジトリURIに合わせる必要があります。

まずはhelloアプリケーションのイメージ作成です。次のようなコマンドになります。

cd (GitHubのディレクトリパス)/dapr-advent-2021

export HELLO_IMAGE=$(aws ecr describe-repositories --repository-names hello --query 'repositories[0].repositoryUri' --output text --region ap-northeast-1)

./mvnw spring-boot:build-image -pl hello -Dspring-boot.build-image.imageName=${HELLO_IMAGE}:1.0.0

(オプションが多くて長くなるため、改行を挟んでいます)

awsコマンドを用いてECRリポジトリのURLを取得し、それを環境変数 HELLO_IMAGE として保持します。リポジトリのURLは (アカウントID).dkr.ecr.ap-northeast-1.amazonaws.com/(リポジトリ名) となっています。

そして、Mavenspring-boot:build-image コマンドでイメージを作成するのですが、ここで -Dspring-boot.build-image.imageName オプションを使い、上で取得したECRリポジトリのURLをイメージ名として指定しています。

イメージの作成に成功したら、invokeアプリケーションも同様にイメージを作成します。

export INVOKE_IMAGE=$(aws ecr describe-repositories --repository-names invoke --query 'repositories[0].repositoryUri' --output text --region ap-northeast-1)

./mvnw spring-boot:build-image -pl invoke -Dspring-boot.build-image.imageName=${INVOKE_IMAGE}:1.0.0

作成が完了したら、念のため確認しておきましょう。

docker images | grep hello
(アカウントID).dkr.ecr.ap-northeast-1.amazonaws.com/hello   1.0.0          d62af7e6faa8   41 years ago    261MB
hello                                                     1.0.0          73b3e3b9795a   41 years ago    261MB
docker images | grep invoke
(アカウントID).dkr.ecr.ap-northeast-1.amazonaws.com/invoke   1.0.0          d38a74e45a8f   41 years ago    261MB
invoke                                                     1.0.0          bf464e49d0e6   41 years ago    261MB

ECRのURIがついたイメージができているはずです。もし以前に作ったhelloやinvokeのイメージがあれば、一緒に表示されます。

イメージのプッシュ

それでは作成したイメージをECRにプッシュしましょう。ECRにログインしてからでないとプッシュできないので、次のコマンドでログインをします。

export ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin https://${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com

アカウントIDやログインパスワードをawsコマンドで取得して、それを使ってログインしています。

これでログインに成功したら、イメージをプッシュします。

docker push ${HELLO_IMAGE}:1.0.0
docker push ${INVOKE_IMAGE}:1.0.0

イメージを作成した際に使用した環境変数をここでも使っています。

これでECRにイメージがプッシュされました。

アプリケーションをEKSにデプロイ

それでは、ECRにプッシュしたイメージを使って、EKS上にアプリケーションをデプロイしてみましょう。

helloアプリケーションのマニフェストファイルを作成

helloアプリケーションをデプロイするためのマニフェストファイルを次のように作成します。

eks/hello-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-app
  labels:
    app: hello
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "hello-app"
        dapr.io/app-port: "8080"
    spec:
      containers:
      - name: hello
        image: ***.dkr.ecr.ap-northeast-1.amazonaws.com/hello:1.0.0
        ports:
        - containerPort: 8080
        imagePullPolicy: IfNotPresent

以前に作ったものと同様です。イメージ名にECRリポジトリURIを指定する必要があるため *** の部分を自分のアカウントIDに置き換えてください。また、helloアプリケーションには外部からアクセスしないため、Serviceは作成しません。

invokeアプリケーションのマニフェストファイルを作成

続いて、invokeアプリケーション用のマニフェストファイルを作成します。

eks/invoke-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: invoke-app
  labels:
    app: invoke
spec:
  replicas: 1
  selector:
    matchLabels:
      app: invoke
  template:
    metadata:
      labels:
        app: invoke
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "invoke-app"
        dapr.io/app-port: "8081"
    spec:
      containers:
      - name: invoke
        image: ***.dkr.ecr.ap-northeast-1.amazonaws.com/invoke:1.0.0
        ports:
        - containerPort: 8081
        imagePullPolicy: IfNotPresent
        env:
        - name: spring.profiles.active
          value: dapr

---
kind: Service
apiVersion: v1
metadata:
  name: invoke-svc
  labels:
    app: invoke
spec:
  selector:
    app: invoke
  ports:
  - protocol: TCP
    port: 8081
    targetPort: 8081
  type: LoadBalancer

これも以前に作ったものと同様です。イメージ名の *** の部分は自分のアカウントIDに置き換えてください。

環境変数spring.profiles.activedapr を指定して、application-dapr.yaml の設定が有効になるようにしています。

また、こちらは外部からアクセスするため、Serviceを作成しています。

アプリケーションのデプロイ

それではアプリケーションをデプロイしましょう。デプロイ方法はminikubeでもEKSでも変わりません。

cd (GitHubのディレクトリパス)/dapr-advent-2021
kubectl apply -f eks/hello-app.yaml
kubectl apply -f eks/invoke-app.yaml

コマンドに成功したらしばらく待って、podの一覧を見てみましょう。

kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
hello-app-6f97c6ff57-dqqhd    2/2     Running   0          30s
invoke-app-674c8bfb75-zpdrc   2/2     Running   0          33s

hello-appとinvoke-appが起動していればOKです。

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

さて、続いてアプリケーションへのアクセスを行います。

Amazon EKSではk8s上に spec.typeLoadBalancer であるServiceを作成すると、自動的にCLB(Classic Load Balancer)が作成され、ポートが外部に公開されます。作成された時点で世界中からアクセスできてしまうため、注意してください。

AWSのコンソールから確認するなり、次のコマンドで確認するなりして、CLBのアドレスを確認します。

aws elb describe-load-balancers --region ap-northeast-1

アドレスの確認ができたら、curlコマンドでアクセスします。

curl (CLB名).ap-northeast-1.elb.amazonaws.com:8081/invokeHello

次の結果が表示できるはずです。

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

このAdvent Calendarをずっと読んでる人には見慣れたメッセージが表示されました。invokeアプリケーションからDaprを経由してhelloアプリケーションのメッセージを取得することができています。

k8sやeksctlコマンドを使っていると、どこでクラスタ動いているのかをあまり強く意識せず使えてしまえるため忘れがちになりますが、CLBのアドレスでアクセスできている以上、Amazon EKS上で動いているのは確かなのです。意外と簡単でしょ。

f:id:cero-t:20211219103748p:plain
EKS上で動くアプリケーション

後片付け

無事にEKS上で動作確認ができたので、忘れないうちに環境を削除しておきましょう。そのまま残していてはセキュリティ的にも課金的にも危険です。

まずはデプロイしたアプリケーションを削除します。Serviceを削除する際に、一緒にCLBも削除されます。

kubectl delete -f eks

CLBの削除に時間が掛かるため、いつもより応答が遅くなる可能性があります。

続いて、ECRの削除を行います。

aws ecr delete-repository --repository-name hello --region ap-northeast-1 --force
aws ecr delete-repository --repository-name invoke --region ap-northeast-1 --force

最後に、EKSクラスタ自体を削除します。

eksctl delete cluster --name dapr-test --region ap-northeast-1

これで削除は完了です。

AWS上でどのように運用するか

今回はAmazon EKS上にhelloアプリケーションとinvokeアプリケーションのみをデプロイしましたが、もちろん他のアプリケーションをデプロイすることも可能です。

また、これまで使ってきたRabbitMQやPostgreSQLなどは、k8s上にデプロイするのではなく、マネージドサービスのAmazon MQ for RabbitMQやAmazon RDS for PostgreSQL、あるいはAmazon Aurora PostgreSQLなどを利用することも可能です。

それぞれのマネージドサービスでインスタンスを作成し、Daprの接続先がそれぞれのインスタンスになるよう指定し、適切にセキュリティグループを設定すればアクセスができます。その辺りはAWSの説明になりすぎますし、説明すると長くなるため今回は割愛しました。

ところで、今回はk8sのLoadBalancerを利用し、AWSのCLBが自動的に作成されることを確認しました。ただ、たとえばAPI GatewayAPIを管理する場合には、対象としてCLBを指定できないため、ALBが必要となります。実際に僕が運用しているシステムでは、CLBではなくALBを利用しています。

ALBを使う場合には、アプリケーションのServiceの type として LoadBalancer ではなく NodePort を指定し、それとは別に Ingress を作成してALBと接続する設定を行います。その辺りの使い方については、クラスメソッドのブログなどが詳しいのでそちらを参考にしてください。

dev.classmethod.jp

いずれにせよ、目的に合わせて柔軟に環境を構築できることは間違いありません。

まとめ

  • eksctlコマンドを使って、Amazon EKSのクラスタを作成しました
  • kubectlコマンドを使って、EKS上にアプリケーションのデプロイを行いました
  • typeLoadBalancer のServiceを作成すると、CLBが自動的に作成され、インターネット経由でアクセスすることができます
  • ALBを利用する場合は typeNodePort のServiceを作成し、Ingress を利用してALBと接続します
  • EKSを試す場合には、使い終わったら削除することを忘れないようにしてください

ところで今回もソースコードには全く手を入れることがありませんでした。このAdvent Calendarを書き始めたときに、最初からk8sの環境で動かすことを想定してサンプルのソースコードを作成したわけではなく、あくまでDaprを使うことだけを想定していたのですが、k8sでも問題なく動いています。

Daprを使うことで環境から切り離され、ローカルPCで直接動かしても、minikubeで動かしても、あるいはAmazon EKSで運用しても、それぞれの設定ファイルを作成するだけで、ソースコードは変更しなくて済んだのです。

これは間違いなく、Daprのメリットだと言えるでしょうね。

それでは、また明日!

Dapr Advent Calendar 18日目 - Dapr + k8sとZipkinで分散トレーシング

こんにちは Dapr Advent Calendar 18日目です。気づけばあと2週間で年明けじゃないですか。歳を取ると1年が早くなるって言いますけども、それとは別に、この2年はやっぱり早かったですよねぇ。

k8sとDaprで分散トレーシング

さて、今回はk8s + Dapr上のアプリケーションの分散トレーシングを行います。主にDaprのパイプライン設定ファイルをk8sに適用するところや、それをアプリケーション側で有効にする辺りが、今回のポイントとなります。

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

アプリケーションの作成

動かすアプリケーションは Dapr Advent Calendar 8日目 - DaprとZipkinで分散トレーシング で作成したものです。そちらを先に読んでからこのエントリーを読んでください。

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

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

また Dapr Advent Calendar 13日目 - Dapr + k8sでHello World で説明したツールなどがセットアップ済みであることを前提で説明を書いています。

作成したソースコードのおさらい

以前のエントリーで作成したアプリケーションのソースコードをおさらいします。

TracingController.java(抜粋)

public class TracingController {
    private RestTemplate restTemplate;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method")
    private String helloUrl;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/publish-app/method")
    private String publishUrl;

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

    @GetMapping("/invokeHello2")
    public Map<String, ?> invokeHello(@RequestHeader("traceparent") String traceparent) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set("traceparent", traceparent);
        HttpEntity<?> request = new HttpEntity<>(httpHeaders);

        Map<?, ?> result = restTemplate.exchange(helloUrl + "/hello", HttpMethod.GET, request, Map.class).getBody();
        return Map.of("baseUrl", helloUrl, "remoteMessage", result);
    }

    @PostMapping("/invokePublish")
    public void invokePublish(@RequestBody Object message, @RequestHeader("traceparent") String traceparent) {
        System.out.println(traceparent);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set("traceparent", traceparent);
        HttpEntity<?> request = new HttpEntity<>(message, httpHeaders);

        restTemplate.exchange(publishUrl + "/publish2", HttpMethod.POST, request, Void.class);
    }

Hello Worldを呼ぶ /invokeHello2 と、メッセージのpublishを呼ぶ /invokePublish というエンドポイントを提供しています。分散トレーシングに用いる traceparent というHTTPヘッダを伝播させるために、少しだけ実装が複雑になっています。

アプリケーションのイメージ作成

続いて、このアプリケーションのイメージを作成しましょう。

イメージ作成先がminikubeのイメージレジストリになるよう次のコマンドを実行します。

eval $(minikube docker-env)

そしてイメージ作成のコマンドを実行します。

cd (GitHubのディレクトリパス)/dapr-advent-2021/tracing
../mvnw spring-boot:build-image

これでイメージが作成できました。

また、hello publish subscribe のアプリケーションも必要となるため、それぞれイメージを作成します。

cd (GitHubのディレクトリパス)/dapr-advent-2021
./mvnw spring-boot:build-image -pl hello,publish,subscribe

これでイメージ側の準備は完了です。

マニフェストファイルの作成

次に、このアプリケーションをデプロイするためのマニフェストファイルを作成します。

k8s/tracing-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tracing-app
  labels:
    app: tracing
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tracing
  template:
    metadata:
      labels:
        app: tracing
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "tracing-app"
        dapr.io/app-port: "8087"
    spec:
      containers:
      - name: tracing
        image: tracing:1.0.0
        ports:
        - containerPort: 3500
        imagePullPolicy: IfNotPresent

---
kind: Service
apiVersion: v1
metadata:
  name: tracing-svc
  labels:
    app: tracing
spec:
  selector:
    app: tracing
  ports:
  - protocol: TCP
    port: 3500
    targetPort: 3500
  type: LoadBalancer

以前作成した hello.yaml とほとんど同じ構成ですが、今回はアプリケーションのポートを外部公開するのではなく、Daprのポートを外部公開するため3500番ポートを公開する設定にしています。Daprを経由しなければ最初の traceparent を取得できないため、このような構成にしています。

アプリケーションのポートを公開すべきか、Daprのポートを公開すべきかは、あとで論じたいと思います。

Zipkinのマニフェストファイルの作成

続いて、分散トレーシングの収集とUIを提供するZipkinをデプロイするためのマニフェストファイルを作成します。

k8s/zipkin.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: zipkin-deploy
  labels:
    app: zipkin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: zipkin
  template:
    metadata:
      labels:
        app: zipkin
    spec:
      containers:
      - name: zipkin
        image: openzipkin/zipkin
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 9411

---
kind: Service
apiVersion: v1
metadata:
  name: zipkin-svc
  labels:
    app: zipkin
spec:
  type: LoadBalancer
  selector:
    app: zipkin
  ports:
  - protocol: TCP
    port: 9411
    targetPort: 9411

イメージを使ってZipkinをデプロイしています。内容は以前作成したPostgreSQL用のものとほとんど同じです。外部から9411番ポートにアクセスできるようServiceも作成しています。

Daprのパイプライン設定ファイルでZipkinへのトレースを指定する

続いて、Daprのパイプライン設定ファイルを作成して、Zipkinにトレーシング情報を送るようにします。

k8s/zipkin.yaml

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: tracing
spec:
  tracing:
    samplingRate: "1"
    zipkin:
      endpointAddress: http://zipkin-svc:9411/api/v2/spans

これは Dapr Advent Calendar 8日目 - DaprとZipkinで分散トレーシング で説明した ~/.dapr/config.yaml とほとんど同じ内容で、接続先だけが localhost ではなく zipkin-svc を参照するように修正しています。

samplingRate には 1 を設定して、すべてのトレーシング情報をZipkinに送るようにしています。

またこの設定には metadata.nametracing という名前をつけています。

アプリケーション側で分散トレーシングを有効にする

さて、上でHTTPのパイプライン設定ファイルを作成しましたが、このパイプライン設定がすべてのアプリケーションで有効になる、というわけではありません。Daprでは設定ファイルをk8sにapplyすると、わりと自動で全体的に有効化されることが多かったのですが、パイプラインのConfigurationに関しては勝手に全体で有効化されません。全体に有効化すると弊害が大きいというか、アプリケーションによってパイプライン設定が異なるのが当たり前ですからね。

そのため、アプリケーション側がこのパイプライン設定を使うように設定する必要があります。

アプリケーション側のマニフェストファイルを次のように修正します。

k8s/tracing-app.yaml(抜粋)

      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "tracing-app"
        dapr.io/app-port: "8087"

これを次のように変更します。

      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "tracing-app"
        dapr.io/app-port: "8087"
        dapr.io/config: "tracing"

dapr.io/configtracing を指定しています。この tracing というのは上のパイプライン設定ファイルの metadata.name で指定した名前です。これで、アプリケーション側がパイプライン設定を利用するようになり、分散トレーシングが有効になるわけです。

同じように k8s/hello-app.yaml k8s/publish-app.yaml k8s/subscribe-app.yaml にも dapr.io/config: "tracing" を追加してください。

これでマニフェストファイルの作成は完了です。

アプリケーションをデプロイしてアクセスする

イメージをk8sにデプロイ

それでは作成したイメージをk8sにデプロイします。

cd (GitHubのディレクトリパス)/dapr-advent-2021
kubectl apply -f k8s/zipkin.yaml
kubectl apply -f k8s/tracing.yaml
kubectl apply -f k8s/tracing-app.yaml
kubectl apply -f k8s/hello-app.yaml
kubectl apply -f k8s/publish-app.yaml
kubectl apply -f k8s/subscribe-app.yaml

まずZipkinをデプロイし、HTTPパイプライン設定ファイルをデプロイし、アプリケーションのデプロイという順番に行っています。

RabbitMQなどをデプロイしていない場合には、そちらもデプロイする必要があります。面倒なので次のコマンドでまとめてやってしまったほうが良いでしょう。

kubectl apply -f k8s

これでk8sフォルダにあるすべてのマニフェストファイルに対して kubectl apply が行われます。

k8s上のアプリケーションにアクセスする

それでは、起動したアプリケーションにアクセスしてみましょう。

ポートフォワードの設定

まずはローカルPCからminikube内のpodにアクセスできるよう、ポートフォワードの設定を行います。

次のコマンドを実行します。

kubectl port-forward service/zipkin-svc 9411:9411

これでローカルPCの9411番ポート経由でZipkinにアクセスできるようになりました。

また、別のコンソールで次のコマンドを実行します。

kubectl port-forward service/tracing-app 3500:3500

これでローカルPCの3500番ポート経由で、tracing-appのDaprにアクセスできるようになりました。

Hello Worldを呼び出す処理にアクセスする

それでは、別のコンソールからアクセスしてみましょう。

curl localhost:3500/invokeHello2 -H "dapr-app-id:tracing-app"

せっかくなのでDapr 1.4から使えるようになった方法でアクセスしてみました。これは curl localhost:3500/v1.0/invoke/tracing-app/method/invokeHello2 と同じ意味になります。

次のような結果が表示されます。

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

正しくメッセージが返ってきました。

Zipkin側でトレース情報を確認してみましょう。

f:id:cero-t:20211218134028p:plain
Zipkinのトレース一覧

f:id:cero-t:20211218134058p:plain
helloを呼んだトレースの詳細

トレーシング情報は、上から順番に

  • Dapr → tracing-app (/invokeHello2)
  • tracing-app → Dapr (Invoke APIでhello-appの /hello)
  • Dapr → hello-app (/hello)

となっています。Daprを通過する部分で分散トレーシングの情報が取得されていることが分かります。

Pub/subを呼び出す処理にアクセスする

続いて、Pub/subを行うエンドポイントにもアクセスしてみましょう。

curl -XPOST "localhost:3500/invokePublish" -H "dapr-app-id:tracing-app" -H "Content-type:application/json" -d '{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}'

特にエラーなどが起きなければメッセージングは成功しているはずです。

Zipkin側でトレース情報を確認してみましょう。

f:id:cero-t:20211218134116p:plain
Zipkinのトレース一覧

f:id:cero-t:20211218134131p:plain
publish/subscribeを呼んだトレースの詳細

トレーシング情報は、上から順番に

  • Dapr → tracing-app (/invokePublish)
  • tracing-app → Dapr (Invoke APIでpublish-appの /publish2)
  • Dapr → publish-app (/publish2)
  • publish-app → Dapr (Pub/sub APIでrabbitmq-pubsubのmy-message)
  • (Dapr -> RabbitMQ -> Dapr)
  • Dapr → subscribe-app (my-messageの受信)

となっており、やはりDaprを通過する部分で分散トレーシングの情報が取得されています。

RabbitMQにエンキュー、デキューするところ自体は分散トレーシングの対象外になっています。本当はここも表示されれば嬉しいんですけどね。

いずれにせよ、k8s上でも問題なく分散トレーシングができることが分かりました。

f:id:cero-t:20211218134311p:plain
Dapr + k8s上で動く分散トレーシングと対象のアプリケーション

外部からアプリケーションにアクセスすべきか、Daprにアクセスすべきか

今回はアプリケーションのポートではなく、Daprのポートに外部からアクセスする形としました。実際に運用する際、アプリケーション側を外部に公開すべきか、Dapr側を公開すべきか、どちらが良いのか改めて考えてみましょう。

基本的にはアプリケーション側のポートを外部公開するべきだと思いますし、実際に僕はそのように運用しています。

ただ、Dapr側を公開することで、次のようなメリット/デメリットがあります。

  • メリット
    • 外部からアクセスする際に、OAuthやJWTトークン検証などの認可機能を利用することができる
    • Dapr内部の通信だけでなく、外部からアクセスされた最初のリクエストを分散トレーシングに含めることができる
      • (逆に言えば、アプリケーションのポートに直接アクセスさせた場合は、最初のリクエストが分散トレーシングの範疇外となる)
    • アプリケーションの個別のポート番号を気にする必要はなく、Daprの3500番ポートにアクセスしさえすれば良い
  • デメリット
    • Daprが管理するすべてのアプリケーションやステートストア、メッセージング、シークレットストアにアクセスされてしまう危険性がある

デメリットが大きすぎるため、原則としてアプリケーション側のポートを外部公開すべきなのです。

ただ今回の例のように -H "dapr-app-id:app-name" を指定することで、任意のアプリケーションのDaprにのみアクセスさせることは可能です。k8sの前段にあるロードバランサなどでヘッダを固定し、仮に元のリクエストに dapr-app-id が含まれていてもそれを確実に無視するような仕組みが利用できるのであれば、daprのポートを安全に外部公開することも可能でしょう。そうすれば、認可や分散トレーシングなどの機能をうまく活用することができます。

実は僕もいま実運用しているシステムで、ちょうど同じ課題に直面しており、外部からのリクエストを分散トレーシングに含めるために、Daprのポートを外部公開するか、それともアプリケーション側のポートを外部公開して、アプリケーション側で traceparent を発行するのか、あるいはk8sまでのどこかで traceparent を発行するのかを迷っているところです。

僕自身は「迷ったときは安全側に倒したほうが良い」と考えているので、アプリケーション側のポートを公開し、一方で多少は雑に扱っても良い traceparent はクライアント側で発行するとか、k8sの前段のどこかで発行する、くらいが良いのではないかと考えています。

この辺りは様々なポリシーがあるでしょうから、Daprの識者たちともディスカッションしてみたいですね。

まとめ

  • Dapr + k8sの環境でも問題なく分散トレーシングができました
  • アプリケーション側の metadata.annotations で利用するHTTPパイプラインを指定する必要があります
  • 外部公開するのは、アプリケーション側のポートか、Dapr側のポートか、慎重に検討する必要があります

だいぶDaprとk8sの運用に慣れてきて、構成も考えられるようになってきましたね。

これでいったんDapr + k8sの機能を紹介するのはおしまいにして、明日はAWS上にデプロイする方法を説明したいと思います。

それでは!