Daprが提供する主要な機能について理解してみる - Daprの基本とService-to-service invocationについて
January 11, 2020
Daprとは?
Microsoftがオープンソースとして公開しているマイクロサービス/分散システム向けのランタイムです。 現在(2019/12)はまだアルファ段階です。
Note: Dapr is currently under community development in alpha phase. Dapr is not expected to be used for production workloads until its 1.0 stable release.
読みはyoutubeでMicrosoftの人が話してるのを聞くと「ダッパー」と聞こえます こちらの記事によると、もしくは「ダパァ」らしいです。
名前の由来はAzure fridayによると「Distributed Application Runtime」からとっているそうです。
A) D-A-P-R. What does that stand for? B) Distributed Application Runtime
特徴としてましては、以下の画像のように、分散システムにおける通信の様々なサポートを言語やフレームワークによらず(対応言語はリポジトリ参照)提供してくれるようです。
主要な機能として上の図にも記載されている通り以下が提供されています。
- Service-to-service invocation
- State management
- Publish and subscribe
- Resource bindings & triggers
- Actors
- Distributed tracing
- Extensible…
単語だけだとイメージがつかなかったので、調べてみようと思いました。 全部を1つの記事に書いていくとすごく長くなりそうだったので、この記事ではDaprの基本的な部分とサービス間呼び出し(Service-to-service invocation)をまとめてみます。
Daprが動作するk8sクラスタ構築
まずDaprが動作する環境の構築を行い、daprが介入することで構成がどのように変わるかをみてみます。
Daprは多様な環境で動作するそうですが、今回はk8s上で動作させます。 k8sクラスタとしては、自分が慣れてるのもありEKSを利用します。
k8sクラスタ構築
eksctlで基本的なクラスタを構築します。
$ eksctl create cluster --name dapr-test-cluster \
--version 1.14 \
--nodegroup-name dapr-test-cluster-worker \
--node-type t3.small \
--nodes 3 \
--nodes-min 1 \
--nodes-max 5 \
--managed
完了したら、contextがこちらのクラスタに切り替わってるはずです。
$ kubectl get no
NAME STATUS ROLES AGE VERSION
ip-192-168-51-32.ap-northeast-1.compute.internal Ready <none> 2m32s v1.14.7-eks-1861c5
Daprのデプロイ
DaprはCLIを提供しており、これを使ってk8s上にDaprをデプロイできます。
Macであれば、以下のコマンドでCLIをインストールできます。
$ curl -fsSL https://raw.githubusercontent.com/dapr/cli/master/install/install.sh | /bin/bash
2019/12時点では、v0.3.0が最新となります。
$ dapr --version
CLI version: 0.3.0
Runtime version: 0.3.0
もしruntimeのversionが古かったりすれば、以下のコマンドで最新を取得できます
$ dapr init --runtime-version 0.3.0
k8sへDaprをデプロイするには以下のコマンドを叩きます
$ dapr init --kubernetes
正常にデプロイできるとdefault namespaceにdapr用のdeploymentが3つ立ち上がります。
$ kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
dapr-operator 1/1 1 1 2m4s
dapr-placement 1/1 1 1 2m4s
dapr-sidecar-injector 1/1 1 1 2m4s
Daprが立ち上げる3つのdeploymentリソース
- dapr-operator
- dapr-placement
- dapr-sidecar-injector
この3つがDaprのコントロールプレーンとなります。
まずわかりやすいdapr-sidecar-injector
をみていきます。
名前の通りですが、Daprはサイドカーパターンを採用しています。
1つのPodの中にアプリ用のコンテナとDaprコンテナが存在し、これらがHTTP/gRPCでやりとりします
一般的にk8sでサイドーカーパターンというと以下のようなマニフェストを想像します。 アプリケーション用のメインコンテナの他に、loggerのような付加的な機能をもつコンテナを一緒のpodで管理しているマニフェストです。
apiVersion: v1
kind: Pod
metadata:
name: sample-app
spec:
containers:
- image: app
name: main-container
- image: logger
name: sidecar-container
では、Daprを使うpodでは、マニフェストにDaprコンテナの情報を記述して動かすのか?というと半分正解で半分間違いです。 Daprでは、マニフェストに直接的にコンテナ情報を付与せず、アノテーションを付与することで、自動的にサイドカーとしてDaprをインジェクションします。
apiVersion: v1
kind: Pod
metadata:
name: sample-app
annotations:
# アノテーションを付与する
dapr.io/enabled: "true"
spec:
containers:
- image: nginx
name: sample-app
これをapplyしてあげると、1つしかコンテナがないはずなのに、READYが2/2
になってます。
kubectl get po sample-app
NAME READY STATUS RESTARTS AGE
sample-app 2/2 Running 0 28s
中身を確認してみましょう
$ kubectl get po sample-app -o yaml
apiVersion: v1
kind: Pod
metadata:
annotations:
dapr.io/enabled: "true"
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{"dapr.io/enabled":"true"},"name":"sample-app","namespace":"default"},"spec":{"containers":[{"image":"nginx","name":"sample-app"}]}}
kubernetes.io/psp: eks.privileged
creationTimestamp: "2019-12-10T17:05:09Z"
name: sample-app
namespace: default
resourceVersion: "4794"
selfLink: /api/v1/namespaces/default/pods/sample-app
uid: 38ee7d0f-1b6f-11ea-be00-0aa1afee716c
spec:
containers:
- image: nginx
imagePullPolicy: Always
name: sample-app
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: default-token-mrs9r
readOnly: true
- args:
- --mode
- kubernetes
- --dapr-http-port
- "3500"
- --dapr-grpc-port
- "50001"
- --app-port
- ""
- --dapr-id
- sample-app
- --control-plane-address
- http://dapr-api.default.svc.cluster.local
- --protocol
- http
- --placement-address
- dapr-placement.default.svc.cluster.local:80
- --config
- ""
- --enable-profiling
- "false"
- --log-level
- info
- --max-concurrency
- "-1"
command:
- /daprd
env:
- name: HOST_IP
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: status.podIP
- name: NAMESPACE
value: default
image: docker.io/daprio/dapr:latest
imagePullPolicy: Always
name: daprd
ports:
- containerPort: 3500
name: dapr-http
protocol: TCP
- containerPort: 50001
name: dapr-grpc
protocol: TCP
...
このような感じで、daprはアノテーションを付与することで、サイドカーにdaprコンテナを注入してくれます。
これが、dapr-sidecar-injector
の機能です。
次にdapr-placement
ですが、これはDaprコンテナが注入された全てのpodのActor情報を管理して、Actor間の通信のルーター的な機能を持ちます。
Daprを含むpodとそのActorIDをハッシュテーブルで管理しており、あるサービスAから、サービスB(ActorID=hogehoge)を呼び出すと、このハッシュテーブルを参照に呼び先のpodにつないでくれるようになります。
これについてはActorの章でもう少し触れようと思います。
最後にdapr-operator
はdapr.io/enabled: "true"
ラベルがついたリソースのイベント(作成/更新/削除)を監視して、様々なアクションを発生させるそうです。
公式ドキュメントや、参考にした資料だとこのdapr-operator
がdapr.io/enabled: "true"
ラベルのついたpodを検知して、dapr-sidecar-injector
にsidecarを注入させるという説明がされているのですが、実際に動かすと、dapr-sidecar-injector
は単独で動いてるように見えます。
dapr-operator
をTerminatedにした状態でも、新しいpodを立てたらDaprサイドカーが注入されたので。
Service-to-service invocation
環境が整ったので、実際の機能をみていきます。
Daprでは、Daprの届く範囲(今回ではk8sクラスタ内)でレジリエントなサービス間呼び出しを提供します。
Resilient service-to-service invocation enables method calls, including retries, on remote services wherever they are located in the supported hosting environment.
一般的なマイクロサービスアプリケーションであれば、呼び出したいサービスに対してservice経由でHTTPリクエストを行うかと思います。
Daprの場合はService invocationを利用して別サービスの呼び出しを行います。 相手のサービスを直接呼びにいかず、自分自身のサイドカーコンテナであるDaprにリクエストを送り、Daprコンテナが別サービスのDaprコンテナを呼び出すといった挙動になります。
これはIstioなどのサービスメッシュと似てますね。
HTTPやりとりでのアプリ間通信であれば, ちょっと利点がみえづらいですが 例えば、アプリ間通信でKafka, redisなどを挟んだりする場合はアプリにクライアントライブラリを入れたりの対応が必要ですが Daprであれば、そこのプロトコル周りをDaprがよしなにやってくれるのでアプリ側は常にHTTP or gRPCで通信が行えます(このあたりはResource bindings & triggersでまとめています)
サービスメッシュがなければ、各マイクロサービスはサービス間通信を規定するロジックをコーディングする必要があり、これでは開発者はビジネス目標に専念できなくなります。また、サービス間通信を規定するロジックが各サービス内に隠蔽されるので、通信エラーの診断が難しくなります。
動かしてみる
webサーバとcurlを行うpodを用意して、daprのservice invocationでアクセスしてみましょう マニフェストを2つ用意します
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app-deployment
spec:
selector:
matchLabels:
app: web-app
replicas: 2
template:
metadata:
labels:
app: web-app
annotations:
dapr.io/enabled: "true"
dapr.io/id: "web-app"
dapr.io/port: "80"
spec:
containers:
- name: web-app
image: nginx:1.7.9
ports:
- containerPort: 80
apiVersion: apps/v1
kind: Deployment
metadata:
name: caller-deployment
spec:
selector:
matchLabels:
app: caller
replicas: 1
template:
metadata:
labels:
app: caller
annotations:
dapr.io/enabled: "true"
dapr.io/id: "caller"
spec:
containers:
- name: caller
image: centos:7
command:
- "tail"
- "-f"
- "dev/null"
新たに以下のラベルを付与しました。
dapr.io/id: "web-app"
dapr.io/port: "80"
dapr.io/id
はこのdeploymentで提供されるサービスを識別するためのものになります。
dapr.io/port
の方はserviceでいうtargetPortのような役割になります。
外部からサービス呼び出しされた際に、daprコンテナはリクエストをこのportに対して投げます。
このweb-appはnginxを利用して80ポートで受け付けているので、dapr.io/port
も80にしています。
では実際にcallerの中から、呼んでみましょう
# calle podを特定する
$ kubectl get po | grep caller
caller-deployment-5887c557cd-7g8zm 2/2 Running 0 13m
$ kubectl exec -it caller-deployment-5887c557cd-7g8zm bash
# callerコンテナに入り、sidecar経由でサービス呼び出しを行う
$ curl localhost:3500/v1.0/invoke/web-app/method/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
$ curl localhost:3500/v1.0/invoke/web-app/method/50x.html
<!DOCTYPE html>
<html>
<head>
<title>Error</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>An error occurred.</h1>
<p>Sorry, the page you are looking for is currently unavailable.<br/>
Please try again later.</p>
<p>If you are the system administrator of this resource then you should check
the <a href="http://nginx.org/r/error_log">error log</a> for details.</p>
<p><em>Faithfully yours, nginx.</em></p>
</body>
</html>
localhostにアクセスして、nginxが呼び出せましたね。 少しURLを確認してみます
localhost:3500/v1.0/invoke/web-app/method/
サイドカーでは同じネットワークを利用するので、localhost
でDaprサイドカーにつなげることができます。またDaprは標準で3500ポートをHTTP用のportとして公開しているので、こちらを使います(gRPC用のportも公開しています)
v1.0
はバージョンで、invoke
がサービス呼び出し,web-app
がdapr.io/id
で指定したidになります。そして最後のmethod
以降がルートパスになります。
ので、上記のURLであれば、nginxのlocation /
につながりますし
2つ目に打ってるlocalhost:3500/v1.0/invoke/web-app/method/50x.html
であればlocation /50x.html
につながります。
まとめ
呼び出したいサービスに割り当てたdapr.idと呼び出したいメソッド名を指定するだけで、サービスの呼び出しが可能になります。