Daprが提供する主要な機能について理解してみる - Daprの基本とService-to-service invocationについて

Daprが提供する主要な機能について理解してみる - Daprの基本とService-to-service invocationについて

January 11, 2020
Kubernetes
kubernetes, dapr, microservices

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でやりとりします https://github.com/dapr/docs/blob/master/overview.md

一般的に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-operatordapr.io/enabled: "true"ラベルがついたリソースのイベント(作成/更新/削除)を監視して、様々なアクションを発生させるそうです。

公式ドキュメントや、参考にした資料だとこのdapr-operatordapr.io/enabled: "true"ラベルのついたpodを検知して、dapr-sidecar-injectorにsidecarを注入させるという説明がされているのですが、実際に動かすと、dapr-sidecar-injectorは単独で動いてるように見えます。

dapr-operatorをTerminatedにした状態でも、新しいpodを立てたらDaprサイドカーが注入されたので。


Service-to-service invocation

環境が整ったので、実際の機能をみていきます。

Daprでは、Daprの届く範囲(今回ではk8sクラスタ内)でレジリエントなサービス間呼び出しを提供します。

公式documentより

Resilient service-to-service invocation enables method calls, including retries, on remote services wherever they are located in the supported hosting environment.

一般的なマイクロサービスアプリケーションであれば、呼び出したいサービスに対してservice経由でHTTPリクエストを行うかと思います。

Untitled.001.png

Daprの場合はService invocationを利用して別サービスの呼び出しを行います。 相手のサービスを直接呼びにいかず、自分自身のサイドカーコンテナであるDaprにリクエストを送り、Daprコンテナが別サービスのDaprコンテナを呼び出すといった挙動になります。

Untitled.002.png

これは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-appdapr.io/idで指定したidになります。そして最後のmethod以降がルートパスになります。

ので、上記のURLであれば、nginxのlocation /につながりますし 2つ目に打ってるlocalhost:3500/v1.0/invoke/web-app/method/50x.htmlであればlocation /50x.htmlにつながります。

まとめ

呼び出したいサービスに割り当てたdapr.idと呼び出したいメソッド名を指定するだけで、サービスの呼び出しが可能になります。

参考