💨

Dapr の動作をローカルと AKS で確認する

2023/07/23に公開1

分散アプリケーションランタイム「Dapr」について手元でワイワイ動作させてみました。

まえがき

[Dapr とは]
Dapr (Distributed Application Runtime)は、クラウドネイティブなマイクロサービス開発を簡素化するための、オープンソースのランタイムフレームワークです。Daprは、分散システムの要素(例えば、ステート管理、サービス間通信、イベント駆動アーキテクチャなど)を抽象化し、開発者がこれらの機能を直接コードに組み込むことなく、シンプルなAPI経由で使用できるようにします。Daprはプラットフォームに依存せず、どの言語やフレームワークでも使用できます。これにより、マイクロサービスの開発とデプロイを大幅に効率化できます。(by ChatGPT)

Dapr に関しての理解を深くしたいなと思っていたので、記事を書くことによって自ずと知識が深まるんじゃないかと期待してこの記事を書いてます。(浅い考え)
詳しくはこちらの記事にも書かれているのでご参考ください。

今回はローカル環境の Docker を使って動作を確認し、その後チュートリアルのアプリケーションを使って AKS に Dapr をインストールし、デプロイしていきます。

ローカル環境での確認

Dapr をインストール

まずは動作を手元で確認してみます。私の環境は M2 MacBook Air になります。Air いいですね、軽くて。
CLI のインストールから開始します。

% brew install dapr/tap/dapr-cli

アプリケーションの作成

手元での確認用にこのドキュメントをみて進めていましたが、せっかくなので言語を変えて Go で同じことをやってみたいと思います。
ちなみに ChatGPT 君に手助けしてもらいました。ありがとう、心の友。

Go で Dapr を使うので、go get しておきます。

% go get github.com/dapr/go-sdk/client

実際に作成したアプリケーションは以下となります。

package main

import (
	"context"
	"fmt"
	"log"
	"strconv"
	"time"

	dapr "github.com/dapr/go-sdk/client"
)

const (
	storeName = "statestore"
	key       = "counter"
)

func main() {
	client, err := dapr.NewClient()
	if err != nil {
		log.Fatalf("Failed to create Dapr client: %v", err)
	}
	defer client.Close()

	item, err := client.GetState(context.Background(), storeName, key, nil)
	if err != nil {
		log.Fatalf("Failed to get state: %v", err)
	}

	var counter int
	if item.Value != nil {
		counter, err = strconv.Atoi(string(item.Value))
		if err != nil {
			log.Fatalf("Failed to parse counter: %v", err)
		}
	}

	for {
		counter++
		fmt.Printf("Counter = %d\n", counter)

		err = client.SaveState(context.Background(), storeName, key, []byte(strconv.Itoa(counter)), nil)
		if err != nil {
			log.Fatalf("Failed to save state: %v", err)
		}

		time.Sleep(1 * time.Second)
	}
}

ついでに、先日使った Draft も使って Dockerfile も作ってもらいました。やはり便利です。

FROM golang:1.20
ENV PORT 80
EXPOSE 80

WORKDIR /go/src/app
COPY . .

RUN go mod vendor
RUN go build -v -o app
RUN mv ./app /go/bin/

CMD ["app"]

実行

では実行していきます。この時にちゃんと Docker 立ち上げておかないとエラーになります(当たり前のことですが、私は忘れていました)
まず初期化します。

% dapr init
⌛  Making the jump to hyperspace...
ℹ️  Container images will be pulled from Docker Hub
()
✅  Success! Dapr is up and running. To get started, go here: https://aka.ms/dapr-getting-started

「Success」と出ました。プリケーションを Dapr コマンドを使って実行します。

 % dapr run --app-id dapr-go go run main.go
ℹ️  Starting Dapr with id dapr-go. HTTP Port: 58112. gRPC Port: 58113
ℹ️  Checking if Dapr sidecar is listening on HTTP port 58112
()
ℹ️  Checking if Dapr sidecar is listening on GRPC port 58113
ℹ️  Dapr sidecar is up and running.
ℹ️  Updating metadata for appPID: 92690
ℹ️  Updating metadata for app command: go run main.go
✅  You're up and running! Both Dapr and your app logs will appear here.

== APP == dapr client initializing for: 127.0.0.1:58113
== APP == Counter = 1
== APP == Counter = 2
== APP == Counter = 3
== APP == Counter = 4
== APP == Counter = 5
== APP == Counter = 6
^Cℹ️  
()
== APP == signal: interrupt
✅  Exited Dapr successfully
✅  Exited App successfully

アプリケーションは以下のようなカウントをしています。

== APP == Counter = 1
== APP == Counter = 2
== APP == Counter = 3
== APP == Counter = 4
== APP == Counter = 5
== APP == Counter = 6

一度アプリケーションを止めてみます。その後再開すると、カウンターが再開することがわかります。

% dapr run --app-id dapr-go go run main.go
ℹ️  Starting Dapr with id dapr-go. HTTP Port: 58142. gRPC Port: 58143
ℹ️  Checking if Dapr sidecar is listening on HTTP port 58142
()
ℹ️  Updating metadata for app command: go run main.go
✅  You're up and running! Both Dapr and your app logs will appear here.

== APP == dapr client initializing for: 127.0.0.1:58143
== APP == Counter = 7
== APP == Counter = 8
== APP == Counter = 9
== APP == Counter = 10
^Cℹ️  
terminated signal received: shutting down
INFO[0005] Dapr shutting down                            app_id=dapr-go instance=MihonoMacBook-Air.local scope=dapr.runtime type=log ver=1.11.1
()
✅  Exited Dapr successfully
✅  Exited App successfully

続きの == APP == Counter = 7 からカウントされました。前の状態が保存されていますね。何が起きているのか動作を確認してみましょう。

ここ

ローカル環境用に Dapr を最初に初期化したときに、Redis コンテナーが自動的にプロビジョニングされています。 その後、Redis コンテナーは、Dapr により、statestore.yaml という名前のコンポーネント構成ファイルを使用して、既定の状態ストア コンポーネントとして構成されています。 その内容は次のようになります。

と記載されています。ほう...?では、Redis コンテナーが作成されているかを確認してみましょう。

Docker.png

(下の3つが自動的に作成されたもの)ここで状態が保存されていることがわかります。

コンポーネント構成ファイル

既定の場合は、以下のような構成ファイルで Kubernetes だと用意されているようです。

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"

ドキュメントを読んでいくと namespace で分けたりもできそうです。

必要な場合は、コンポーネントを特定のアプリケーションにさらに限定することもできます。 production 名前空間内で、Redis キャッシュへのアクセスを DaprCounter アプリケーションのみに制限することができます。

AKS で実装してみる。

動作がなんとなくわかったところで、AKS で実装してみます。今回使うのは以下のリポジトリになります。

参考にしたサイト:
https://learn.microsoft.com/ja-jp/azure/aks/quickstart-dapr?tabs=azure-cli

リポジトリ:
https://github.com/dapr/quickstarts/tree/master/tutorials/hello-kubernetes

Dapr.png
※上記のイメージはリポジトリから引用させていただきました。

状態ストアはさまざまなコンポーネントから選択できます。今回は Redisで作成します。
何はともあれまずは gitclone でサンプルアプリケーションを Clone します。

% git clone https://github.com/dapr/quickstarts.git
% cd quickstarts/tutorials/hello-kubernetes/

AKS の作成

AKS を作成します。

% myResourceGroup=<リソースグループ名>
% location=japaneast
% myAKSCluster=<AKS クラスター名>

% az group create --name $myResourceGroup --location $location
% az aks create -g $myResourceGroup -n $myAKSCluster --enable-managed-identity --node-count 3 --enable-addons monitoring --enable-msi-auth-for-monitoring  --generate-ssh-keys

作成後、AKS に接続します。

% az aks get-credentials --resource-group $myResourceGroup --name $myAKSCluster

接続できたことを確認します。

% kubectl get pods
No resources found in default namespace.

Dapr 拡張機能をクラスターに作成します。詳しくはこちらを参考にしてください。
拡張機能を作成して AKS クラスターに Dapr をインストールします。

% az k8s-extension create --cluster-type managedClusters \
--cluster-name $myAKSCluster \
--resource-group $myResourceGroup \
--name dapr \
--extension-type Microsoft.Dapr

うっかり見逃しがちですが、初めてインストールする場合はこちら N にしましょう。私は見逃しました。

Is Dapr already installed in the cluster? (y/N): N

これで準備は完了です。

Redis ストアの作成

Azure Portal から作成します。20分くらいかかりますので気長に待ちます。作成後、インスタンスのホスト名をメモします。

例:XXXXXX.redis.cache.windows.net

[設定]-[アクセスキー] から、アクセスキーをメモしておきます。
アクセスキーを先ほど作成した AKS に格納します。Kubernetes の Secret を作成し、そこに入れます。今回は検証のため、Secret を使用しましたが、Azure KeyVault に入れたいですね。

% kubectl create secret generic redis --from-literal=redis-password=<your-redis-password>

作成できていることを確認します。

% kubectl get secret
NAME    TYPE     DATA   AGE
redis   Opaque   1      6s

Dapr コンポーネントの構成

Clone した "hello-kubernetes" の中のディレクトリ "deploy" の中に YAML ファイルが複数あります。その中の "redis.yaml" を開いて編集します。

  • redisHost: メモしたホスト名 + ポート番号 (例:xxxxxxx.redis.cache.windows.net:6380)
  • redisPassword: Secret 名を redis-password から変更した場合は、key をその名前に変更
  • enableTLS: true を追加
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: xxxxxx.redis.cache.windows.net:6380
  - name: redisPassword
    secretKeyRef:
      name: redis
      key: redis-password
  - name: enableTLS
    value: true
auth:
  secretStore: kubernetes

構成を適用します。

% kubectl apply -f ./deploy/redis.yaml
component.dapr.io/default created

Dapr サイドカーを使用して Node.js アプリをデプロイ

今回はそのまま用意されているアプリを使います。まずは Node.js アプリをデプロイ

% kubectl apply -f ./deploy/node.yaml
service/nodeapp created
deployment.apps/nodeapp created

稼働を確認します。

% kubectl get pods
NAME                       READY   STATUS    RESTARTS   AGE
nodeapp-7bbf67cd77-dt46j   2/2     Running   0          67s

STATUS が Error になっていた場合は、正しく稼働していません。kubectl describe pod <Pod 名> を確認し以下のようなメッセージが出ていた場合は、Redis の接続がうまく行っていない場合があるので、設定を見直ししましょう。

  • Pod の状態
% kubectl get pod
NAME                       READY   STATUS   RESTARTS      AGE
nodeapp-7bbf67cd77-hwkcp   1/2     Error    4 (55s ago)   2m5s
  • エラーメッセージ
  Warning  Unhealthy  5s (x4 over 35s)   kubelet            Readiness probe failed: Get "http://10.244.2.11:3501/v1.0/healthz": dial tcp 10.244.2.11:3501: connect: connection refused
  Warning  Unhealthy  5s (x3 over 35s)   kubelet            Liveness probe failed: Get "http://10.244.2.11:3501/v1.0/healthz": dial tcp 10.244.2.11:3501: connect: connection refused

私は以下の点に引っかかりました。ちゃんとドキュメントは読みましょうということですね。

  • RedisHost の名前が設定されていなかった
  • port をちゃんと見てなかった xxxxxx.redis.cache.windows.net:6380

Pod が立ち上がったことを確認して、External-IP を確認します。

% kubectl get svc nodeapp
NAME      TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)        AGE
nodeapp   LoadBalancer   10.0.184.136   XX.44.185.69   80:31297/TCP   3m49s

確認

% curl XX.44.185.69/ports
{"DAPR_HTTP_PORT":"3500","DAPR_GRPC_PORT":"50001"}%

では、curl を使ってアプリケーションに命令をしてみます。この sample.json には、{"data":{"orderId":"42"}} が格納されています。

% curl --request POST --data "@sample.json" --header Content-Type:application/json $EXTERNAL_IP/neworder

curl を使用して、命令が保持されていることを確認します。

% curl $EXTERNAL_IP/order
{"orderId":"42"}%

Python アプリを使う

次に Python アプリをデプロイします。これは Dapr のデフォルトリスニングポートである localhost:3500 に JSON メッセージを投稿する基本的な Python アプリです。詳しくはリポジトリの中身を見ていただくとわかりますが、1秒ごとに orderId の番号が増えている動作をしています。

それではデプロイします。

% kubectl apply -f ./deploy/python.yaml
% kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
nodeapp-7bbf67cd77-dt46j     2/2     Running   0          15m
pythonapp-6bdf8dd995-dcp7k   2/2     Running   0          81s

ではカウントが増えているか確認してみましょう。

% curl $EXTERNAL_IP/order
{"orderId":657}%
% curl $EXTERNAL_IP/order
{"orderId":659}%

Redis 側も見てみよう

Redis 周りもちゃんと使われているか見てみたいと思います。Redis に接続します。

% key="アクセスキー"
% redis-cli -h XXX.redis.cache.windows.net -a $key -p 6380 --tls
XXX.redis.cache.windows.net:6380> HGET nodeapp||order data
"{\"orderId\":1646}"

monitor コマンドでも見てみると

XXX.redis.cache.windows.net:6380> monitor
OK
1689985041.744649 [0 xxxx.253.120.25:3072] "EVAL" "\n\tlocal etag = redis.pcall(\"HGET\", KEYS[1], \"version\");\n\tif type(etag) == \"table\" then\n\t  redis.call(\"DEL\", KEYS[1]);\n\tend;\n\tlocal fwr = redis.pcall(\"HGET\", KEYS[1], \"first-write\");\n\tif not etag or type(etag)==\"table\" or etag == \"\" or etag == ARGV[1] or (not fwr and ARGV[1] == \"0\") then\n\t  redis.call(\"HSET\", KEYS[1], \"data\", ARGV[2]);\n\t  if ARGV[3] == \"0\" then\n\t    redis.call(\"HSET\", KEYS[1], \"first-write\", 0);\n\t  end;\n\t  return redis.call(\"HINCRBY\", KEYS[1], \"version\", 1)\n\telse\n\t  return error(\"failed to set key \" .. KEYS[1])\n\tend" "1" "nodeapp||order" "0" "{\"orderId\":1483}" "1"
1689985041.744803 [0 lua] "HGET" "nodeapp||order" "version"
1689985041.744822 [0 lua] "HGET" "nodeapp||order" "first-write"
1689985041.744834 [0 lua] "HSET" "nodeapp||order" "data" "{\"orderId\":1483}"
1689985041.744853 [0 lua] "HINCRBY" "nodeapp||order" "version" "1"

できてましたね。

まとめ

Dapr を手元と AKS で動かしてみることにより、少し動作の雰囲気がわかりました。分散システムを作るときの状態管理に Dapr を使うのはとても良さそうです。
実際にアプリケーションを1から構築すると、もっと理解が深まると思うので EC サイトなどを作って動作検証をしてみたいと思います。

参考

https://zenn.dev/microsoft/articles/3a11be2a448f96

https://learn.microsoft.com/ja-jp/dotnet/architecture/dapr-for-net-developers/getting-started

GitHubで編集を提案
Microsoft (有志)

Discussion

SeaOtter (Kentaro Mori)SeaOtter (Kentaro Mori)

参考にされている... !
最近AI に浮気してるので Container 周りにもキャッチアップ頑張ります :)