谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

Dapr Advent Calendar 18日目 - Dapr + k8sとZipkinで分散トレーシング

こんにちは Dapr Advent Calendar 18日目です。気づけばあと2週間で年明けじゃないですか。歳を取ると1年が早くなるって言いますけども、それとは別に、この2年はやっぱり早かったですよねぇ。

k8sとDaprで分散トレーシング

さて、今回はk8s + Dapr上のアプリケーションの分散トレーシングを行います。主にDaprのパイプライン設定ファイルをk8sに適用するところや、それをアプリケーション側で有効にする辺りが、今回のポイントとなります。

f:id:cero-t:20211218132418p:plain
今回作るアプリケーション

アプリケーションの作成

動かすアプリケーションは Dapr Advent Calendar 8日目 - DaprとZipkinで分散トレーシング で作成したものです。そちらを先に読んでからこのエントリーを読んでください。

ソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021

また Dapr Advent Calendar 13日目 - Dapr + k8sでHello World で説明したツールなどがセットアップ済みであることを前提で説明を書いています。

作成したソースコードのおさらい

以前のエントリーで作成したアプリケーションのソースコードをおさらいします。

TracingController.java(抜粋)

public class TracingController {
    private RestTemplate restTemplate;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method")
    private String helloUrl;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/publish-app/method")
    private String publishUrl;

    public TracingController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/invokeHello2")
    public Map<String, ?> invokeHello(@RequestHeader("traceparent") String traceparent) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set("traceparent", traceparent);
        HttpEntity<?> request = new HttpEntity<>(httpHeaders);

        Map<?, ?> result = restTemplate.exchange(helloUrl + "/hello", HttpMethod.GET, request, Map.class).getBody();
        return Map.of("baseUrl", helloUrl, "remoteMessage", result);
    }

    @PostMapping("/invokePublish")
    public void invokePublish(@RequestBody Object message, @RequestHeader("traceparent") String traceparent) {
        System.out.println(traceparent);

        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.set("traceparent", traceparent);
        HttpEntity<?> request = new HttpEntity<>(message, httpHeaders);

        restTemplate.exchange(publishUrl + "/publish2", HttpMethod.POST, request, Void.class);
    }

Hello Worldを呼ぶ /invokeHello2 と、メッセージのpublishを呼ぶ /invokePublish というエンドポイントを提供しています。分散トレーシングに用いる traceparent というHTTPヘッダを伝播させるために、少しだけ実装が複雑になっています。

アプリケーションのイメージ作成

続いて、このアプリケーションのイメージを作成しましょう。

イメージ作成先がminikubeのイメージレジストリになるよう次のコマンドを実行します。

eval $(minikube docker-env)

そしてイメージ作成のコマンドを実行します。

cd (GitHubのディレクトリパス)/dapr-advent-2021/tracing
../mvnw spring-boot:build-image

これでイメージが作成できました。

また、hello publish subscribe のアプリケーションも必要となるため、それぞれイメージを作成します。

cd (GitHubのディレクトリパス)/dapr-advent-2021
./mvnw spring-boot:build-image -pl hello,publish,subscribe

これでイメージ側の準備は完了です。

マニフェストファイルの作成

次に、このアプリケーションをデプロイするためのマニフェストファイルを作成します。

k8s/tracing-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tracing-app
  labels:
    app: tracing
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tracing
  template:
    metadata:
      labels:
        app: tracing
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "tracing-app"
        dapr.io/app-port: "8087"
    spec:
      containers:
      - name: tracing
        image: tracing:1.0.0
        ports:
        - containerPort: 3500
        imagePullPolicy: IfNotPresent

---
kind: Service
apiVersion: v1
metadata:
  name: tracing-svc
  labels:
    app: tracing
spec:
  selector:
    app: tracing
  ports:
  - protocol: TCP
    port: 3500
    targetPort: 3500
  type: LoadBalancer

以前作成した hello.yaml とほとんど同じ構成ですが、今回はアプリケーションのポートを外部公開するのではなく、Daprのポートを外部公開するため3500番ポートを公開する設定にしています。Daprを経由しなければ最初の traceparent を取得できないため、このような構成にしています。

アプリケーションのポートを公開すべきか、Daprのポートを公開すべきかは、あとで論じたいと思います。

Zipkinのマニフェストファイルの作成

続いて、分散トレーシングの収集とUIを提供するZipkinをデプロイするためのマニフェストファイルを作成します。

k8s/zipkin.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: zipkin-deploy
  labels:
    app: zipkin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: zipkin
  template:
    metadata:
      labels:
        app: zipkin
    spec:
      containers:
      - name: zipkin
        image: openzipkin/zipkin
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 9411

---
kind: Service
apiVersion: v1
metadata:
  name: zipkin-svc
  labels:
    app: zipkin
spec:
  type: LoadBalancer
  selector:
    app: zipkin
  ports:
  - protocol: TCP
    port: 9411
    targetPort: 9411

イメージを使ってZipkinをデプロイしています。内容は以前作成したPostgreSQL用のものとほとんど同じです。外部から9411番ポートにアクセスできるようServiceも作成しています。

Daprのパイプライン設定ファイルでZipkinへのトレースを指定する

続いて、Daprのパイプライン設定ファイルを作成して、Zipkinにトレーシング情報を送るようにします。

k8s/zipkin.yaml

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: tracing
spec:
  tracing:
    samplingRate: "1"
    zipkin:
      endpointAddress: http://zipkin-svc:9411/api/v2/spans

これは Dapr Advent Calendar 8日目 - DaprとZipkinで分散トレーシング で説明した ~/.dapr/config.yaml とほとんど同じ内容で、接続先だけが localhost ではなく zipkin-svc を参照するように修正しています。

samplingRate には 1 を設定して、すべてのトレーシング情報をZipkinに送るようにしています。

またこの設定には metadata.nametracing という名前をつけています。

アプリケーション側で分散トレーシングを有効にする

さて、上でHTTPのパイプライン設定ファイルを作成しましたが、このパイプライン設定がすべてのアプリケーションで有効になる、というわけではありません。Daprでは設定ファイルをk8sにapplyすると、わりと自動で全体的に有効化されることが多かったのですが、パイプラインのConfigurationに関しては勝手に全体で有効化されません。全体に有効化すると弊害が大きいというか、アプリケーションによってパイプライン設定が異なるのが当たり前ですからね。

そのため、アプリケーション側がこのパイプライン設定を使うように設定する必要があります。

アプリケーション側のマニフェストファイルを次のように修正します。

k8s/tracing-app.yaml(抜粋)

      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "tracing-app"
        dapr.io/app-port: "8087"

これを次のように変更します。

      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "tracing-app"
        dapr.io/app-port: "8087"
        dapr.io/config: "tracing"

dapr.io/configtracing を指定しています。この tracing というのは上のパイプライン設定ファイルの metadata.name で指定した名前です。これで、アプリケーション側がパイプライン設定を利用するようになり、分散トレーシングが有効になるわけです。

同じように k8s/hello-app.yaml k8s/publish-app.yaml k8s/subscribe-app.yaml にも dapr.io/config: "tracing" を追加してください。

これでマニフェストファイルの作成は完了です。

アプリケーションをデプロイしてアクセスする

イメージをk8sにデプロイ

それでは作成したイメージをk8sにデプロイします。

cd (GitHubのディレクトリパス)/dapr-advent-2021
kubectl apply -f k8s/zipkin.yaml
kubectl apply -f k8s/tracing.yaml
kubectl apply -f k8s/tracing-app.yaml
kubectl apply -f k8s/hello-app.yaml
kubectl apply -f k8s/publish-app.yaml
kubectl apply -f k8s/subscribe-app.yaml

まずZipkinをデプロイし、HTTPパイプライン設定ファイルをデプロイし、アプリケーションのデプロイという順番に行っています。

RabbitMQなどをデプロイしていない場合には、そちらもデプロイする必要があります。面倒なので次のコマンドでまとめてやってしまったほうが良いでしょう。

kubectl apply -f k8s

これでk8sフォルダにあるすべてのマニフェストファイルに対して kubectl apply が行われます。

k8s上のアプリケーションにアクセスする

それでは、起動したアプリケーションにアクセスしてみましょう。

ポートフォワードの設定

まずはローカルPCからminikube内のpodにアクセスできるよう、ポートフォワードの設定を行います。

次のコマンドを実行します。

kubectl port-forward service/zipkin-svc 9411:9411

これでローカルPCの9411番ポート経由でZipkinにアクセスできるようになりました。

また、別のコンソールで次のコマンドを実行します。

kubectl port-forward service/tracing-app 3500:3500

これでローカルPCの3500番ポート経由で、tracing-appのDaprにアクセスできるようになりました。

Hello Worldを呼び出す処理にアクセスする

それでは、別のコンソールからアクセスしてみましょう。

curl localhost:3500/invokeHello2 -H "dapr-app-id:tracing-app"

せっかくなのでDapr 1.4から使えるようになった方法でアクセスしてみました。これは curl localhost:3500/v1.0/invoke/tracing-app/method/invokeHello2 と同じ意味になります。

次のような結果が表示されます。

{
  "baseUrl": "http://localhost:3500/v1.0/invoke/hello-app/method",
  "remoteMessage": {
    "message": "Hello, world!"
  }
}

正しくメッセージが返ってきました。

Zipkin側でトレース情報を確認してみましょう。

f:id:cero-t:20211218134028p:plain
Zipkinのトレース一覧

f:id:cero-t:20211218134058p:plain
helloを呼んだトレースの詳細

トレーシング情報は、上から順番に

  • Dapr → tracing-app (/invokeHello2)
  • tracing-app → Dapr (Invoke APIでhello-appの /hello)
  • Dapr → hello-app (/hello)

となっています。Daprを通過する部分で分散トレーシングの情報が取得されていることが分かります。

Pub/subを呼び出す処理にアクセスする

続いて、Pub/subを行うエンドポイントにもアクセスしてみましょう。

curl -XPOST "localhost:3500/invokePublish" -H "dapr-app-id:tracing-app" -H "Content-type:application/json" -d '{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}'

特にエラーなどが起きなければメッセージングは成功しているはずです。

Zipkin側でトレース情報を確認してみましょう。

f:id:cero-t:20211218134116p:plain
Zipkinのトレース一覧

f:id:cero-t:20211218134131p:plain
publish/subscribeを呼んだトレースの詳細

トレーシング情報は、上から順番に

  • Dapr → tracing-app (/invokePublish)
  • tracing-app → Dapr (Invoke APIでpublish-appの /publish2)
  • Dapr → publish-app (/publish2)
  • publish-app → Dapr (Pub/sub APIでrabbitmq-pubsubのmy-message)
  • (Dapr -> RabbitMQ -> Dapr)
  • Dapr → subscribe-app (my-messageの受信)

となっており、やはりDaprを通過する部分で分散トレーシングの情報が取得されています。

RabbitMQにエンキュー、デキューするところ自体は分散トレーシングの対象外になっています。本当はここも表示されれば嬉しいんですけどね。

いずれにせよ、k8s上でも問題なく分散トレーシングができることが分かりました。

f:id:cero-t:20211218134311p:plain
Dapr + k8s上で動く分散トレーシングと対象のアプリケーション

外部からアプリケーションにアクセスすべきか、Daprにアクセスすべきか

今回はアプリケーションのポートではなく、Daprのポートに外部からアクセスする形としました。実際に運用する際、アプリケーション側を外部に公開すべきか、Dapr側を公開すべきか、どちらが良いのか改めて考えてみましょう。

基本的にはアプリケーション側のポートを外部公開するべきだと思いますし、実際に僕はそのように運用しています。

ただ、Dapr側を公開することで、次のようなメリット/デメリットがあります。

  • メリット
    • 外部からアクセスする際に、OAuthやJWTトークン検証などの認可機能を利用することができる
    • Dapr内部の通信だけでなく、外部からアクセスされた最初のリクエストを分散トレーシングに含めることができる
      • (逆に言えば、アプリケーションのポートに直接アクセスさせた場合は、最初のリクエストが分散トレーシングの範疇外となる)
    • アプリケーションの個別のポート番号を気にする必要はなく、Daprの3500番ポートにアクセスしさえすれば良い
  • デメリット
    • Daprが管理するすべてのアプリケーションやステートストア、メッセージング、シークレットストアにアクセスされてしまう危険性がある

デメリットが大きすぎるため、原則としてアプリケーション側のポートを外部公開すべきなのです。

ただ今回の例のように -H "dapr-app-id:app-name" を指定することで、任意のアプリケーションのDaprにのみアクセスさせることは可能です。k8sの前段にあるロードバランサなどでヘッダを固定し、仮に元のリクエストに dapr-app-id が含まれていてもそれを確実に無視するような仕組みが利用できるのであれば、daprのポートを安全に外部公開することも可能でしょう。そうすれば、認可や分散トレーシングなどの機能をうまく活用することができます。

実は僕もいま実運用しているシステムで、ちょうど同じ課題に直面しており、外部からのリクエストを分散トレーシングに含めるために、Daprのポートを外部公開するか、それともアプリケーション側のポートを外部公開して、アプリケーション側で traceparent を発行するのか、あるいはk8sまでのどこかで traceparent を発行するのかを迷っているところです。

僕自身は「迷ったときは安全側に倒したほうが良い」と考えているので、アプリケーション側のポートを公開し、一方で多少は雑に扱っても良い traceparent はクライアント側で発行するとか、k8sの前段のどこかで発行する、くらいが良いのではないかと考えています。

この辺りは様々なポリシーがあるでしょうから、Daprの識者たちともディスカッションしてみたいですね。

まとめ

  • Dapr + k8sの環境でも問題なく分散トレーシングができました
  • アプリケーション側の metadata.annotations で利用するHTTPパイプラインを指定する必要があります
  • 外部公開するのは、アプリケーション側のポートか、Dapr側のポートか、慎重に検討する必要があります

だいぶDaprとk8sの運用に慣れてきて、構成も考えられるようになってきましたね。

これでいったんDapr + k8sの機能を紹介するのはおしまいにして、明日はAWS上にデプロイする方法を説明したいと思います。

それでは!