daprでつくるマイクロサービス

## はじめに

この記事は、 [富士通クラウドテクノロジーズ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/fjct) の**2日目**の記事です。

1日目は @miyuush さんの **[ニフクラがTerraformに対応したので使ってみた【基礎編】](https://blog.pfs.nifcloud.com/20201201_terraform_provider_nifcloud)** でした！
昨日リリースされたばかりで生まれたてホヤホヤ感のある [nifcloud/terraform-provider-nifcloud](https://github.com/nifcloud/terraform-provider-nifcloud) v1.0.0 ですがこれからの機能エンハンスが楽しみですね！どんどんIaCにしていきたい:yum:


改めましてこんにちは！NIFCLOUDのいくつかのサービスのAPIを開発している @kzmake と申します。
入社しサービスを開発してはや4年目になりました。ここ数年はいくつかのサービス開発を経験し、そこそこcleanでdddなアプリケーションをかけるようになってきたました :sunglasses: 
最近はどうすればスピード感ある開発ができるかな〜と考えている今日このごろです。

今日は自分が使ってみたいなぁ〜と感じている 

1. **[github.com/dapr/dapr] について**
2. **daprを使ったマイクロサービスアプリケーション実装**

を紹介したいと思います！

## [dapr] とは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48903/b894075e-d0e7-9c20-3c99-d2f90b67ccd8.png)

[dapr] は、 **Distributed Application Runtime** という名のとおりマイクロサービスアプリケーションとして必要な機能をビルディングブロックとして提供してくれるランタイムです。stable はまだ [v0.11.3](https://github.com/dapr/dapr/tags) と比較的若いながら、[★8.4k](https://star-history.t9t.io/#dapr/dapr) とかなりホット :fire: な OSSプロジェクトではないかなとおもっています。そのコンセプトは、**Any language, any framework, anywhere** としており多様性をもった利用ができるところもポイントですね。

本来実装したいコアロジックにサイドカーとして利用することで、簡単にマイクロサービスを作成することができます。
更にそれぞれのビルディングブロックは抽象化されており、 HTTP/gRPC API を通して利用するものとなっているため言語に縛られない開発ができるのも魅力となっています。

### マイクロサービスのためのビルディングブロック

[dapr] が現在(2020/11/29)提供しているビルディングブロックには下記のものがあります。

![dapr_bilding_blocks.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48903/0b092a10-216d-dfd6-77d5-83614ae91ef9.png)

- [Service-to-service invocation]: `/v1.0/invoke`
    - 他のマイクロサービスサービスへ通信するための機能
- [State management]: `/v1.0/state`
    - key/valueベースの永続化や参照機能
- [Publish and subscribe]: `/v1.0/publish` and `/v1.0/subscribe`
    - Publish/subscribeモデルで非同期にメッセージを送受信する機能
- [Resource bindings]: `/v1.0/bindings`
    - 外部コンポーネントやサービスを抽象化しイベントの送受信を行う機能
- [Actors]: `/v1.0/actors`
    - 分散性や並行・並列性をもち、非同期なメッセージ駆動のアクターモデルを提供
- [Observability]
    - ログ・トレース・メトリクス・ヘルスチェックといったオブザーバビリティに必要な要素を提供
- [Secrets]: `/v1.0/secrets`
    - 安全にパスワードなどのクレデンシャルなデータにアクセスする機能

それぞれのコンポーネントはライブラリとしてアプリケーションに組み込むのではなく、yamlのコンポーネント定義ファイルをロードさせることで利用することができます。

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

ビルディングブロックとしての提供なので、実装に一切手を加えず、**検証環境ではredis、本番環境では何かしらのクラウドサービス**など切り替えもできるのがいいですね:tada:


### ミドルウェア(http)

ミドルウェアも各種ビルディングブロックと同様にコンポーネントを定義することで利用可能となっています。

```yaml
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: pipeline
  namespace: default
spec:
  httpPipeline:
    handlers:
    - name: oauth2
      type: middleware.http.oauth2
    - name: uppercase
      type: middleware.http.uppercase
```

特徴的なのが、 `ratelimit` や `oauth2` など定義済みのミドルウェアだけでなく、 [Open Policy Agent（OPA)](https://www.openpolicyagent.org/) を用いてミドルウェアを追加できるようです( ex: https://play.openpolicyagent.org/p/oRIDSo6OwE )。提供されているミドルウェアは https://github.com/dapr/components-contrib/blob/master/middleware/http で確認できます。

### サポートしているSDK

daprが提供する HTTP/gRPC API にアクセスするためのsdkを利用することもできます。提供されているSDKは、

- [java-sdk](https://github.com/dapr/java-sdk)
- [python-sdk](https://github.com/dapr/python-sdk)
- [dotnet-sdk](https://github.com/dapr/dotnet-sdk)
- [js-sdk](https://github.com/dapr/js-sdk)
- [go-sdk](https://github.com/dapr/go-sdk)
- [cpp-sdk](https://github.com/dapr/cpp-sdk)
- WIP: [rust-sdk](https://github.com/dapr/rust-sdk)
 

となってます。今後の[ロードマップ](https://github.com/dapr/dapr/wiki/Roadmap#sdks-and-frameworks)はこんな感じらしい。

### ダッシュボード

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48903/3a30a9b5-6009-d9e9-6988-12afc9b996a1.png)

まだ機能は少ないですが、ダッシュボードも利用できるようです。今後の機能追加も期待ですね！

### [IDEのサポート](https://docs.dapr.io/developing-applications/ides/)

言語サポートだけでなく、IDEのサポートもあるみたいです:tada:

- [Visual Studio Code integrations with Dapr](https://docs.dapr.io/developing-applications/ides/vscode/)

## んでなにを作ったの？

`time.Now()` など現在時刻を直接取得せず、マイクロサービスでシンプルな 
**時計サービス:timer: : [github.com/kzmake/dapr-clock]**
をつくってみました(同期にこのアイデアをいただきました)！

```bash
$ docker-compose up -d --build
$ docker-compose exec dev curl -s http://web/now | jq .
{
  "hour": 15,
  "minute": 33,
  "second": 25
}
```

上記のコードは、時計をざっくりとモデリングし…

- **clock**: 現在時刻を取得するサービス
    - `/v1.0/clock/invoke/now` で時刻を取得するAPIを提供
        - `hour-hand/minute-hand/second-hand` から針の情報を取得する
- **hour-hand**: 時針を管理するサービス
    - `時刻同期のイベント` に時針を設定する
    - `60分間経過のイベント` を元に分針を `運針する`
    - 時針を永続化する
    - `23` → `0` になると `24時間経過のイベント` を発行する
- **minute-hand**: 分針を管理するサービス
    - `時刻同期のイベント` に分針を設定する
    - `60秒間経過のイベント` を元に分針を `運針する`
    - 分針を永続化する
    - `59` → `0` になると `60分間経過のイベント` を発行する
- **second-hand**: 秒針を管理するサービス
    - `時刻同期のイベント` に秒針を設定する
    - `1秒間経過のイベント` を元に秒針を `運針する`
    - 秒針を永続化する
    - `59` → `0` になると `60秒間経過のイベント` を発行する
- **ticker**: 1秒をカウントするサービス
    - `一定間隔(1sec)毎` に `1秒間経過のイベント` を発行する
- **synchronizer**: NTPを用いて時刻を同期するサービス
    - `一定間隔(24h)毎` に `時刻同期のイベント` を発行する

をマイクロサービスとして設計しています。

### アーキテクチャ
daprを利用して以下のような設計してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48903/a3655649-627a-afae-3f3e-64dfab831308.png)

今回は、

- [Service-to-service invocation]
    - `clock`サービスから`second-hand/minute-hand/hour-hand`の呼び出し
- [Publish and subscribe]
    - cronを使って `ticker`サービス / `synchronizer`サービス を一定間隔毎にトリガー
- [Resource bindings]
    - redis経由で各イベントを通知
- [State management]
    - 時針・分針・秒針の情報を永続化
 
を利用してみようとおもいます。

## [github.com/kzmake/dapr-clock]

[github.com/dapr/go-sdk] を利用してdaprサイドカーと通信するマイクロサービスアプリケーションを [github.com/kzmake/dapr-clock] の `/microservices` に各マイクロサービスを作成しました。各機能についてdaprでどう実装できるかを紹介していきたいと思います。

### 現在時刻を取得する機能

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48903/11adce1d-cc8d-b493-e7bf-f91d96d7117a.png)


ユーザーは`clock`サービスを通して`second-hand/minute-hand/hour-hand`サービスが管理する時針・分針・秒針を取得するとします。
また、`second-hand/minute-hand/hour-hand`はそれぞれの針の状態を時針で持ちたくないのでdaprが提供するストア機能で永続化も試みます。ここでdaprのビルディングブロックとしては、

- [Service-to-service invocation]
- [State management]

を利用し、dapr上で`POST /v1.0/invoke/clock/method/now`のAPIを提供します。まず、APIリクエストを受け取る部分から記述していきます。今回は`アプリケーション <--> dapr`間でgrpcを利用したかったので`github.com/dapr/go-sdk/service/grpc`を使っています。

#### [Service-to-service invocation]

下記は`clock`サービスの実装ですが、`second-hand/minute-hand/hour-hand` も呼び出す handler が違うだけで差分はありません。`second-hand/minute-hand/hour-hand`も同じように`AddServiceInvocationHandler` を使って `"now"` のAPIを追加しています。

```golang
import (
	daprd "github.com/dapr/go-sdk/service/grpc"

	"github.com/kzmake/dapr-clock/microservices/clock/handler"
)

func main() {
	s, err := daprd.NewService(":3000")
	if err != nil {
		log.Fatalf("failed to start the server: %+v", err)
	}

	// POST /v1.0/invoke/clock/method/now
	if err := s.AddServiceInvocationHandler("now", handler.Now); err != nil {
		log.Fatalf("error adding invocation handler: %+v", err)
	}

	if err := s.Start(); err != nil {
		log.Fatalf("server error: %+v", err)
	}
}
```

`clock`サービスのhandlerは、`second-hand/minute-hand/hour-hand`のAPIをリクエストするため、daprのビルディングブロックである [Service-to-service invocation] を利用します。`client.InvokeService`を利用している部分がそれにあたりますが、daprが提供する固定のエンドポイントを利用することでサービスディスカバリーを実装する必要がないようになっています！

```golang
func Now(ctx context.Context, in *common.InvocationEvent) (*common.Content, error) {
	client, err := dapr.NewClient()
	if err != nil {
		return nil, err
	}

	// POST /v1.0/invoke/hour-hand/method/now
	hourHandRes, err := client.InvokeService(ctx, "hour-hand", "now")
	if err != nil {
		return nil, err
	}

	// POST /v1.0/invoke/minute-hand/method/now
	minuteHandRes, err := client.InvokeService(ctx, "minute-hand", "now")
	if err != nil {
		return nil, err
	}

	// POST /v1.0/invoke/second-hand/method/now
	secondHandRes, err := client.InvokeService(ctx, "second-hand", "now")
	if err != nil {
		return nil, err
	}

	// ...それぞれのレスポンスの json.Unmarshal 処理してレスポンスを作成するなど

	return &common.Content{ContentType: "application/json", Data: res}, nil
}
```

#### [State management]

上記のリクエストを `second-hand/minute-hand/hour-hand`サービスで受け付け、 永続化されている針を取得していきます。[State management] では、コンポーネントとしてyamlで定義した`statestore`を利用していきます。[バックエンドとして多くのものをサポート](https://docs.dapr.io/operations/components/setup-state-store/supported-state-stores/)していますが、今回はredisを使用しています。

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

[State management] では、`metadata`で設定した`name`や`namespace`を元にコード内で永続・参照先を指定します。今回の例では`statestore`を指定します。参照のみなので、`client.GetState(ctx, "statestore", "hour")` のようにデータを取得します。
`hour-hand`サービスの`POST /v1.0/invoke/hour-hand/method/now`を処理するハンドラーとしては下記のようになります。

```golang
import (
	dapr "github.com/dapr/go-sdk/client"
	"github.com/dapr/go-sdk/service/common"
)

func Now(ctx context.Context, in *common.InvocationEvent) (*common.Content, error) {
	client, err := dapr.NewClient()
	if err != nil {
		return nil, err
	}

	item, err := client.GetState(ctx, "statestore", "hour")
	if err != nil {
		return nil, err
	}

	// ...レスポンスを生成

	return &common.Content{ContentType: "application/json", Data: res}, nil
}
```

### 一定間隔毎に針を運針する機能

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48903/11c02e03-3f74-290e-4bdd-6068904124c4.png)

[Resource bindings]でサポートしている`cron`を利用して一定間隔でスケジューリングできるトリガーをもとに、`ticker`サービスでイベントを発行し、`second-hand/minute-hand/hour-hand`サービスの運針(データを永続化)を実現します。tickerからブロードキャストするのではなく、
```
ticker --[Ticked]--> second-hand --[60sTicked]--> minutes-hand --[60mTicked]--> hour-hand
```
とtickerのイベントをトリガーに各サービスもイベントを発行するようにしています。daprのビルディングブロックとしては、

- [Publish and subscribe]
- [Resource bindings]
- [State management]

を使いました。

#### [Resource bindings]

まずは`ticker`サービスのトリガーとなる`input bindings`から見ていきます。

```yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: ticker
spec:
  type: bindings.cron
  metadata:
    - name: schedule
      value: "@every 1s"
```

と1sec毎に`ticker`サービスの処理を行うためのトリガーを設定します。[Service-to-service invocation]と同様に、ここでは`ticker`コンポーネントと`handler.Tick`ハンドラーを結びつけるため、`s.AddBindingInvocationHandler("ticker", handler.Tick)` を行います。

```golang
import (
	daprd "github.com/dapr/go-sdk/service/grpc"

	"github.com/kzmake/dapr-clock/microservices/ticker/handler"
)

func main() {
	s, err := daprd.NewService(":3001")
	if err != nil {
		log.Fatalf("failed to start the server: %+v", err)
	}

	if err := s.AddBindingInvocationHandler("ticker", handler.Tick); err != nil {
		log.Fatalf("error adding binding handler: %+v", err)
	}

	if err := s.Start(); err != nil {
		log.Fatalf("server error: %+v", err)
	}
}
```

このようにするだけで、外部コンポーネントをトリガーとして処理を行うサービスを作成できます。

#### [Publish and subscribe]

次はマイクロサービスの肝となるPublish/Subcribeをdaprを使って実装していきます。`pubsub`コンポーネントの定義し、

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

コードではコンポーネントのname/namespaceを指定して`client.PublishEvent(ctx, "pubsub", "Ticked", nil)` を使うことでメッセージのPublishを実装します。

```golang
func Tick(ctx context.Context, in *common.BindingEvent) ([]byte, error) {
	client, err := dapr.NewClient()
	if err != nil {
		return nil, err
	}

	if err := client.PublishEvent(ctx, "pubsub", "Ticked", nil); err != nil {
		return nil, err
	}

	return nil, nil
}
```

`second-hand`サービスのSubscribeは、`AddTopicEventHandler` を使ってSubscribeハンドラーを登録します。

```golang
import (
	"github.com/dapr/go-sdk/service/common"
	daprd "github.com/dapr/go-sdk/service/grpc"

	"github.com/kzmake/dapr-clock/microservices/second-hand/handler"
)

func main() {
	s, err := daprd.NewService(":3000")
	if err != nil {
		log.Fatalf("failed to start the server: %+v", err)
	}

	// ...invokeなど

	if err := s.AddTopicEventHandler(&common.Subscription{
		PubsubName: "pubsub",
		Topic:      "Ticked",
		Route:      "/increase",
	}, handler.Increase); err != nil {
		log.Fatalf("error adding event handler: %+v", err)
	}

	if err := s.Start(); err != nil {
		log.Fatalf("server error: %+v", err)
	}
}
```

あとは、イベントを受信した際にdaprストア機能で秒針の情報を取得し、1sec分運針した後、永続化します。

```golang
func Increase(ctx context.Context, e *common.TopicEvent) (bool, error) {
	client, err := dapr.NewClient()
	if err != nil {
		return false, err
	}

	item, err := client.GetState(ctx, "statestore", "second")

	// ...itemからsecを取得

	// 59 -> 0 への運針であれば新規にイベント発行
	if (sec+1)/60 == 1 {
		if err := client.PublishEvent(ctx, "pubsub", "Ticked.60s", nil); err != nil {
			return 0, err
		}
	}

	// ...1sec運針する処理

	if err := client.SaveState(ctx, "statestore", "second", []byte(fmt.Sprintf("%d", sec))); err != nil {
		return false, err
	}

	return false, nil
}
```

`59 -> 0` へと秒針が一周する場合は`"Ticked.60s"`をPublishして `minute-hand/hour-hand`にイベントを渡していくようにしてみました。今回はこのようにサービス毎のイベントをPub/Subすることで時針・分針・秒針の運針をdaprを使って実装してみました。

### 現在時刻を同期する機能

最後に電波時計的な機能を実装しようと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/48903/db5dd1dc-233f-42dd-9d05-993d2da00c88.png)


先程と基本的には同じビルディングブロックを利用し、`cron`を利用して一定間隔でスケジューリングできるトリガーをもとに、`synchronizer`サービスでNTPより取得した現在時刻の同期イベントを発行し、`second-hand/minute-hand/hour-hand`サービスの時刻同期を実現します。今回は`synchronizer`からブロードキャストし、ペイロードにjsonを渡すことで`second-hand/minute-hand/hour-hand`の時刻を設定します。ここも [dapr] のビルディングブロックとしては、

- [Publish and subscribe]
- [Resource bindings]
- [State management]

を使いました。新規に下記の`synchronizer`コンポーネントを定義し、`1日毎`に現在時刻の同期を試みます。

```yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: synchronizer
spec:
  type: bindings.cron
  metadata:
    - name: schedule
      value: "@daily"

```

#### [Publish and subscribe]

[Resource bindings] を利用してトリガーとしてコンポーネントを登録しつつ、 [Publish and subscribe] でペイロードを持つイベントを発行してみます。Publish部分では NTP より取得した時刻情報をペイロードに設定してPublishさせます。

```golang
func Synchronize(ctx context.Context, in *common.BindingEvent) ([]byte, error) {
	client, err := dapr.NewClient()
	if err != nil {
		return nil, err
	}

	time, err := ntp.Time("ntp server address...")
	if err != nil {
		return nil, err
	}

	payload, err := json.Marshal(map[string]interface{}{"hour": time.Hour(), "minute": time.Minute(), "second": time.Second()})
	if err != nil {
		return nil, err
	}

	if err := client.PublishEvent(ctx, "pubsub", "Synchronized", payload); err != nil {
		return nil, err
	}

	return nil, nil
}
```

後は`second-hand/minute-hand/hour-hand`でイベントをSubscribeし、時刻同期を行うといった実装になります。

以上で時計サービスとして、

- 現在時刻を取得する機能
- 一定間隔で秒針・分針・時針を運針する機能
- 現在時刻を同期する機能

と最低限の機能を [dapr] + マイクロサービス として実現できたかなと思います！

## さいごに

どうでしたでしょうか！まだまだ成長途中なOSSかと思いますが、コンポーネントとしてインフラの差し替え可能だったり、ビルディングブロックが豊富だったりと [dapr] に乗っかることでコアロジックに集中できそうだなと改めて思いました :smile: 

[github.com/kzmake/dapr-clock] は6マイクロサービスを組み合わせてノリで作ってみましたが、設計＆開発には1日もかかりませんでした :sunglasses:  これなら開発効率ももりもり上げていけそうですね :muscle: (v1.0 が待ち遠しい…)

ニフクラのサービスとして提供している [hatoba](https://pfs.nifcloud.com/service/hatoba.htm) 上で [dapr] を動かしたり、マルチクラウドに利用しても面白いかもしれないですね！

今回紹介しきれなかったものに、

- [Actors]
- [Observability]
- [Secrets]

があるんですが、また別の機会で紹介しようと思います。([github.com/dapr/go-sdk]はまだactorを利用できないようでした…残念… [#21](https://github.com/dapr/go-sdk/issues/21))

さて、明日は @yaaamaaaguuu さんが **[VMware製品を気軽に検証するためのtips](https://yaaamaaaguuu.hatenablog.com/entry/2020/12/03/042231?_ga=2.152422970.2060075168.1606930021-1363148790.1515407700)** について書いてくれるようです！お楽しみに:yum:

## 参考文献

この記事は以下の情報を参考にしています。

- [Dapr - portable, event-driven, serverless runtime.](https://dapr.io/)
- [github.com/dapr/docs](https://github.com/dapr/docs)
- [github.com/dapr/dapr](https://github.com/dapr/dapr)
- [github.com/dapr/samples](https://github.com/dapr/samples)


[dapr]: https://github.com/dapr/dapr
[dapr/dapr]: https://github.com/dapr/dapr
[github.com/dapr/dapr]: https://github.com/dapr/dapr
[Service-to-service invocation]: https://docs.dapr.io/developing-applications/building-blocks/service-invocation/service-invocation-overview/
[State management]: https://docs.dapr.io/developing-applications/building-blocks/state-management/state-management-overview/
[Publish and subscribe]: https://docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-overview/
[Resource bindings]: https://docs.dapr.io/developing-applications/building-blocks/bindings/bindings-overview/
[Actors]: https://docs.dapr.io/developing-applications/building-blocks/actors/actors-overview/
[Secrets]: https://docs.dapr.io/developing-applications/building-blocks/secrets/secrets-overview/
[Observability]: https://docs.dapr.io/developing-applications/building-blocks/observability/
[github.com/kzmake/dapr-clock]: https://github.com/kzmake/dapr-clock
[github.com/dapr/go-sdk]: https://github.com/dapr/go-sdk