分散アプリケーションランタイム「Dapr」で構築したアプリケーションをEKS上にデプロイしてみる

Daprに入門してみました
2021.07.18

CX事業本部@大阪の岩田です。Microsoftがリードする分散アプリケーションランタイムDaprについて調べる機会があったので、試しにEKS上にサンプルアプリをデプロイして動作させるまでの流れを試してみました。せっかくなので調べた内容やデプロイ手順について紹介させていただきます。

Daprとは

DaprはOSSでMicrosoftがリードするOSSの分散アプリケーション向けランタイムです。Daprによって分散アプリケーションを構築するために必要な基本機能が提供されるため、開発者はこれらの機能を自作する必要がなくなります。

以下はGitHubのDaprリポジトリで紹介されているDaprの概要です。

様々なプログラミング言語からHTTP APIもしくはgRPC APIを通じてビルディングブロックと呼ばれる様々な機能が利用できることが分かります。Daprは任意の環境で実行できるように設計されており、MicrosoftのクラウドサービスであるAzure以外にもAWSやGCPといったクラウド環境上での動作がサポートされています。

サポートされる言語

様々な言語向けにDaprのSDKが提供されており、SDKを利用することで簡単にDaprをアプリケーションに組み込めます。SDKは

  • Daprのビルディングブロックを呼び出すためのClient SDK
  • 別サービスからの呼び出しやトピックのサブスクライブを実現するための利用するServer extensions
  • 仮想アクターを構築するためのActor SDK

の3種に分かれます。

2021/7時点での言語別各種SDKの対応状況は以下の通りです。

Language Status Client SDK Server extensions Actor SDK
.NET Stable ASP.NET Core
Python Stable gRPC FastAPI Flask
Java Stable Spring Boot
Go Stable
PHP Stable
C++ In development
Rust In development
Javascript In development

SDK languages

ビルディングブロック

Daprはいくつかのビルディングブロックから構成されており、開発者は自分に必要なビルディングブロックだけを選択して利用できように構成されています。提供されるビルディングブロックは以下の通りで、分散アプリケーションの構築に必要となる一通りの機能を有していることがわかります

Service invocation

gRPC APIもしくはHTTP APIを利用して、別のアプリケーションをセキュアに呼び出すためのビルディングブロックです

  • 名前空間
  • mTLSによる認証
  • アクセスコントロール
  • リトライ
  • サービスディスカバリ
  • ロードバランシング
  • トレーシング

といった機能が提供されます

State management

キー/バリュー形式でステート管理を実現するためのビルディングブロックです

ステートの保存先として

  • MongoDB
  • Redis
  • Azure Cosmos DB
  • Azure Blob Storage

等のコンポーネントがサポートされています

State store component specs

Publish & subscribe

いわゆるPub/Subですね。メッセージのPublishとSubscribeを実現するためのビルディングブロックです。

  • メッセージのルーティング
  • At-least-onceなメッセージ配信の保証
  • コンシューマーグループの管理

といった機能が提供されます

Bindings

外部システムとDaprを接続するためのビルディングブロックです。外部システムのポーリングやリトライ処理といった定型的な処理はBindingsに任せることで、自前実装が不要になります。

Bindingsは外部リソースのイベントをトリガーにアプリケーションをトリガーするためのInput bindings、外部リソースを呼び出すためのOutput bindings2つの機能が提供されます。Bindingsを利用すると、アプリケーションコードからはデータの保存先がS3なのか?それともDynamoDBなのか?といったことを意識せずにデータを保存できるようになります。

GitHubのリポジトリでは様々な外部リソース向けのBindingsが開発されています

https://github.com/dapr/components-contrib/tree/master/bindings

Actors

任意の言語、プラットフォームでアクターモデルを利用するためのビルディングブロックです。アクターパターンはメッセージを受信して1度に1つずつ処理する自己完結型のユニット=アクターとしてコードを記述します。

アクターモデルはシングル スレッドで動作し、複数のアクターが同時実行可能ですが、各アクターは受信したメッセージを一度に 1 つずつ処理します。 アクター内でアクティブなスレッドは常に1つ以下ということが保証され、同時実行システムや並列システムを簡単に作成できるようになります。

Observability

可観測性を提供するビルディングブロックです。

  • 分散トレーシング
  • メトリックの収取
  • ログの収集
  • ヘルスチェック

といった機能をサポートします。このビルディングブロックはOpenTelemetryとZipkinをサポートするため、New RelicやDataDogといった外部サービスとの連携も容易に実現できます。

Secrets management

DBの接続文字列、APIキー、クライアント証明書といったシークレットを管理するためのビルディングブロックです。このビルディングブロックを利用すると、アプリケーションコードから

  • 環境変数
  • ローカル ファイル
  • Kubernetes シークレット
  • AWS Secrets Manager
  • Azure Key Vault
  • GCP Secret Manager
  • HashiCorp Vault

といったシークレットストアを簡単に利用できるようになります

やってみる

なんとなくDaprの概要が分かったので、実際にサンプルアプリを動かしてみます。今回は以下のリポジトリで提供されているサンプルアプリHello KubernetesをEKS上にデプロイしてみます

https://github.com/dapr/quickstarts/tree/master/hello-kubernetes

環境

今回利用した環境です

  • Kubernetes: 1.19
  • eksctl: 0.56.0
  • kubectl: v1.19.6-eks-49a6c0
  • Dapr CLI: 1.2.0
  • Helm: v3.6.2+gee407bd

今回はCloudShell環境に諸々のCLIツールをインストールし、CloudShell上からコマンドを実行していきます。

EKSクラスタの構築

まずEKSクラスタを構築します

$ eksctl create cluster --name dapr-cluster --managed

CloudFormationのスタックがデプロイされるので

  • ksctl-dapr-cluster-cluster
  • eksctl-dapr-cluster-nodegroup-ng-xxxxxxxx

2つのスタックがCREATE_COMPLETE状態に変わるまで待ちます。スタックのデプロイが完了したら簡単に動作確認しておきましょう。

$ kubectl get svc
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.100.0.1   <none>        443/TCP   19m
$ kubectl get node
NAME                                               STATUS   ROLES    AGE     VERSION
ip-192-168-25-77.ap-northeast-1.compute.internal   Ready    <none>   9m44s   v1.19.6-eks-49a6c0
ip-192-168-62-49.ap-northeast-1.compute.internal   Ready    <none>   9m43s   v1.19.6-eks-49a6c0

Daprの初期化

続いてEKSクラスタにDaprのコンポーネントをデプロイします

$ dapr init --kubernetes
⌛  Making the jump to hyperspace...
ℹ️  Note: To install Dapr using Helm, see here: https://docs.dapr.io/getting-started/install-dapr-kubernetes/#install-with-helm-advanced

✅  Deploying the Dapr control plane to your cluster...
✅  Success! Dapr has been installed to namespace dapr-system. To verify, run `dapr status -k' in your terminal. To get started, go here: https://aka.ms/dapr-getting-started

デプロイされたサービスを確認してみます

$ kubectl get svc --namespace dapr-system
NAME                    TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)              AGE
dapr-api                ClusterIP   10.100.180.138   <none>        80/TCP               9m37s
dapr-dashboard          ClusterIP   10.100.56.127    <none>        8080/TCP             9m37s
dapr-placement-server   ClusterIP   None             <none>        50005/TCP,8201/TCP   9m37s
dapr-sentry             ClusterIP   10.100.29.44     <none>        80/TCP               9m37s
dapr-sidecar-injector   ClusterIP   10.100.178.19    <none>        443/TCP              9m37s

デプロイされたPodは以下の通りです

$ kubectl get pods --namespace dapr-system
NAME                                     READY   STATUS    RESTARTS   AGE
dapr-dashboard-58b4647996-4fzs2          1/1     Running   0          10m
dapr-operator-85bdd7d89d-9tndp           1/1     Running   0          10m
dapr-placement-server-0                  1/1     Running   0          10m
dapr-sentry-76bfc5f7c7-wqhs6             1/1     Running   0          10m
dapr-sidecar-injector-786645f444-ph62k   1/1     Running   0          10m

RedisのState Storeをデプロイ

今回デプロイするサンプルアプリHello KubernetesはState StoreにRedisを利用します。Helmを使ってRedisのState Storeを作成しておきましょう。

まずはリポジトリの追加

$ helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" has been added to your repositories

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈

続いてbitnami/redisチャートをインストール

$ helm install redis bitnami/redis

NAME: redis
LAST DEPLOYED: Sun Jul 18 04:16:20 2021
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
** Please be patient while the chart is being deployed **

Redis(TM) can be accessed on the following DNS names from within your cluster:

    redis-master.default.svc.cluster.local for read/write operations (port 6379)
    redis-replicas.default.svc.cluster.local for read-only operations (port 6379)



To get your password run:

    export REDIS_PASSWORD=$(kubectl get secret --namespace default redis -o jsonpath="{.data.redis-password}" | base64 --decode)

To connect to your Redis(TM) server:

1. Run a Redis(TM) pod that you can use as a client:

   kubectl run --namespace default redis-client --restart='Never'  --env REDIS_PASSWORD=$REDIS_PASSWORD  --image docker.io/bitnami/redis:6.2.4-debian-10-r13 --command -- sleep infinity

   Use the following command to attach to the pod:

   kubectl exec --tty -i redis-client \
   --namespace default -- bash

2. Connect using the Redis(TM) CLI:
   redis-cli -h redis-master -a $REDIS_PASSWORD
   redis-cli -h redis-replicas -a $REDIS_PASSWORD

To connect to your database from outside the cluster execute the following commands:

    kubectl port-forward --namespace default svc/redis-master 6379:6379 &
    redis-cli -h 127.0.0.1 -p 6379 -a $REDIS_PASSWORD

Redisのサービス、Podが動作していることを確認しておきます

$ kubectl get svc
NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
kubernetes       ClusterIP   10.100.0.1       <none>        443/TCP    37m
redis-headless   ClusterIP   None             <none>        6379/TCP   3m6s
redis-master     ClusterIP   10.100.237.250   <none>        6379/TCP   3m6s
redis-replicas   ClusterIP   10.100.16.88     <none>        6379/TCP   3m6s
$ kubectl get pods
NAME               READY   STATUS    RESTARTS   AGE
redis-master-0     1/1     Running   0          3m17s
redis-replicas-0   1/1     Running   0          3m17s
redis-replicas-1   1/1     Running   0          2m27s
redis-replicas-2   1/1     Running   0          110s

サンプルアプリのデプロイ

準備ができたので、サンプルアプリをデプロイしていきます。まずGitHubのリポジトリをクローン

$ git clone https://github.com/dapr/quickstarts.git
Cloning into 'quickstarts'...
remote: Enumerating objects: 2593, done.
remote: Counting objects: 100% (295/295), done.
remote: Compressing objects: 100% (151/151), done.
remote: Total 2593 (delta 164), reused 233 (delta 134), pack-reused 2298
Receiving objects: 100% (2593/2593), 10.30 MiB | 26.04 MiB/s, done.
Resolving deltas: 100% (1528/1528), done.

Node.jsのサンプルアプリをデプロイします

$ cd quickstarts/hello-kubernetes
$ kubectl apply -f ./deploy/node.yaml 
service/nodeapp created
deployment.apps/nodeapp created

サンプルアプリのソースコードはこちらです。State StoreとしてはRedisを利用していますが、アプリケーションコードからはRedisの存在が隠蔽されており、単にHTTPリクエストを発行するだけでRedisに対して読み書きできることが分かります。

// ------------------------------------------------------------
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// ------------------------------------------------------------

const express = require('express');
const bodyParser = require('body-parser');
require('isomorphic-fetch');

const app = express();
app.use(bodyParser.json());

// These ports are injected automatically into the container.
const daprPort = process.env.DAPR_HTTP_PORT; 
const daprGRPCPort = process.env.DAPR_GRPC_PORT;

const stateStoreName = `statestore`;
const stateUrl = `http://localhost:${daprPort}/v1.0/state/${stateStoreName}`;
const port = 3000;

app.get('/order', (_req, res) => {
    fetch(`${stateUrl}/order`)
        .then((response) => {
            if (!response.ok) {
                throw "Could not get state.";
            }

            return response.text();
        }).then((orders) => {
            res.send(orders);
        }).catch((error) => {
            console.log(error);
            res.status(500).send({message: error});
        });
});

app.post('/neworder', (req, res) => {
    const data = req.body.data;
    const orderId = data.orderId;
    console.log("Got a new order! Order ID: " + orderId);

    const state = [{
        key: "order",
        value: data
    }];

    fetch(stateUrl, {
        method: "POST",
        body: JSON.stringify(state),
        headers: {
            "Content-Type": "application/json"
        }
    }).then((response) => {
        if (!response.ok) {
            throw "Failed to persist state.";
        }

        console.log("Successfully persisted state.");
        res.status(200).send();
    }).catch((error) => {
        console.log(error);
        res.status(500).send({message: error});
    });
});

app.get('/ports', (_req, res) => {
    console.log("DAPR_HTTP_PORT: " + daprPort);
    console.log("DAPR_GRPC_PORT: " + daprGRPCPort);
    res.status(200).send({DAPR_HTTP_PORT: daprPort, DAPR_GRPC_PORT: daprGRPCPort })
});

app.listen(port, () => console.log(`Node App listening on port ${port}!`));

3つのルートが定義されたExpressのアプリで、それぞれの処理概要は以下の通りです

  • GET /ports
    • Daprのポート番号を返却する
  • POST /neworder
    • State Storeに注文データを保存する
  • GET /order
    • State Storeから注文データを取得する

デプロイ完了後に確認すると、nodeappという名前のサービスが動作していることが分かります。

$ kubectl get svc nodeapp
NAME      TYPE           CLUSTER-IP       EXTERNAL-IP                                                                    PORT(S)        AGE
nodeapp   LoadBalancer   10.100.241.126   xxxxxx.ap-northeast-1.elb.amazonaws.com   80:31990/TCP   96s

Podの前段にELBがデプロイされているので、ELBのFQDNを取得します

$ export NODE_APP=$(kubectl get svc nodeapp --output 'jsonpath={.status.loadBalancer.ingress[0].hostname}')

curlコマンドで簡単に動作確認します

$ curl $NODE_APP/ports
{"DAPR_HTTP_PORT":"3500","DAPR_GRPC_PORT":"50001"}

レスポンスが返却されました。ちゃんと動いてそうですね。

続いてPythonのサンプルアプリをデプロイします。こちらのサンプルアプリはState Storeに注文データを送信し続けるアプリです。

# ------------------------------------------------------------
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ------------------------------------------------------------

import os
import requests
import time

dapr_port = os.getenv("DAPR_HTTP_PORT", 3500)
dapr_url = "http://localhost:{}/v1.0/invoke/nodeapp/method/neworder".format(dapr_port)

n = 0
while True:
    n += 1
    message = {"data": {"orderId": n}}

    try:
        response = requests.post(dapr_url, json=message, timeout=5)
        if not response.ok:
            print("HTTP %d => %s" % (response.status_code,
                                     response.content.decode("utf-8")), flush=True)
    except Exception as e:
        print(e, flush=True)

    time.sleep(1)

デプロイします

$ kubectl apply -f ./deploy/python.yaml
deployment.apps/pythonapp created

しばらくするとサンプルアプリのPodが起動してきます

$ kubectl get pods --selector=app=python 
NAME                       READY   STATUS    RESTARTS   AGE
pythonapp-fcb4f49b-gv4tr   2/2     Running   0          26s

Node.jsアプリのログを確認するとPythonアプリから連続して注文データがPOSTされていることが分かります

$ kubectl logs --selector=app=node -c node --tail=-1
Got a new order! Order ID: 82
Successfully persisted state.
Got a new order! Order ID: 83
Successfully persisted state.
...略

Node.jsアプリのエンドポイント/orderにアクセスするとState Storeに保存された注文データがPythonアプリから上書きされていることが分かります

$ curl $NODE_APP/order
{"orderId":177}
$ curl $NODE_APP/order
{"orderId":178}

最後に redis-cliからState Store(Redis)の中身を直接確認してみましょう

$ export REDIS_PASSWORD=$(kubectl get secret --namespace default redis -o jsonpath="{.data.redis-password}" | base64 --decode)
$ kubectl exec -it redis-master-0 -- redis-cli -a $REDIS_PASSWORD
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> hgetall "nodeapp||order"
1) "data"
2) "{\"orderId\":707}"
3) "version"
4) "700"

まとめ

Daprの概要調査とサンプルアプリのデプロイまで試してみました。今回はDaprのビルディングブロックのうち、State managementぐらいしか試せていませんが、他のビルディングブロックについても改めて試してみたいと思います。

参考