Dapr を使ってみる - 入門編

この記事は Dapr に関する一連の記事の一部です。

- [Dapr って何？ - 概要編](https://qiita.com/takashiuesaka/items/ecb8d232d00f0a6591a5)
- Dapr を使ってみる - 入門編
- [Dapr を使ってみる - State management 編](https://qiita.com/takashiuesaka/items/713499c531095d5687c2)

# 開発環境

- Windows11/Mac Sonoma
- Docker Desktop 4.29.0 (145265)
- Visual Studio 17.10.0 Preview 6.0
- Visual Studio Code
    - C# DevKit

# 環境準備

Dapr は Sidecar パターンを使用しています。

[サイドカー パターン](https://learn.microsoft.com/ja-jp/azure/architecture/patterns/sidecar)

サイドカーコンテナはアプリケーションから見た場合、同一ホストに稼働する別プロセスです。つまり Kubernetes であれば 同一　Pod 内で稼働し、Azure Container Apps であれば同一アプリで稼働する前提です。

ローカル開発では、複数のアプリケーション・サービスを同時に立ち上げながら開発することも多いでしょう。その場合、各アプリケーションごとに Sidecar コンテナを立ち上げる必要がある、ということになりますが、Dapr はローカル開発用として 複数の Dapr プロセスを異なるポート番号で立ち上げることで 複数の Sidecar コンテナの代わりとすることができます。

そのようなローカル開発用のツールとして Dapr CLI があります。まずはこれをインストールします。

[Install the Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)

インストールをしたら、Docker Desktop を起動します。
次にTerminal, Powershellなどのコマンド入力ツールを開き、次のコマンドを実行すると 既定の Dapr 環境が構築されます。

```bash
dapr init
```

実行終了後に Docker Desktop などで実行中のコンテナを確認すると、次のように3つのコンテナが稼働しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/368ae1c7-f6a4-9aa7-7826-477178b49fc7.png)

この3つのコンテナは Docker を立ち上げるたびに起動します。Dapr 環境が不要なときは、消しておくことができます。

```bash
dapr uninstall --all
```
--all オプションをつけて削除すると、コンテナは綺麗に消えてくれますが、この後ご紹介する設定ファイルも全て消えてしまいますので、注意してください。

# 使用するサンプル


.NET Aspire Starter アプリケーションを使用します。この Starter アプリケーションは Web アプリと WebApi アプリが連携するようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/568d1170-7541-19b7-e87b-5edea4d7db0e.png)

Web の weather ページを開くと WebApi の /weatherforecast にアクセスして天気予報データを取得し、画面に表示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/2c2c231d-c5bf-8e14-f211-5a59f522a7d1.png)

WebApi の /weatherforecast に直接アクセスして、天気予報データの json を取得し、確認することもできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ae566d77-486c-b725-812a-400728e96984.png)

天気予報データはアクセスされるたびにランダム生成されます。

Starter アプリケーションでは、OpenTelemetry や回復性を実装するための ServiceDefaults プロジェクトも存在します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/1651cce7-a625-cff0-9edb-b7d436a1d8d7.png)

ServiceDefaults には OpenTelemetry　や回復力を実現するためのコーディングがしてあります。Web と WebApi プロジェクト から ServiceDefaults プロジェクト を参照し、コードを呼び出すように実装してあります。

Starter アプリケーションには最後にもう１つ、AppHost というプロジェクトがあります。この AppHost は Web と WebApi プロジェクトを起動し、便利なダッシュボード画面も立ち上げます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/f3b9f62f-c187-2e02-9b4c-2a6c94e5b797.png)

Starter アプリケーションは Dapr に対応していません。

## Starter アプリケーションを作成する

Visual Studio 2022 Preview を立ち上げ、新しいプロジェクトの作成画面で .NET Aspire Starter Application を選択します。右上の「プロジェクトの種類」検索ボックスで .NET Aspire と入力するとすぐ見つかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/0a03f346-0106-35bc-9fb6-284a02eb8978.png)

キャッシュ用に Redis を使用する（Docker が必要)のチェックボックスは今回は外します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/7e0de241-a0e7-c61a-a022-ef667d4c13cb.png)

ソリューションエクスプローラを確認すると、このように4つのプロジェクトが存在しているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/ac80224f-ce2c-33b8-c115-cc93241850ad.png)

## Mac OS または Visual Studio を使用しない場合

.NET 8 と .NET Aspire workload のインストールが必要です。以下のページを参考にインストールしてください。

[.NET 8.0 (Linux, MacOS, Windows)をダウンロード](https://dotnet.microsoft.com/ja-jp/download/dotnet/8.0)

[Install .NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/setup-tooling?tabs=dotnet-cli%2Cwindows#install-net-aspire)

.NET Aspire Starter Application の作成は次のコマンドを実行します。

```bash
dotnet new aspire-starter -n DaprDemo -o DaprDemo
```

-n　はプロジェクト名、 -o は出力先のディレクトリを指定しています。ディレクトリがない場合は作成されます。どちらも好きに変更してください。

Visual Studio Code で開くと次のように4つのプロジェクトがあることがわかります。（※右側のソリューションエクスプローラは C# DevKit 使用時のみ表示）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/6d346b36-456a-b99e-3d8c-b550b9648611.png)

# Service Invocation してみる

Web と WebApi の各プロジェクトついて、それぞれで Dapr の Sidecar に相当する Dapr プロセスを稼働して、Web から WebApi の呼び出し時に Dapr を使用して通信させてみます。

この図の Service A が Web プロジェクトで、 Service B が WebApi プロジェクトです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e1a40418-6c59-f184-bb0f-faade31fb9a4.png)

ローカル環境で Dapr の Sidecar に相当するプロセスをこの図のように　Web, WebApi のそれぞれ用に稼働させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/bbfd5a66-0acf-e745-586f-88da90736611.png)

## Web プロジェクト用の Dapr プロセスを起動する

Web プロジェクト（Service A） の Proxy として動作する Dapr プロセスはポート番号 3500 で http リクエストを待ち受けることにします。つまり Web プロジェクト（Service A） は localhost:3500 に対してアクセスすることで Dapr と通信します。この 3500 ポートは Dapr Sidecar の既定の ポート番号です。

次のコマンドを Terminal または PowerShell などで実行します。

```bash
dapr run --app-id web --dapr-http-port 3500  --dapr-grpc-port 50001
```

Dapr Sidecar プロセスには明示的に名前をつける必要があります。--app-id オプションで指定した web がその名前です。
--dapr-http-port オプションで 3500 を指定します。もう１つのオプションの --dapr-grpc-port は、Dapr Sidecar 間の通信で使用される gRPC のポート番号です。この gRPC での通信はサービス呼び出し時に使うこともできます。50001 は gRPC 通信時の既定のポート番号です。

## WebApi プロジェクト用の Dapr プロセスを起動する

WebApi プロジェクト（Service B)の ReverseProxy として動作する Dapr プロセスはポート番号 3501 で http 通信を、50002 で gRPC 通信することにします。また、ReverseProxy ですから着信したトラフィックを流す先として WebApi プロジェクトのポート番号も必要です。図で Port:5092 となっている箇所です。もちろん、作成した環境に応じてこのポート番号は修正する必要があります。

ポート番号の確認は、WebApi プロジェクトの Properties/launchSettings.json を開き、applicationUrlに記載された Url のポート番号を見ます。

Web プロジェクト用の dapr run コマンドは実行状態のままにして、新しい別の Terminal または Powershell で次のコマンドを実行します。

```bash
dapr run --app-id api --app-port 5092 --dapr-http-port 3501  --dapr-grpc-port 50002
```

この Dapr Sidecar プロセスには　--app-id オプションで api と名付けています。この次に Web から WebApi にアクセする実装をする時、この api という名前を使用して WebApi にアクセスしますので、名前はとても重要です。

## Dapr で通信するように Web プロジェクトを修正

Dapr Sidecar へのアクセスは localhost:xxxx に対して Http リクエストを投げることで実現できます。できますが、いろんな言語ごとに SDK が準備されていますのでそれを利用した方が圧倒的に簡単です。

Web プロジェクトを右クリックして Nuget パッケージの管理を選択し、Nuget ダイアログを表示します。検索ボックスに「Dapr.AspNetCore」と入力すると先頭に Dapr の ASP.NET Core 用の SDK が表示されます。選択してインストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/0657a6df-f6f2-6aa0-68c4-d8e646339e23.png)

MacOS、または Visual Studio 以外の環境の場合は次のコマンドを Terminal または PowerShell で実行します。

```bash
dotnet add package Dapr.AspNetCore --version 1.13.1
```

SDK を使う場合、SDK　が提供する DaprClient オブジェクトを使用します。Web プロジェクトの Program.cs を開いて、次のように修正します。

```diff_c_sharp:Web/Program.cs
+ builder.Services.AddDaprClient();
+ builder.Services.AddSingleton<WeatherApiClient>();

- builder.Services.AddHttpClient<WeatherApiClient>(client =>
-     {
-         // This URL uses "https+http://" to indicate HTTPS is preferred over HTTP.
-         // Learn more about service discovery scheme resolution at https://aka.ms/dotnet/sdschemes.
-         client.BaseAddress = new("https+http://apiservice");
-     });
```

HttpClient を使用したアクセスは不要になりましたのでコメントアウトしています。代わりに WeatherApiClient を DI に登録しています。

続いてその WeathreApiClient で DaprClient を使用するように修正します。コンストラクタで DaprClient オブジェクトを受け取り、その DaprClient オブジェクトを使用して WebApi にアクセスします。アクセスする時、 WebApi の Dapr Sidecar プロセスに付けた api という名前を使用して Dapr Sidecar に対してアクセスしていることに注目してください。

```diff_c_sharp:Web/WeatherApiClient
+ using Dapr.Client;
namespace DaprDemo.Web;

- public class WeatherApiClient(HttpClient httpClient)
+ public class WeatherApiClient(DaprClient daprClient)
{
    public async Task<WeatherForecast[]> GetWeatherAsync(int maxItems = 10, CancellationToken cancellationToken = default)
    {
"api", "weatherforecast");
-        List<WeatherForecast>? forecasts = null;
-
-        await foreach (var forecast in httpClient.GetFromJsonAsAsyncEnumerable<WeatherForecast>("/weatherforecast", cancellationToken))
-        {
-            if (forecasts?.Count >= maxItems)
-            {
-                break;
-            }
-            if (forecast is not null)
-            {
-                forecasts ??= [];
-                forecasts.Add(forecast);
-            }
-        }
-
-        return forecasts?.ToArray() ?? [];
+        return await daprClient.InvokeMethodAsync<WeatherForecast[]>(HttpMethod.Get, 
    }
}

public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
```

修正が終わったら実行します。Weather ページを開くと、問題なくデータが取得できて表示されるはずです。見た目からはわかりませんが、データが表示されたということは Dapr Sidecar プロセスを経由してデータが取得できています。

:::note info
Dapr の InvokeMethod はまだ IAsyncEnumerable を実装したクラスを返却するメソッドの提供がありません。そのため、元の実装のように1件ずつ非同期に取得しながら取得数をカウントする実装ではなく、全件取得する実装になっています。
:::

本当に Dapr Sidecar プロセスを経由しているのかとても気になりますね。確認しましょう。
ここで突然ですが、 http://localhost:9411 にアクセスしてみましょう。このような画面が出てくるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/8f119594-7c80-cf94-ac98-a6dd55cdc806.png)

dapr init コマンドを使って一番最初に Dapr 環境を作成したとき、 ローカル環境に 3つのコンテナが立ち上がりましたが、そのうちの１つは Zipkin コンテナです。Zipkin は分散トレースを視覚化してくれる Web アプリケーションです。Dapr アプリケーションを稼働すると、既定の設定では Zipkin にログが送信されるのでこのようにトレース情報を見ることができます。右側の SHOW ボタンをクリックすると、Dapr Sidecar　プロセスの web から api にリクエストされたことがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/f0779b3a-d744-5b0c-99e4-48ef2150fa9f.png)

Dapr をローカルで稼働している場合にトレース情報を確認するには Zipkin を見るのが既定の方法ですが、今回は .NET Aspire で稼働していますので、分散トレースを見る場合は .NET Aspire ダッシュボードで確認することができますし、そのほうが早いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/d2047ea8-f989-53f1-58c0-a4c607a64b78.png)

# .NET Aspire の Dapr サポートを使う

Dapr の挙動を理解するためには１つひとつ手を動かして確認するのは大事ですが、実際に開発する場合、事前に Dapr Sidecar プロセスを動かしておかなければいけないのは、正直手間です。ReverseProxy として稼働させる必要がある Dapr Sidecar プロセスを動かす場合は呼び出されるアプリケーションのポート番号を調べる必要もあります。このような環境のセットアップに煩わされないようするために .NET Aspire は開発されました。もちろん Dapr サポートもありますので使ってみましょう。

.NET Aspire についてはこちらをご覧ください。

- [.NET Aspire って何？ - 概要](https://qiita.com/takashiuesaka/items/f45e930ef296c5710acc)
- [.NET Aspire を使ってみる](https://qiita.com/takashiuesaka/items/8794f99b0bf3ce3c3106)
- [.NET Aspire を デプロイする](https://qiita.com/takashiuesaka/items/3da744d0511f29270299)
- [.NET Aspire で Prometheus, Jaeger, Grafana を使う](https://qiita.com/takashiuesaka/items/4811e62aa52e39ed834c)
- [Next.js + ASP.NET Core を .NET Aspire で構成する（with YARP）](https://qiita.com/takashiuesaka/items/e167852af299a7b00939)
- [.NET Aspire でデータベースを扱う - PostgreSQL編](https://qiita.com/takashiuesaka/items/137cb1450c3d9fbc8d1e)
- [.NET Aspire でデータベースを扱う - SQL Server編](https://qiita.com/takashiuesaka/items/6d5ec4d4994649c5dca5)
- [.NET Aspire のダッシュボードを単独で使う](https://qiita.com/takashiuesaka/items/338ae64ca2033e078f7f)

<hr />

まず、事前に立ち上げておいた2つの Dapr Sidecar プロセスは CTRL+C を押下して停止しておいてください。

AppHost プロジェクトを右クリックし、追加→.NET Aspire パッケージ を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/7a309231-156b-395a-c97c-f9ce1b388965.png)

検索ボックスに「owner:Aspire tags:component」と入力済みの状態で Nuget パッケージマネージャーが立ち上がります。この検索条件を最初の owner:Aspire だけを残して tags:component を削除した後、「owner:Aspire Dapr」に修正します。すると、.NET Aspire が Dapr を扱うために開発されたライブラリが表示されますのでインストールします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/2bee3882-c8ac-63c1-a2ff-5142d44a3b09.png)

AppHost プロジェクトの Program.cs を開いて、Dapr 対応をします。

```diff_c_sharp:AppHost/Program.cs
- var apiService = builder.AddProject<Projects.DaprDemo_ApiService>("apiservice");
+ var apiService = builder.AddProject<Projects.DaprDemo_ApiService>("apiservice")
+     .WithDaprSidecar("api");

builder.AddProject<Projects.DaprDemo_Web>("webfrontend")
    .WithExternalHttpEndpoints()
-   .WithReference(apiService);
+   .WithDaprSidecar("web");
```

差分で見るとちょっとわかりにくいですが、WithDaprSidecar メソッドを追加しているだけです。Dapr Sidecar に付ける名前である appid を明示的に指定するために引数に "api" と "web" を引数にセットしています。

Web プロジェクトも WebApi プロジェクトも Dapr Sidecar を使用することだけを指定すれば(WithDaprSidecarメソッドを使えば)、やり取りは Dapr Sidecar プロセスとしか行いませんので、WithReference メソッドによる参照関係の構築は不要になるため、削除しています。

では起動してみましょう。ダッシュボードが立ち上がります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/e57bfad6-d1aa-48b4-5d24-7787005faef6.png)

Dapr Sidecar プロセスも.NET Aspire が立ち上げてくれていることがわかります。アプリケーションのポート番号も自動的に渡されていますし、http と gRPC のポート番号も重複しないようにランダムで自動生成したものが使用されています。

トレースを見ても、間違いなく Dapr Sidecar プロセス越しに　WebApi にアクセスしていることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/9baed0c6-a049-2fe3-3034-1b26031fb6c2.png)

Dapr Sidecar プロセスを自分で稼働することなく、全体が稼働するようになりました。C# での開発の場合は .NET Aspire を使用することを強くお勧めします。

# dapr init コマンドは何をしているのか

ローカル環境で Dapr Sidecar プロセスを稼働するためには、 dapr init コマンドを実行して環境を作成しておく必要があります。このコマンドを実行すると、3つのコンテナが稼働することは既にご紹介しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/394e1f28-2aad-4be2-b60a-09ed7f12cc91.png)

この3つのコンテナはなんでしょうか。実は Dapr を動かす場合に必須なコンテナは **dapr_placement** コンテナだけです。**dapr_redis** はその名の通り Redis のコンテナで、**dapr_zipkin** は Zipkin コンテナですが、この２つはオプションです。dapr init コマンドで作成された既定の設定でこの２つを使用するようになっているため、Image を Pull して起動しています。

その既定の設定ですが、次の場所にファイルが作成されています。

- Windows の場合
    - %userprofile%\\.dapr

- MacOS の場合
    - $HOME/.dapr

%userprofile% とは、通常は C:\Users\<ユーザー名> フォルダのことです。

ここに config.yaml と components フォルダがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/2a9ba05b-fd7b-33b1-1490-822c1f562c57.png)

この config.yaml には　Dapr 全体の設定を記載します。例えば Log, Metrics, Trace の設定や、Dapr Sidecar の中で処理パイプラインに挿入できる Middleware のセットアップ、Serivce Invocation 時の細やかなアクセス制御などを構成できます。

dapr init 実行時に作成される config.yaml では、次のような設定がされています。

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

これは Zipkin に Trace を出力する設定です。そのため、Zipkin コンテナが必要でした。次に components フォルダの中を見てみると、ファイルが2つあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/222127/0157ce1c-dce3-eae7-4890-96717148664b.png)

components フォルダの中には、使用したい Building Block について設定ファイルを格納します。既定では PubSub と State management 用の設定が作成されます。

pubsub.yaml の中を見てみると、pubsub.redis という記載があります。これは　PubSub を使う時の情報の格納先として Redis Stream を使うことを意味しています。だから Redis コンテナが自動的に起動するように dapr init コマンドはセットアップしたのです。

```yaml:pubsub.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: pubsub
spec:
  type: pubsub.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
```

statestore.yaml の中を見てみると、state.redis という記載があることから、　State management を使う時の情報の格納先としてこちらも Redis コンテナを使うことがわかります。

```yaml:statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""
  - name: actorStateStore
    value: "true"
```

これらの yaml ファイルは自由にカスタマイズ可能です。別のリソースを使用することももちろんできますし、Middleware の挙動をテストする、なんてことができます。
注意したいのは、次のコマンドを実行するとコンテナはもちろん、これらの構成ファイルは .dapr フォルダ毎全て消えてしまいます。

```bash
dapr uninstall --all
```

--all オプションをつけなければ、構成ファイルは残ります。ただし、オプションで起動している Recdis と Zipkin コンテナはそのまま残りますので、不要であれば削除が必要です。

## Dapr を構成する yaml の参考情報

全体構成の詳細についてはこちらをご確認ください。
[Overview of Dapr configuration options](https://docs.dapr.io/operations/configuration/configuration-overview/)

PubSub, State management 構成についてはこちらをご確認ください。各リソースのリンクをクリックすると、そのリソース用の yaml 構成についての詳細が記載されています。

[Pub/sub brokers](https://docs.dapr.io/reference/components-reference/supported-pubsub/)
[State stores](https://docs.dapr.io/reference/components-reference/supported-state-stores/)

# まとめ

Service Invocation を使うことで Dapr の仕組みをご紹介しました。.NET Aspire を使うととても楽に Dapr を使用するシステムの開発ができます。C# を採用したシステム開発の場合は是非積極的に活用してください。