谷本 心 in せろ部屋

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

Dapr Advent Calendar 14日目 - Dapr + k8sでサービスを呼ぶ

こんにちは Dapr Advent Calendar 14日目です。皆さん、Log4J 2対応のほうは大丈夫ですか?

k8sでDaprのアプリケーション同士を連携

今回はk8sの上で2つのアプリケーションを動かし、Daprを経由してそれらを連携させてみます。

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

アプリケーションの作成

今回動かすアプリケーションは Dapr Advent Calendar 3日目 - Daprでサービスを呼ぶ で作成したものなので、そちらを先に読んでからこのエントリーを読んでください。

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

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

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

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

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

まずは呼び出される側のhelloアプリケーションです。

(hello) HelloController.java

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Map<String, String> hello() {
        return Map.of("message", "Hello, world!");
    }
}

/hello にアクセスすると Hello, world! を含んだJSONを返すだけの処理です。このアプリケーションはポート8080番で起動します。

これを呼び出すinvokeアプリケーションのコードは次のようになっています。

(invoke) InvokeController.java

@RestController
public class InvokeController {
    private RestTemplate restTemplate;

    @Value("${baseUrl}")
    private String baseUrl;

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

    @GetMapping("/invokeHello")
    public Map<String, ?> invokeHello() {
        Map<?, ?> result = restTemplate.getForObject(baseUrl + "/hello", Map.class);
        return Map.of("baseUrl", baseUrl, "remoteMessage", result);
    }
}

helloアプリケーションの /hello を呼び出すだけの簡単な処理です。

また、設定ファイルは次のようになっています。

(invoke) application.properties

server.port=8081
baseUrl=http://localhost:8080

このアプリケーションはポート8081番で起動します。

また baseUrl は次のファイルで上書きしています。

(invoke) application-dapr.properties

baseUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method

Springのプロファイルが dapr の時は baseUrl の値として http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method が使われるようになっています。

この DAPR_HTTP_PORT は、k8s上のDaprでは 3500環境変数で渡されるようになっています。

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

それでは、invokeアプリケーションのイメージを作成しましょう。

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

eval $(minikube docker-env)

そして、イメージを作成するために次のコマンドを実行します。

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

これでイメージの作成は完了です。

もし hello のイメージを作成していなければ、同じように hello ディレクトリで ../mvnw spring-boot:build-image コマンドを実行してください。

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

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

helloアプリケーションのマニフェストファイル

hello側のマニフェストファイルは前回作成したものと同じです。

k8s/hello-app.yaml

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

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

今回は hello-svc は使わないので削除しても構いませんが、念のため残しておきます。

invokeアプリケーションのマニフェストファイル

つづいて、invokeアプリケーションをデプロイするためのマニフェストファイルを作成します。

k8s/invoke-app.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: invoke-app
  labels:
    app: invoke
spec:
  replicas: 1
  selector:
    matchLabels:
      app: invoke
  template:
    metadata:
      labels:
        app: invoke
      annotations:
        dapr.io/enabled: "true"
        dapr.io/app-id: "invoke-app"
        dapr.io/app-port: "8081"
    spec:
      containers:
      - name: invoke
        image: invoke:1.0.0
        ports:
        - containerPort: 8081
        imagePullPolicy: IfNotPresent
        env:
        - name: spring.profiles.active
          value: dapr

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

基本的には hello-app.yamlhelloinvoke に変え、80808081 にしただけです。

ただ一点だけ spec.template.spec.containers.env環境変数に関する設定を追加しています。

        - name: spring.profiles.active
          value: dapr

Springのプロファイルに dapr を指定し、application-dapr.yaml が利用されるようにしています。これで baseUrl の値が http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method になります。

別の方法として、次のように指定することもできます。

        - name: baseUrl
          value: http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method

Spring Bootは環境変数でパラメータを上書きできるため、このように baseUrl の値を直接指定しても構いません。Dapr関連の設定をk8sマニフェストに任せるという方針の場合は、このような設定を使うほうが良いでしょうね。

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

イメージをk8sにデプロイ

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

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

次のように表示されるはずです。

deployment.apps/invoke-app created
service/invoke-svc created

これでinvokeアプリケーションがk8s上にデプロイされました。

もしhelloアプリケーションをデプロイしていなければ kubectl apply -f k8s/hello-app.yaml でデプロイしてください。

起動したpodを次のコマンドで確認しましょう。

kubectl get pods

次のように表示されるはずです。

NAME                          READY   STATUS    RESTARTS   AGE
hello-app-fb644cd5d-tsxmh     2/2     Running   0          3h8m
invoke-app-745b5f4bf6-chj5w   2/2     Running   0          65m

hello-appinvoke-app の2つのpodが実行中であることが分かります。

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

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

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

kubectl port-forward service/invoke-svc 8081:8081

次のように表示されます。

Forwarding from 127.0.0.1:8081 -> 8081
Forwarding from [::1]:8081 -> 8081

これでローカルPCの8081番ポートから、minikubeの8081番ポートにアクセスできるようになりました。

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

curl localhost:8081/invokeHello

次のメッセージが取得できるはずです。

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

baseUrl がポート3500番のDaprにアクセスするURLになっており、helloアプリケーションが返すメッセージも取得できました。

k8s上で作成した2つのpodが、DaprのInvoke APIを経由してアクセスしていることが分かりました。

f:id:cero-t:20211213221836p:plain
k8sで動くアプリケーションの詳細

なぜ繋がるのか?

どうやってDaprを経由して別のアプリケーションを呼び出すことができたのでしょうか。それに関するドキュメントはこれです。

docs.dapr.io

f:id:cero-t:20211213214902p:plain
Service invocation overview

この画像の「Name resolution component」としてk8sDNSが利用されているのです。ローカル環境のmDNSにせよk8sDNSにせよ、特に別のDNSなどを使わなくても別アプリケーション呼び出しができるのは、とても楽ですね。

ログを確認する

ところで、これまでコンソールに出ていたログはどこに出ているのでしょうか。k8sではログを見るコマンドがあるので、そちらを使ってログを確認することができます。

まずはpodの一覧を取得します。

kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
hello-app-fb644cd5d-tsxmh     2/2     Running   0          3h26m
invoke-app-745b5f4bf6-chj5w   2/2     Running   0          83m

このうち、hello-appのログは、次のコマンドで見ることができます。

kubectl logs hello-app-fb644cd5d-tsxmh hello

次のようなログが表示されるはずです。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.6.1)

2021-12-12 06:23:28.001  INFO 1 --- [           main] n.c.e.d.advent.hello.HelloApplication    : Starting HelloApplication v1.0.0 using Java 11.0.13 on hello-app-fb644cd5d-tsxmh with PID 1 (/workspace/BOOT-INF/classes started by cnb in /workspace)
2021-12-12 06:23:28.004  INFO 1 --- [           main] n.c.e.d.advent.hello.HelloApplication    : No active profile set, falling back to default profiles: default
2021-12-12 06:23:29.986  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-12-12 06:23:29.996  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-12-12 06:23:29.997  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.55]
2021-12-12 06:23:30.096  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-12-12 06:23:30.096  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1925 ms
2021-12-12 06:23:30.681  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-12-12 06:23:30.702  INFO 1 --- [           main] n.c.e.d.advent.hello.HelloApplication    : Started HelloApplication in 3.316 seconds (JVM running for 3.826)
2021-12-12 06:23:30.891  INFO 1 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2021-12-12 06:23:30.892  INFO 1 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2021-12-12 06:23:30.893  INFO 1 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms

Spring Bootアプリケーションのログが出ていますね。

また、helloアプリケーションのDapr側のログは次のコマンドで見ることができます。

kubectl logs hello-app-fb644cd5d-tsxmh daprd
time="2021-12-12T09:54:35.62529075Z" level=info msg="starting Dapr Runtime -- version 1.5.1 -- commit c6daae8e9b11b3e241a9cb84c33e5aa740d74368" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime type=log ver=1.5.1
time="2021-12-12T09:54:35.625323485Z" level=info msg="log level set to: info" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime type=log ver=1.5.1
time="2021-12-12T09:54:35.625564411Z" level=info msg="metrics server started on :9090/" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.metrics type=log ver=1.5.1
time="2021-12-12T09:54:35.625927922Z" level=info msg="loading default configuration" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime type=log ver=1.5.1
time="2021-12-12T09:54:35.625951378Z" level=info msg="kubernetes mode configured" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime type=log ver=1.5.1
time="2021-12-12T09:54:35.625958635Z" level=info msg="app id: hello-app" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime type=log ver=1.5.1
(略)
time="2021-12-12T09:54:35.721210634Z" level=info msg="application protocol: http. waiting on port 8080.  This will block until the app is listening on that port." app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime type=log ver=1.5.1
time="2021-12-12T09:54:35.721505377Z" level=info msg="starting workload cert expiry watcher. current cert expires on: 2021-12-13 10:09:35 +0000 UTC" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime.grpc.internal type=log ver=1.5.1
time="2021-12-12T09:54:43.551406906Z" level=info msg="application discovered on port 8080" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime type=log ver=1.5.1
time="2021-12-12T09:54:44.133351345Z" level=info msg="application configuration loaded" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime type=log ver=1.5.1
time="2021-12-12T09:54:44.133506244Z" level=info msg="actor runtime started. actor idle timeout: 1h0m0s. actor scan interval: 30s" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime.actor type=log ver=1.5.1
time="2021-12-12T09:54:44.13353591Z" level=info msg="dapr initialized. Status: Running. Init Elapsed 8507.587964ms" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime type=log ver=1.5.1
time="2021-12-12T09:54:44.222137967Z" level=info msg="placement tables updated, version: 0" app_id=hello-app instance=hello-app-fb644cd5d-tsxmh scope=dapr.runtime.actor.internal.placement type=log ver=1.5.1

Dapr側のログも確認ができました。

アプリケーションの起動に失敗したり、うまく繋がらなかったり、例外が起きてしまったりした時には、前回説明した kubectl describe pods コマンドや、この kubectl logs コマンドで詳細を確認することで状況を把握することができるでしょう。

まとめ

  • k8s上にDaprアプリケーションを複数デプロイすれば、Invoke APIを使って相手のapp-idを指定してアクセスすることができます
  • k8s環境では DAPR_HTTP_PORT は3500番で固定になります
  • k8s環境ではk8sDNSが利用されます
  • ログは kubectl logs コマンドで確認することができます

だいぶk8sにも慣れてきましたかね!? それでは、また明日!

Dapr Advent Calendar 13日目 - Dapr + k8sでHello World

こんにちは Dapr Advent Calendar 13日目です。ついに半分を超えましたよ! 人間、やればできるもんなんですね!! いや、まだ終わってないですけどね。

k8sでDaprのHello World

前回のエントリーではk8sを使わずに分散環境で使ってみようと試みましたが、今回からは公式ドキュメントでも推奨されているとおりk8sを使って試してみます。

そんなに難しくはないので、k8sをこれまで触ってこなかったという方も、この機会に気軽に試してもらえれば嬉しいです。

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

Dapr + k8s環境のセットアップ

ツールのインストール

k8s上でDaprを利用するために、次のツールをインストールする必要があります。

Dapr CLIはこれまでのエントリーでも使ってきた、Daprのセットアップや実行をするためのCLIツールです。

dockerは仮想化環境構築ツールです。

kubectlはk8sコマンドラインで操作するためのツールです。ローカル環境のk8sでも、リモートやクラウド上のk8sであっても、同じようにコマンドで操作ができます。

minikubeはローカル環境にk8sを構築するためのツールです。minikube以外にもローカル環境にk8sを構築するためのツールはあるのですが、Daprのドキュメントでminikubeが使われていたため、これを使うことにしました。

インストール手順は、それぞれのURLから確認してください。

minikubeを起動する

まずはローカル環境でk8sを動作させるため、minikubeを起動します。

minikube start --cpus=4 --memory=4096

(※2021-12-16追記) オプションを指定しないと2CPU、メモリ2GBで運用しようとするので、アプリを6つほど起動しようとするとリソース不足になります。

数十秒ほど掛かって

🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

このような表示が出たら起動は完了です。

起動したminikubeにkubectlコマンドでアクセスしてみましょう。念のため、kubectlの接続先がminikubeになっていることを確認します。

kubectl config get-contexts

kubectlコマンドの接続先の一覧が表示されます。

CURRENT   NAME       CLUSTER    AUTHINFO   NAMESPACE
*         minikube   minikube   minikube   default

minikubeしか接続先がない場合は、このように表示されます。

複数の接続先が表示された場合は、minikubeCURRENT* がついているかを確認してください。もし別の環境に接続していた場合は、次のコマンドでminikubeを参照するようにします。

kubectl config use-context minikube

これでkubectlコマンドがminikubeに繋がるようになりました。

podの一覧を見る

kubectlコマンドを使って、minikube上にあるpodの一覧を見てみましょう。

「pod」とは、1つ〜複数のコンテナをまとめたリソースです。たとえばDaprでは、アプリケーションのコンテナとDaprのコンテナを一つの「pod」にまとめて利用します。

次のコマンドでpodの一覧を表示します。

kubectl get pods -A

次のように表示されます

NAMESPACE              NAME                                         READY   STATUS    RESTARTS   AGE
kube-system            coredns-558bd4d5db-h579g                     1/1     Running   0          1d
kube-system            etcd-minikube                                1/1     Running   0          1d
kube-system            kube-apiserver-minikube                      1/1     Running   0          1d
kube-system            kube-controller-manager-minikube             1/1     Running   0          1d
kube-system            kube-proxy-bgt78                             1/1     Running   0          1d
kube-system            kube-scheduler-minikube                      1/1     Running   0          1d
kube-system            storage-provisioner                          1/1     Running   0          1d
kubernetes-dashboard   dashboard-metrics-scraper-7976b667d4-9ffr8   1/1     Running   0          1d
kubernetes-dashboard   kubernetes-dashboard-6fcdf4f6d-z5vjh         1/1     Running   0          1d

何やらk8s関連のpodだけが起動しているようですね。詳しくは僕も知らないので、フーンという感じで眺めるだけにしておきます。

Daprのセットアップ

それではminikube上にDapr関連のpodを作成しましょう。次のコマンドでDaprの初期化を行います。

dapr init -k

オプションの -kk8sを対象とするという意味です。

初期化が終わったら、もう一度podの一覧を見てみましょう。

kubectl get pods -A

次のように表示されるはずです。

NAMESPACE              NAME                                         READY   STATUS    RESTARTS   AGE
dapr-system            dapr-dashboard-57b4db56fc-tng2j              1/1     Running   0          54s
dapr-system            dapr-operator-5b4b68b5c5-cljjl               1/1     Running   0          54s
dapr-system            dapr-placement-server-0                      1/1     Running   0          54s
dapr-system            dapr-sentry-c6b746cdf-nkvnv                  1/1     Running   0          54s
dapr-system            dapr-sidecar-injector-6f749dbf87-vr9f6       1/1     Running   0          54s
kube-system            coredns-558bd4d5db-h579g                     1/1     Running   0          1d
kube-system            etcd-minikube                                1/1     Running   0          1d
kube-system            kube-apiserver-minikube                      1/1     Running   0          1d
kube-system            kube-controller-manager-minikube             1/1     Running   0          1d
kube-system            kube-proxy-bgt78                             1/1     Running   0          1d
kube-system            kube-scheduler-minikube                      1/1     Running   0          1d
kube-system            storage-provisioner                          1/1     Running   0          1d
kubernetes-dashboard   dashboard-metrics-scraper-7976b667d4-9ffr8   1/1     Running   0          1d
kubernetes-dashboard   kubernetes-dashboard-6fcdf4f6d-z5vjh         1/1     Running   0          1d

NAMESPACEdapr-system となっているpodが5つ追加されました。これらがDaprを運用するために必要なpod群です。

Dapr CLIのコマンドでも、状況を確認できます。

dapr status -k

次のように表示されます。

  NAME                   NAMESPACE    HEALTHY  STATUS   REPLICAS  VERSION  AGE  CREATED              
  dapr-dashboard         dapr-system  True     Running  1         0.9.0    5m   2021-12-13 00:00.00  
  dapr-sidecar-injector  dapr-system  True     Running  1         1.5.1    5m   2021-12-13 00:00.00  
  dapr-placement-server  dapr-system  True     Running  1         1.5.1    5m   2021-12-13 00:00.00  
  dapr-operator          dapr-system  True     Running  1         1.5.1    5m   2021-12-13 00:00.00  
  dapr-sentry            dapr-system  True     Running  1         1.5.1    5m   2021-12-13 00:00.00  

同じように5つのpodが動いていることを確認できます。

ちなみに、これらのpodを削除したい場合は、次のコマンドを実行します。

dapr uninstall -k

とても簡単ですね。

Hello Worldアプリケーションをk8sの上で動かす

続いて、Hello Worldアプリケーションをk8s上で動かしてみましょう。Hello WorldアプリケーションのソースコードGitHubにあります。

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

この中から Dapr Advent Calendar 2日目 - DaprでHello World で作成した hello アプリケーションを使います。

作成したアプリケーションのおさらい

helloアプリケーションのメインロジックである HelloControllerソースコードは次のようになっています。

(hello) HelloController.java

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Map<String, String> hello() {
        return Map.of("message", "Hello, world!");
    }
}

Hello, world! を含んだメッセージを返すだけの簡単なアプリケーションです。

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

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

イメージ作成は docker build コマンドでもできるのですが、Spring Bootでは mvn spring-boot:build-image コマンドでBuildpackを使って簡単にイメージ作成をすることができるので、これを利用しましょう。できあがるイメージのサイズも最適化されているし、Javaのオプションなども適切に指定されるため、手軽に最適なイメージを作成することができます。

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

eval $(minikube docker-env)

このコマンドを実行せずにイメージ作成すると、作成したはずのイメージをminikubeが見つけられない、というハメになります。僕も何度となくハマりました。

それではイメージを作成します。次のコマンドを実行します。

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

初回はビルドするためのイメージなどをダウンロードするため時間が掛かります。

作成が終わったら、イメージ一覧を見てみましょう。

docker images

次のようなイメージが見つかるはずです。

hello                                     1.0.0                 d0a362971d5c   41 years ago    261MB

41年の由緒正しい歴史あるイメージができあがりました。なぜか1980年に作られたことになってしまうみたいですね。まぁ気にしないでおきましょう。

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

それでは、作成したイメージをk8sで動かしてみましょう。

イメージをk8sにデプロイ

まずはイメージをk8sにデプロイします。次のコマンドを実行します。

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

この kubectl apply -fマニフェストファイルを用いてk8sにデプロイするコマンドです。hello-app.yaml の詳しい説明は後ほど行います。

実行すると次のように表示されるはずです。

deployment.apps/hello-app created
service/hello-svc created

hello-app という名前のデプロイメントと、hello-svc という名前のサービスが作成されました。hello-app がpodで、hello-svc がpodに外部からアクセスできるようにするためのサービスです。

起動したpodを次のコマンドで確認しましょう。

kubectl get pods

次のように表示されるはずです。

NAME                        READY   STATUS    RESTARTS   AGE
hello-app-fb644cd5d-wf785   2/2     Running   0          20s

READY2/2 は、アプリケーションのコンテナと、Daprのコンテナの2つがそれぞれ起動していることを示しています。

起動には少し時間が掛かるため、起動中の場合は READY2/2 になっていないこともありますが、何度か kubectl get pods コマンドを実行して 2/2 になるのを確認してください。

何度実行しても次のように表示される場合は、設定や環境に問題があります。

NAME                         READY   STATUS             RESTARTS   AGE
hello-app-5fd947547b-vs8lj   0/2     ImagePullBackOff   0          17s

もしこのように表示された場合は、次のコマンドで詳細を確認しましょう。

kubectl describe pods hello-app-5fd947547b-vs8lj

たとえばイメージが見つからない場合には、次のように表示されます。

  Normal   Scheduled  27s               default-scheduler  Successfully assigned default/hello-app-5fd947547b-vs8lj to minikube
  Normal   Pulling    9s (x2 over 27s)  kubelet            Pulling image "hello:1.0.0"
  Warning  Unhealthy  9s (x3 over 21s)  kubelet            Liveness probe failed: HTTP probe failed with statuscode: 500
  Warning  Unhealthy  9s (x3 over 21s)  kubelet            Readiness probe failed: HTTP probe failed with statuscode: 500
  Normal   Killing    9s                kubelet            Container daprd failed liveness probe, will be restarted
  Warning  Failed     6s (x2 over 24s)  kubelet            Failed to pull image "hello:1.0.0": rpc error: code = Unknown desc = Error response from daemon: pull access denied for hello, repository does not exist or may require 'docker login': denied: requested access to the resource is denied
  Normal   Started    6s (x2 over 24s)  kubelet            Started container daprd
  Warning  Failed     6s (x2 over 24s)  kubelet            Error: ErrImagePull
  Normal   Pulled     6s (x2 over 24s)  kubelet            Container image "docker.io/daprio/daprd:1.5.1" already present on machine
  Normal   Created    6s (x2 over 24s)  kubelet            Created container daprd
  Normal   BackOff    5s (x3 over 23s)  kubelet            Back-off pulling image "hello:1.0.0"
  Warning  Failed     5s (x3 over 23s)  kubelet            Error: ImagePullBackOff

この辺りはk8sの使いこなしの話になるので、いったんここでは詳細は割愛します。

もしこのような問題が起きたら、ここまでの手順を見直して、抜けていることなどないか確認してください。

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

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

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

kubectl port-forward service/hello-svc 8080:8080

次のように表示されるはずです。

Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080

これでローカルPCの8080番ポートから、minikubeの8080番ポートにアクセスできるようになりました。

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

curl localhost:8080/hello

次のメッセージが取得できるはずです。

{"message":"Hello, world!"}

無事にアクセスできましたね。

作成したpodの削除

作成したpodは次のコマンドで削除できます。

kubectl delete -f k8s/hello-app.yaml

実行すると次のように表示されます。

deployment.apps "hello-app" deleted
service "hello-svc" deleted

これでpodとServiceがそれぞれ削除されました。

また、次のコマンドでそれぞれ削除することもできます。

kubectl delete deployment hello-app
kubectl delete service hello-svc

マニフェストファイルがない場合や、マニフェストファイルを書き換えてしまった場合には、この方法を用いても良いでしょう。

やったことの解説

それでは、今回行ったことの説明です。

セットアップのおさらい

環境構築に関わる部分をおさらいします。

  • kubectlをインストールした
  • minikubeをインストールした
  • dapr init -k で環境を構築した
  • eval $(minikube docker-env) でminikubeのイメージレジストリにするイメージが作成されるようにした
  • mvnw spring-boot:build-image でイメージを作成した
  • kubectl apply -f k8s/hello-app.yaml でアプリケーションをデプロイした

直接構築に関わる部分はこれくらいですね。

アプリケーションをデプロイするためのマニフェストファイル

それでは説明を飛ばしていたマニフェストファイルについて説明します。ファイルは次のようになっています。

k8s/hello-app.yaml

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

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

後半の --- より上がpodのデプロイの設定、下がサービスの設定です。k8sマニフェストファイルは、--- で区切ることで1ファイル内に複数の設定を書くことができます。

デプロイの設定

まずはデプロイ設定の主要な部分を説明します。デプロイ設定はpodを作成するための設定です。

metadata.namehello-app がアプリケーション名です。podの名前などに利用されます。

spec.template.metadata.annotations がDapr関連の設定です。

        dapr.io/enabled: "true"
        dapr.io/app-id: "hello-app"
        dapr.io/app-port: "8080"

dapr.io/enabledtrue にすると、このアプリケーションのサイドカーとしてDaprが適用されます。

dapr.io/app-id はアプリケーションのIDで、dapr run コマンドの --app-id に該当します。

dapr.io/app-port はアプリケーションのポート番号で dapr run コマンドの --app-port に該当します。

つまり dapr run --app-id hello-app --app-port 8080 でアプリケーションを起動するのと同等となります。

spec.template.spec.containers がコンテナ関連の設定です。

      - name: hello
        image: hello:1.0.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080

imagePullPolicyIfNotPresent はローカルにイメージがなければDocker Hubを見に行くという設定です。ここの設定を Always にしてしまうと常にDocker Hubを見に行ってしまい、Docker Hubに hello:1.0.0 などないというエラーが発生してしまいます。よくハマりました。

ports にはアプリケーション側のポート番号を指定します。このポートをpod外に公開します。

これでこのアプリケーションがDaprとともに起動するpodができ、podの8080番ポートがpod外に公開されることになります。

サービスの設定

つづいて、サービス設定の主要部分を説明します。podを外部に公開するための設定です。

metadatea.name がサービスの名称です。

spec がポートに関する設定です。

spec:
  selector:
    app: hello
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080
  type: LoadBalancer

selector.app で対象となるpodのラベルを指定します。ここでは hello-app のラベルである hello を指定します。

ports でpodのどのポートを公開するかを設定します。ここでは TCP8080 番ポートを、minikubeの 8080 番ポートとして公開するように指定しています。

type がこのサービスのタイプです。LoadBalancer を指定することで、podのレプリカを複数起動した際に、ラウンドロビン形式でリクエストを分散させることができます。

これでminikubeの8080番ポートにアクセスすると hello-app の8080番ポートにアクセスできるようになる、ということです。

ただしminikubeの各ポートは、通常そのままでは外部からはアクセスできないため kubectl port-forward service/hello-svc 8080:8080 を使うことで localhost:8080 をminikubeの8080番ポートのフォワードして、アクセスができるようになったのです。

f:id:cero-t:20211213084927p:plain
幾重にも重なるk8sのアクセス構造

この辺りは少し分かりにくいところではあるのですが、使っていくうちに慣れていくと思います。

あれ? Daprは使ってない?

ここまで説明してきましたが、もしかしたら「あれ、Daprにアクセスしてないのでは?」と気づいた方もいらっしゃるかも知れません。その通りです。

アプリケーションを8080番ポートで起動し、podの8080番ポートを公開し、サービスを使ってminikubeの8080番ポートにマッピングし、ポートフォワードでlocalhost:8080からアクセスできるようになりました。つまりアクセスしたのはあくまでもアプリケーションの8080番ポートです。

今回の手順では、アプリケーションをDaprとともに起動しましたが、あくまでもアプリケーションのポートを外部からアクセスできるようにしただけに過ぎず、Daprは通過すらしていません。Daprの機能を利用するような処理は、明日以降のブログで紹介していきます。

まとめ

  • Daprをk8sで使うために、Dapr CLI、docker、kubectl、minikubeをインストールしましょう
  • dapr init -k で環境を構築しましょう
  • eval $(minikube docker-env) でminikubeのイメージレジストリを指すようにしましょう
  • mvnw spring-boot:build-image でイメージを作成しましょう
  • kubectl apply -f k8s/hello-app.yaml でアプリケーションをデプロイしましょう
  • DeploymentとServiceの設定を使って、アプリケーションを外部からアクセスできるようにします
  • ポートフォワードを使ってlocalhostからminikubeにアクセスできるようにします

なお、今回はアプリケーションの8080番ポートにアクセスしましたが、サービスの設定などで3500番ポートを公開してアクセスすれば、外部からDaprにアクセスすることもできます。しかし、アプリケーションのポートを公開するべきか、Daprのポートを公開するべきかは慎重な設計が必要になるところです。その辺りについては、またいずれ触れたいと思います。

それでは、また明日!

Dapr Advent Calendar 12日目 - Daprをk8s以外の分散環境で使う

こんにちは Dapr Advent Calendar 12日目です。昨日は @tmak_tw さんの Daprの回復性を支える仕組み 現状と展望 でした。実際に僕もインフラ側の問題起きた時にDaprを使ったアプリが自動的に復旧しない*1とか、サーキットブレイカーパターンが使えないとか、その辺りに課題感を感じていたので興味があるエントリーでした。

さて、今回からは「分散環境編」です。

Daprの運用環境を考える

これまでのエントリーでは、ローカルの開発環境でDaprを使ってきました。これを運用環境や試験環境で、複数台のサーバでクラスタリングして動作させるにはどうすれば良いのでしょうか。

Daprのドキュメントなどでは、基本的にk8sを使うことが推奨されています。実際に僕もk8sAmazon EKS)上で運用しており、k8sを使ったのは初めてだったものの噂に聞いていたほどは難しくなく、意外と扱いやすいなと思いました。とは言え、k8sを使うことにまだまだ抵抗がある人も多いと思います。

分散環境編の1回目は、まずk8sを使わずにDaprのシステムを運用する方法を考えてみます。

f:id:cero-t:20211212084402p:plain
今回動かすアプリケーション

分散環境の何が問題になるのか?

ローカルPCでの開発から、分散環境(特にクラウド)になることで、何が困るのか整理してみましょう。

  1. Daprを運用するために、Dockerをインストールする必要がある
  2. Invoke APIで名前解決をする際に、他のサーバにあるDaprアプリケーションを見つけられない

この2つです。

この2つの問題以外はDaprの設定ファイルに従って外部のサーバやサービスを利用するため、特に問題にはならないはずです。もちろん設定ファイルやアプリケーションのデプロイが面倒だという問題はあるでしょうけど、それはDaprの運用特有の問題ではありません。

では、この問題の解決方法を1つずつ考えていきましょう。

DockerなしでDaprを利用する

まずは1つめ、Daprの運用にDockerが必要となる件なのですが dapr without dockerググると、あっさりこんなドキュメントが見つかりました。

How-To: Run Dapr in self-hosted mode without Docker | Dapr Docs

Daprの初期化を dapr init --slim というオプション付きで実行すれば、DockerなしでDaprをインストールできるようです。RedisやZipkinはインストールされませんが、必要であれば別に立てるでしょうから問題はありませんね。

何だかあっさり解決してしまいました。

名前解決をどうするか

そして2つめの名前解決についてです。これに関して、このドキュメントを見ると、

Service invocation overview - Round robin load balancing with mDNS | Dapr Docs

名前解決のためのmDNSに関して、こんな記述がありました。

These instance can be on the same machine or on different machines.

複数のマシンでも問題なく動くそうです。今回はこれを試してみましょう。

複数のPCでDaprを動作させる

それでは2台のPCで、それぞれDockerなしでDaprを動かしてみることにします。2台もPCを用意できない人は、ピザでも食べながらこの続きを読んでください。

Daprのアンインストール

これまでDaprを使っていた場合は、いったんDaprをアンインストールします。

dapr uninstall

また、残っているDockerコンテナも停止します。

docker stop `docker ps -q --filter name=dapr_` 
docker rm `docker ps -aq --filter name=dapr_` 

さらに ~/.dapr もフォルダごと削除しました。

これでアンインストールは完了です。

Daprをスタンドアロンモードで初期化

続いて2台のPCでDaprをセットアップします。

Dapr CLIが入っていない場合は、ドキュメントに従ってDapr CLIインストールします。

docs.dapr.io

続いてDaprをスタンドアロンモードで初期化します。

dapr init --slim

インストールが完了したら、Dockerコンテナの一覧を見てみます。

docker ps -a --filter name=dapr_

出力結果はこうなっているはずです。

CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

通常のインストールではDaprやRedis、Zipkinなどのコンテナが作られたのですが、--slim オプションつきの場合は作られませんでした。

またcomponentsディレクトリを見ると

ls ~/.dapr/components

どうやら空のようです。

設定ファイルも確認しておきましょう。

cat ~/.dapr/config.yaml

ファイルはできているのですが、中はこうなっています。

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: daprConfig
spec: {}

何の設定もありませんね。

Dockerコンテナもなく、設定も最小限となっているDapr環境ができました。これを2台分セットアップします。

2台のPCでそれぞれアプリを起動してアクセス

それでは2台のPCでそれぞれアプリを起動してみましょう。

f:id:cero-t:20211212084422p:plain
これから動かすアプリケーション

ソースコード

サンプルアプリケーションのソースコードGitHubに置いてあります。

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

今回はこのうち、3日目のエントリーで説明したInvokeの機能を使います。

2台で起動

まず1台目のPCでHello Worldのアプリケーションを起動します。

cd (GitHubのディレクトリパス)/dapr-advent-2021/hello
dapr run --app-id hello-app --app-port 8080 ../mvnw spring-boot:run

そしてもう一台のPCで、Invokeアプリケーションを起動します。

cd (GitHubのディレクトリパス)/dapr-advent-2021/invoke
dapr run --app-id invoke-app --app-port 8081 -- ../mvnw spring-boot:run -Dspring-boot.run.profiles=dapr

そして、invokeを起動した方のPCで、次のコマンドを実行します。

curl localhost:8081/invokeHello

結果が取得できたはずです。

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

2台のPCで別々に動いていたアプリも問題なく通信ができました。

念のため、Hello WorldアプリケーションをCtrl + Cで停止させた後、もう一度アクセスしてみます。

curl localhost:8081/invokeHello

今度はエラーとなったはずです。

{
  "timestamp": "2021-12-12T00:00:00.000+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/invokeHello"
}

Invokeアプリのコンソールにもエラーが出ているはずです。

== APP == 2021-12-10 00:00:00.000 ERROR 80260 --- [nio-8081-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.client.HttpServerErrorException$InternalServerError: 500 Internal Server Error: "{"errorCode":"ERR_DIRECT_INVOKE","message":"fail to invoke, id: hello-app, err: failed to invoke target hello-app after 3 retries"}"] with root cause

3回リトライしても hello-app にアクセスできなかったというエラーです。つまり、別のPCで動いていたHello Worldにアクセスしようとしていたことが分かりますね。

別々のマシンであっても、DaprはmDNSを使ってアクセスができるようです。

AWSでも運用できるか?

Amazon EC2でDaprを動作させる?

ここまでで、2台のPCで通信できることは確認できましたが、同じことをAWSのEC2でもできるのでしょうか。

試しにやってみたのですが、残念ながら、EC2インスタンス上で動くDaprアプリケーションから別のEC2インスタンス上で動くDaprアプリケーションにはアクセスができませんでした。

mDNSはマルチキャストが使える環境でなければ使えないのですが、AWSのEC2ではマルチキャストが使えないため(たぶん)、mDNSで通信することはできないようです。Invoke API以外のAPIは使えるようなのですが、Invoke APIこそがDaprの要なので、それがなくては話にならないですよね。

また、mDNSの代わりにHashiCorp Consulを使えば良いようですが、僕がConsulに不慣れなので、ちょっと諦めてしまいました。

ちなみにAzureにはManaged HashiCorp Cosul Serviceがあったので、それで試してみようとしたのですが、画面に従ってインスタンスの作成を進めていただけなのに、Consulのインスタンス作成に失敗してしまい、カッとなって諦めてしまいました。無念。

Amazon ECSでDaprを動作させる?

EC2がダメなら、ECS(Elastic Container Service)ならどうか、と思って探していたところ、こういうIssueを見つけました。

AWS ECS name resolution component · Issue #1197 · dapr/components-contrib · GitHub

Issueを斜め読みした限りでは、ECSのサービスディスカバリを使うのではなく、外部DNSを利用できるようにすることで、ECSにも対応させようという試みのようです。この辺りがきちんと対応されれば、ECSで運用することもでそうです。

まとめ

  • Daprの運用にDockerは必須ではない
  • マルチキャストが使える環境であれば、mDNSを用いて複数のサーバ上でDaprを運用することができる
  • AWSのEC2はマルチキャストが使えないためmDNSが使えない。Consulを使えば運用できる可能性がある
  • AWSのECSにはいずれ対応されそう

という感じで、名前解決のところがもう少し強化されれば、k8sを使わずに運用することもできそうですね。SpringユーザーとしてはEurekaに対応してくれれば、サッとサーバを立てて運用できるのにな、と思うのですけどね。自分で実装するしかないですかね😇

そんなわけで、少し寄り道したような形になりましたが、次回からは王道のk8sを使った運用について説明していきます。

それでは、また!

2021-12-18 追記

アンインストール手順について、うらがみさんからコメントをもらいました。知らなかった!

rm -rf コマンドをブログに書きたくなかったので、手で削除するなどと言ってごまかしてましたが、dapr uninstall --all で削除するのが安全で良いですね。

*1:具体的にはk8sクラスタAWSのスポットインスタンスを使っていた時に、インスタンスの削除→再作成が行われたあと、Daprを使ったアプリが復旧しないことがありました

Dapr Advent Calendar 10日目 - Dapr Java SDKを使う

こんにちは Dapr Advent Calendar 10日目です。今回は「基本編」の最終回となります。ひとつの節目を迎えました!

Dapr Java SDKを使ってみる

ここまでのAdvent Calendarで、Daprの主要な機能は(Actorを除き)一通り説明しました。そこで今回は、Daprの機能ではなくDaprのAPIJavaから利用しやすくするためのDapr Java SDKについて説明します。なお、Actorについては僕が使った経験がないのと、機能が多くて大変だったので、いったんスキップすることにしました。ごめんね!

Dapr Java SDKのドキュメントはここにあります。

docs.dapr.io

Dapr Java SDKJava 11以降を求めており、なかなか現代的です。いちおうJava 8向けのコンパイルをしているようなので、Java 8でも動作しそうですけどね。

今回作成するアプリケーションのソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021/tree/main/java-sdk

今回は、これまで書いてきたソースコードがDapr Java SDKを使うとどう変わるかを中心に説明します。

pom.xmlの修正

Dapr Java SDKを利用するために、まずは io.dapr:dapr-sdkをdependenciesに追加します。

pom.xml

        <dependency>
            <groupId>io.dapr</groupId>
            <artifactId>dapr-sdk</artifactId>
            <version>1.3.1</version>
        </dependency>

ここでちょっと注意が必要で、dapr-sdkokhttp のバージョン4.xに依存しているのですが、親のモジュールとして spring-boot-starter-parent を使っていると、okhttp のバージョン3.xが使われてしまうため、起動時にエラーが起きてしまいます。

そのため spring-boot-starter-parent を使っている場合は、okhttp もあわせてdependenciesに追加します。

pom.xml

        <dependency>
            <groupId>io.dapr</groupId>
            <artifactId>dapr-sdk</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.0</version>
        </dependency>

もちろん spring-boot-starter-parent を外して自分でビルド設定を書く方向でも構いません。

これで準備は完了です。

Hello WorldInvoke API

それでは、まずHello Worldと、それを呼び出すAPIを作成します。これはそれぞれDapr Advent Calendarの2日目3日目の内容となります。

Hello World

Hello Worldは特に何も変わりありません。

JavaSdkController.java

@GetMapping("/hello")
public Map<String, String> hello() {
    return Map.of("message", "Hello, world!");
}

DaprのAPIを利用しているわけではなく、呼び出されるだけなので、何も変わらないですね。

Hello Worldを呼び出すAPI

続いて、このHello Worldを外部から呼び出すAPIです。Dapr Advent Calendar3日目では、このように実装していました。

InvokeController.java(分かりやすいよう一部改変しています)

@RestController
public class InvokeController {
    private RestTemplate restTemplate;

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

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

    @GetMapping("/invokeHello")
    public Map<String, ?> invokeHello() {
        Map<?, ?> result = restTemplate.getForObject(baseUrl + "/hello", Map.class);
        return Map.of("baseUrl", baseUrl, "remoteMessage", result);
    }
}

DaprのInvoke APIのURLに対して、RestTemplate を使ってアクセスしていました。

これに対してDapr Java SDKを使うと、次のように変わります。

JavaSdkController.java

@RestController
public class JavaSdkController {
    private DaprClient daprClient;

    public JavaSdkController(DaprClient daprClient) {
        this.daprClient = daprClient;
    }

    @GetMapping("/invokeHello")
    public Map<String, ?> invokeHello() {
        Map<?, ?> result = daprClient.invokeMethod("java-sdk-app", "hello", null, HttpExtension.GET, Map.class)
                .block();
        return Map.of("remoteMessage", result);
    }
}

利用するクライアントが RestTemplate から DaprClient に変わりました。

このDaprClientには invokeMethod というInvoke APIを呼び出すメソッドが定義されています。第1引数が app-id で、第2引数が method であるAPIのパス、第3引数がリクエストボディ、第4引数がHTTPメソッド、第5引数が戻り値の型です。他にも引数のパターンが異なる同名のメソッドがいくつか定義されています。

Dapr Java SDKを使えば、Dapr APIのURLの書き間違いや引数の間違いなどを防ぎやすくなるのが、一つのメリットですね。

またDaprClientはProject Reactorを利用しているため、単独の戻り値はMono、複数の戻り値はFluxとなっています。Project Reactorはノンブロッキングな処理を書くためのライブラリですが、詳しく説明するととんでもなく長くなるので、ここでは説明を割愛します。ごめんね!

DaprClientがProject Reactorを使っているのであれば、アプリケーション側もSpring WebFluxを使って良い感じにノンブロッキングな処理にすることもできるのでしょうけど、ここでは簡単にするために block() メソッドでブロックしています。ごめんね!

Daprを使ったアプリケーションの起動

それではDaprを使ってアプリケーションを起動します。設定ファイルの説明は省略しましたが、アプリケーションのポートは8089番にしています。

dapr run --app-id java-sdk-app --app-port 8089 ../mvnw spring-boot:run

次のコマンドでアクセスします。

curl localhost:8089/hello

次のメッセージが表示されます。

{
  "message": "Hello, world!"
}

もう一つのAPIも確認しましょう。

curl localhost:8089/invokeHello

次のメッセージが表示されます。

{
  "remoteMessage": {
    "message": "Hello, world!"
  }
}

それぞれAPIが正常に動作していることを確認できました。

今回は簡単にするために同じアプリケーション内に2つのAPIを作って呼び出しを行いましたが、もちろん別々のアプリケーションとして作っても構いません。問題なくDaprを経由して呼び出すことができます。

Dapr Java SDKのSpring Boot向け機能を使ってみる

これまでのDapr Advent CalendarでやってきたすべてのコードをDapr Java SDKに置き換えても良いのですが、そんなことを説明してもあまり面白くないでしょうから、アプリケーションの作成は次のメッセージング(Pub/sub API)までにします。

ちなみにGitHubにあるサンプルコードには、データストアの読み書きをするメソッドも書いているので、興味がある人は読んでみてください。

pom.xmlの修正

この先で、Subsribe側の処理を作るところでDapr Java SDKのSpring Boot向け機能を使うため、pom.xmlを少し修正します。

pom.xml

        <dependency>
            <groupId>io.dapr</groupId>
            <artifactId>dapr-sdk-springboot</artifactId>
            <version>1.3.1</version>
        </dependency>

pom.xmldapr-sdkdapr-sdk-springboot に変更します。dapr-sdk-springbootdapr-sdk に依存しているため、変更で構いません。

これで何が変わるかは、後ほど説明します。

Pub/sub API

メッセージのPublish

まずはメッセージのPublish処理です。Dapr Advent Calendar5日目では、このように実装していました。

PublishController.java

@PostMapping("/publish")
public void publish(@RequestBody Object message) {
    restTemplate.postForObject(pubsubUrl, message, Void.class);
}

RestTemplateを使ってDaprのAPIにオブジェクトをPOSTするだけですね。

これがDaprClientを使うと次のように変わります。

JavaSdkController.java

@PostMapping("/publish")
public void publish(@RequestBody Object message) {
    daprClient.publishEvent("pubsub", "my-message", message)
            .block();
}

処理の長さに違いはありませんね。

DaprClientの publishEvent メソッドはPublishを行うもので、引数にpubsub名、メッセージトピック、メッセージ本体を受け取ります。非常に分かりやすいですね。

メッセージのSubscribe

続いてメッセージのSubscribe処理です。Dapr Advent Calendar5日目では、このように実装していました。

SubscribeController.java

@PostMapping("/subscribe")
public void subscribe(@RequestBody String message) {
    System.out.println(message);
}

取得した文字列(JSON)を標準出力に出力しているだけです。

そしてSubscriptionのための設定ファイルも作成していました。

~/.dapr/components/subscription.yaml

apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
  name: subscription
spec:
  pubsubname: pubsub
  topic: my-message
  route: /subscribe
scopes:
  - subscribe-app

pubsub名やメッセージトピック、また呼び出すアプリケーションやエンドポイントURLなどを記載するものです。

これがDapr Java SDK (extension for Spring Boot) を使うと次のように変わります。

JavaSdkController.java

@Topic(pubsubName = "pubsub", name = "my-message")
@PostMapping("/subscribe")
public void subscribe(@RequestBody String message) {
    System.out.println(message);
}

処理は全く同じなのですが @Topic というアノテーションがついています。このアノテーションdapr-sdk-springboot が提供しています。

このアノテーションさえつければ、Subscriptionの設定ファイルは必要ありません。アイエエエエ、ナンデ!? という感じなのですが、その辺りの仕組みは後ほど説明します。

Daprを使って確認

いったんアプリケーションを停止させた後、Daprを使ってアプリケーションを再起動します。

dapr run --app-id java-sdk-app --app-port 8089 ../mvnw spring-boot:run

特にコンポーネント設定ファイルなどは指定しないため、メッセージブローカーとして標準のRedisが使われることになります。

次のコマンドでpublishします。

curl -XPOST "localhost:8089/publish" -H "Content-type:application/json" -d '{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}'

コマンドを実行すると、コンソールに次のようなメッセージが表示されるはずです。

== APP == {"pubsubname":"pubsub","datacontenttype":"application/json","source":"java-sdk-app","type":"com.dapr.event.sent","topic":"my-message","id":"011754a8-c83d-4551-ac77-2fab25ec40e4","specversion":"1.0","traceid":"00-b493697e0f972f44572871d7010d680f-14ad50d87dc50de5-01","data":{"name":"Shin Tanimoto","twitter":"@cero_t"}}

問題なくSubscribeできていました。

繰り返しになりますが、Subscribeのための設定ファイルを作ることなく、Subscribeができたのです。

アノテーションだけでSubscriberを実装する仕組み

これまでは、Daprの挙動を変えるにはDaprの設定ファイルを書く必要がありました。しかし今回は、Dapr側ではなくアプリケーション側にアノテーションを書くだけで、DaprのSubscribeに相当する設定ができました。

Spring Cloudのようにアプリケーションの中で動くようなライブラリならまだしも、Daprのようなサイドカーがアプリケーション側のアノテーションをどういう風に扱っているのか、不思議ですよね。いえ、不思議じゃないよという人は、この項は読み飛ばしてください。

dapr-sdk-springbootがSubscribe情報を集約して外部公開している

dapr-sdk-springboot はとても小さいライブラリで、バージョン1.3.1の時点で5クラスしかありません。

github.com

いちおうソースコードは全部読んだのですが、分かったことは、@Topic アノテーションがついたメソッドの情報を集約して、外部から取得できるようにするためのAPIのエンドポイントを設けている、ということくらいでした。

そのエンドポイントはcurlコマンドで確認できます。

curl localhost:8089/dapr/subscribe 

次のようなレスポンスが返ってきます。

[
  {
    "pubsubName": "pubsub",
    "topic": "my-message",
    "route": "/subscribe",
    "metadata": {}
  }
]

これは確かに @Topic アノテーションがついたメソッドにあった情報です。しかしなぜこれで、Daprのsubscribeが有効になるのでしょうか。

Daprがアプリケーションのエンドポイントを使って設定する

実は /dapr/subscribe を用意するだけでSubscribeできることが、公式のリファレンスに記載されていました。

docs.dapr.io

Daprはアプリケーションの /dapr/subscribe というエンドポイントを参照し、その内容に従ってSubscribeの設定をするようです。最初からドキュメントをきちんと読んでおけって話ですよね・・・。

それでは実際にアプリケーション側のアクセスログを出して確認してみましょう。

application.properties

server.port=8089
server.tomcat.accesslog.enabled=true
server.tomcat.basedir=(任意のディレクトリ)

そしてDaprアプリケーション側を再起動します。

dapr run --app-id java-sdk-app --app-port 8089 ../mvnw spring-boot:run

すると、アプリケーション起動後に次のようなログが出ました。

127.0.0.1 - - [10/Dec/2021:00:00:00 +0900] "GET /dapr/config HTTP/1.1" 200 15
127.0.0.1 - - [10/Dec/2021:00:00:00 +0900] "GET /dapr/subscribe HTTP/1.1" 200 81

このログにある2つのエンドポイントが、アプリケーション起動時にDaprから呼ばれるようです。

こういう仕組みであれば、確かにアプリケーション側で定義したものをDapr側に反映することもできますね。なるほどなぁという感じです。

それで、Dapr Java SDKはどうなの?

さて、ここまでDapr Java SDKを使ってきましたが、どうでしょうか。

  • 型が明確になるから間違いが減る
  • IDEの自動補完が効くから、ドキュメントを読まなくても、どのようなAPIがあるか分かる
  • 環境変数からポート番号を取ってきたり、DaprのURLを設定ファイルに書いたりしなくて済む
  • 特にSubscribeの設定をアノテーションで設定できるのが便利

というメリットがあったと思います。

しかしその一方で

  • 開発時から常にDaprを使う必要がある
  • 設定だけで接続先を変えることができなくなる(モックに接続する、メッセージブローカーを迂回する、など)
  • APIの一部がちょっと分かりづらかったり、使いにくかったりする

というデメリットもあります。

僕自身は、特に開発時から常にDaprを使う必要になる点がデメリットとしては大きいと感じているため、今のところDapr Java SDKは使わない方向で進めています。もちろん最初からDaprを前提にしたほうが、Dapr化した時にハマらないから良いとか、IDEで自動補完できるところが初期の学習コストを下げられるから良い、という判断もあるでしょう。

なお、今回は長くなるので触れませんでしたが、分散トレーシングのための traceparent を伝播するような機能もDapr Java SDKにはありません。伝播させるためには他の仕組みも使って、うまく作り込む必要があります。もしDapr Java SDKを使うだけで、トレースIDが良い感じに伝播されるようなっていれば、また少し判断が変わっていたのかも知れませんね。

どうあれ、使う使わないの辺りは、組織や状況に応じてそれぞれで判断してもらえると良いかなと思います。

まとめ

  • Daprの提供するAPIを、DaprClientのメソッド経由で利用できる
  • DaprClientを使うと型や引数が明確になって間違いを減らせる
  • Dapr JavaSDKはProject Reactorを利用しており、ノンブロッキングな処理を書くことができる
  • Subscribeの設定ファイルの代わりに @Topic アノテーションをメソッドにつけるだけで済むようになる(要dapr-sdk-springboot)
  • 分散トレーシングのトレースIDを伝播するような機能はない

主要な機能とDapr Java SDKを説明し終わったので、これで基本編は終わりです。次回からはk8sを使う分散環境編となります。

なお、明日のエントリーは @tmak_tw さんが書いてくださるようです。Daprの回復性のあたりは、Daprの弱点のようにも思っていたところだったので、どんなエントリーなのか楽しみです!

それでは、また次回!

Dapr Advent Calendar 9日目 - DaprでOAuth2を使う

オーッス! Dapr Advent Calendar 9日目デーッス! 今日のテーマは OAuthッス! オーッス!

DaprでOAuth2による認可を行ってみよう

冒頭の元気な挨拶のことは忘れていただき、今回はDaprへのアクセスにOAuth 2.0による認可を行います。

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

DaprのOAuth対応についてのドキュメントはここにあります。

docs.dapr.io

DaprではOAuthサーバとしてTwitterやSlack、Azure AD、Google APIsなどを利用することができます。今回はGitHubをOAuthサーバとして利用します。

今回作成するアプリケーションのソースコードgithubに置いてあります。

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

僕はセキュリティにあまり詳しくないので、OAuthを認証代わりに使うのはおかしいとか、説明が間違ってるだとか色々あるかも知れませんので、何かあればTwitterなどでお知らせください。

OAuthを使うアプリケーションの作成

コントローラークラスの作成

まずはWebアプリケーションの作成です。別にアプリケーション自体は特に何でも構わないので、Hello World程度にしておきます。

OAuthController.java

@RestController
public class OAuthController {
    @GetMapping("/hello")
    public Map<String, String> hello() {
        return Map.of("message", "Hello, world!");
    }

    @GetMapping("/token")
    public Map<String, ?> token(@RequestHeader("x-oauth-token") String oauthToken) {
        return Map.of("x-oauth-token", oauthToken);
    }
}

Hello Worldとは別に、リクエストの x-oauth-token ヘッダをそのまま返すAPIも追加しました。このヘッダについてはDaprの設定ファイルのところで説明します。

ポート番号をアプリケーション設定ファイルで指定

続いて、設定ファイルでアプリケーションの起動ポートを指定します。

application.properties

server.port=8088

このアプリケーションは8088番ポートを利用します。

GitHubのOAuthを利用するためのDapr設定ファイルの作成

GitHubのOAuthを利用するためのDaprのコンポーネント設定ファイルを作成します。

components/oauth.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: oauth2
spec:
  type: middleware.http.oauth2
  metadata:
  - name: clientId
    value: "***"
  - name: clientSecret
    value: "***"
  - name: scopes
    value: ""
  - name: authURL
    value: "https://github.com/login/oauth/authorize"
  - name: tokenURL
    value: "https://github.com/login/oauth/access_token"
  - name: redirectURL
    value: "http://localhost:18088"
  - name: authHeaderName
    value: "x-oauth-token"

metadata.name は、ソースコードでは使いませんが、また別のDapr設定ファイルで使うので、分かりやすい名前にしておきます。

clientIdclientSecretGitHubで発行します。公式ドキュメントを参考にして、OAuth Appsを作成しました。

docs.github.com

OAuth Appを作成する際の「Homepage URL」の「Authorization callback URL」は、いずれも http://localhost にしました。コールバックURLには実際にはポート番号が入るのですが、GitHub側のOAuthではポート番号は省略しても大丈夫なようです。

OAuth Appを作成すると「Client ID」が発行されるので、これを clientId に指定します。また「Client secrets」にある「Generate a new client secret」ボタンを押すと「Client secret」が発行されるので、これを clientSecret に指定します。

scopes は空で構いません。

authURLtokenURLGitHubのドキュメントに書かれているものを使います。

docs.github.com

redirectURL は、GitHub側での認可が終わったときに戻ってくるアプリケーション側のURLです。認可後にはDaprに戻ってくる必要があるため、Daprのポート番号として利用予定の18088番ポートを指定したURLにしました。

authHeaderName は、GitHubで発行されたトークンをDaprからアプリケーション側に渡す際のHTTPヘッダ名です。x-oauth-token を指定しています。上で作ったコントローラークラスは、この値をそのまま返しているわけです。

public Map<String, ?> token(@RequestHeader("x-oauth-token") String oauthToken) {
    return Map.of("x-oauth-token", oauthToken);
}

少し長くなりましたが、これでDaprのコンポーネント設定ファイルができました。

OAuthを行うためのパイプライン設定ファイルの作成

次に、OAuthを有効にするためにDaprのパイプライン設定ファイルを作成します。このファイルは前回の分散トレーシングを設定する際にも利用したもので、Dapr内部でフィルタのように動作する「Middleware」の設定を行うためのファイルです。

次のような設定を行います。

config.yaml

apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
  name: appconfig
spec:
  httpPipeline:
    handlers:
    - name: oauth2
      type: middleware.http.oauth2

metadata.name はこの設定の名称です。任意の名前で構いません。

spec.httpPipeline はDaprを通過するHTTPリクエストに対して行う処理のパイプラインを記載するもので、handlers には複数のMiddlewareコンポーネントを列挙することができます。今回はOauth2の設定のみを記述しました。

handlers.namehandlers.type には、それぞれコンポーネント設定ファイルで記載した metadata.namespec.type の値を指定します。nameだけ列挙すれば良いような気もするのですが、typeもあわせて列挙する仕様となっています。

これでパイプライン設定ファイルの作成は完了です。

Daprを使ったアプリケーションの起動

それではDaprを使ってアプリケーションを起動します。

dapr run --app-id oauth-app --app-port 8088 --dapr-http-port 18088 --components-path ./components --config config.yaml ../mvnw spring-boot:run

Daprのコマンド引数に --config config.yaml を追加して、新規に作成したパイプライン設定ファイルが有効になるようにしています。

ブラウザからアクセスして確認

アプリケーションが起動したら、ブラウザで次のURLにアクセスします。

http://localhost:18088/v1.0/invoke/oauth-app/method/hello

GitHubに未ログインの場合は、GitHubのログインフォームが表示されます。

f:id:cero-t:20211209061716p:plain:h640
GitHubのログイン画面

さらにログイン後には認可を求める確認画面が表示されます。

f:id:cero-t:20211209061737p:plain:h480
GitHubの認可確認画面

ここで「Authorize」を選択すると、作成したアプリケーションに戻り、次のJSONが表示されるはずです。

{
  "message": "Hello, world!"
}

期待通り、Hello, worldのメッセージを取得できました。

ブラウザからアクセスしてトークンを確認

もう一つのエンドポイントにもアクセスしてみましょう。ブラウザで次のURLにアクセスします。

http://localhost:18088/v1.0/invoke/oauth-app/method/token

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

{
  "x-oauth-token": "Bearer gh*_********"
}

これはGitHubにアクセスする際に必要となるトークンです。これをどう使うかは後で説明するとして、ひとまずGitHubのOAuthが成功してトークンを取得できたことが分かりました。

curlコマンドでアクセス

続いて、curlコマンドでもアクセスしてみましょう。

curl -i localhost:18088/v1.0/invoke/oauth-app/method/hello

次のようなレスポンスが返ってきます。

HTTP/1.1 302 Found
Server: fasthttp
Date: Wed, 09 Dec 2021 00:00:00 GMT
Content-Length: 0
Location: https://github.com/login/oauth/authorize?access_type=offline&client_id=********&redirect_uri=http%3A%2F%2Flocalhost%3A18088&response_type=code&scope=&state=********
Traceparent: 00-0147c54031ce7f17b09f38840c8ec3be-7a79984201eba8b1-00
Set-Cookie: gosessionsid=********; expires=Wed, 09 Dec 2021 02:00:00 GMT; path=/; HttpOnly

GitHubのログイン画面に対するリダイレクトのレスポンスです。このリダイレクトに従って、GitHubのログイン画面に遷移しているわけです。

ちなみに、クッキーに gosessionsid というセッションIDを保存しているようですね。

また、Daprではなく、アプリケーションに直接アクセスした場合はどうなるかも確認しましょう。次のコマンドでアクセスしてください。

curl localhost:8088/hello

次のメッセージが取得できたはずです。

{"message":"Hello, world!"}

OAuthも何もなくアクセスできてしまいました。

OAuthはあくまでもDaprの設定で行っているものであり、Webアプリケーション側に直接アクセスした場合は何の確認もなくアクセスできるのです。その辺りをどう設計すべきか、どうやってアクセスするルートを絞るかなどは別のテーマとなるため、ここでは説明を割愛したいと思います。

トークンを使ってGitHubにアクセスする

ここまでで、アクセス時にGitHub側での認証と認可を行い、アプリケーション側でGitHubトークンを手に入れることができました。ただ、アクセスしたユーザが誰かという情報はありません。もしOAuthを認証代わりに利用するなら、アクセスしてきたのが誰かということを知る必要があります。その実装をしてみましょう。

※そもそもそれならOpenID Connectを使う方が良いのでしょうけど、Dapr単体でOpenID Connectを使って外部のサーバとID連携するのが難しかったので、OAuthを使いました。

コントローラークラスにGitHubにアクセスするメソッドを追加

GitHubで発行されたトークンは、リクエストヘッダの x-oauth-token でアプリケーションに渡されます。このトークンを使えばGitHubにアクセスすることができるので、GitHubにアクセスするメソッドをコントローラークラスに追加しましょう。

OAuthController.java(抜粋)

@Value("https://api.github.com/user")
private String userApi;

@GetMapping("/user")
public Map<?, ?> user(@RequestHeader("x-oauth-token") String oauthToken) {
    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.set("Authorization", oauthToken);
    HttpEntity<?> request = new HttpEntity<>(httpHeaders);

    return restTemplate.exchange(userApi, HttpMethod.GET, request, Map.class).getBody();
}

GitHubのUser APIにアクセスしてユーザ情報を取得する処理を追加しました。Authorization ヘッダに x-oauth-tokenトークンをそのまま渡しています。

アプリケーションの再起動

アプリケーションをいったん止めて、再起動します。

dapr run --app-id oauth-app --app-port 8088 --dapr-http-port 18088 --components-path ./components --config config.yaml ../mvnw spring-boot:run

アプリケーションが起動したら、次のURLにブラウザでアクセスします。

http://localhost:18088/v1.0/invoke/oauth-app/method/user

次のようなJSONが表示されるはずです。

{
  "login": "cero-t",
  "id": 1438519,
  "node_id": "****",
  "avatar_url": "https://avatars.githubusercontent.com/u/1438519?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/cero-t",
  "html_url": "https://github.com/cero-t",
  "followers_url": "https://api.github.com/users/cero-t/followers",
  "following_url": "https://api.github.com/users/cero-t/following{/other_user}",
  "gists_url": "https://api.github.com/users/cero-t/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/cero-t/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/cero-t/subscriptions",
  "organizations_url": "https://api.github.com/users/cero-t/orgs",
  "repos_url": "https://api.github.com/users/cero-t/repos",
  "events_url": "https://api.github.com/users/cero-t/events{/privacy}",
  "received_events_url": "https://api.github.com/users/cero-t/received_events",
  "type": "User",
  "site_admin": false,
  "name": "Shin Tanimoto",
  "company": null,
  "blog": "https://cero-t.hatenadiary.jp/",
  "location": null,
  "email": null,
  "hireable": null,
  "bio": null,
  "twitter_username": "cero_t",
  "public_repos": 44,
  "public_gists": 4,
  "followers": 54,
  "following": 0,
  "created_at": "2012-02-15T02:14:10Z",
  "updated_at": "2021-09-22T04:04:10Z"
}

GitHubのユーザ情報が取得できました。これでアプリケーション側で、アクセスしてきたユーザが誰なのかを把握することができますね。

ブラウザはDaprに何を送っているのか?

ところで、Daprにアクセスする際に、ブラウザはどういう情報をDaprに送っているのでしょうか。GitHubトークンを毎回送っているはずもないので、最後にそれを確認しておきましょう。

まずブラウザの開発者ツールを表示します。たいていキーボードのF12とか、Alt + Command + Iとかで開きます。

開発者ツールを開いたら、次のURLにアクセスします。

http://localhost:18088/v1.0/invoke/oauth-app/method/hello

開発者ツールのネットワークタブに hello へのアクセスなどが表示されるはずです。

f:id:cero-t:20211209064047p:plain
ネットワークタブ

この hello の詳細を開き、下の方にある Request Headers を見てください。

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: max-age=0
Connection: keep-alive
Cookie: gosessionsid=********
Host: localhost:18088
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36

ここで Cookie: gosessionsid= にある通り、クッキーに入ったセッションIDをDaprに送っていることが分かります。Daprがログイン状態をセッションに保存しているのだろうと推測できます。

むろんブラウザがDaprにアクセスする際に x-oauth-token に入っていたようなGitHubのアクセストークンを毎回送るというのも変な話ですから、このようなセッションIDを使うのも当然ですよね。もしセッションではなく、トークンなどを使ってステートレスに認証認可を行いたいのであれば、OpenID ConnectのJWTトークンを使うべきなのでしょうね。

まとめ

  • Daprを使ってOAuthの認可をすることができます
  • 主要なクラウドサービスのOAuthサーバを利用することができます
  • コンポーネント設定ファイルに利用するOAuthサーバのClientIDなどを記述します
  • パイプライン設定ファイルにそのOAuthコンポーネント設定を利用することを記述します
  • OAuthサーバにアクセスするためのトークンを、アプリケーションのHTTPヘッダで受け取ることができます
  • アプリケーションとDapr間でセッションを使って認証認可の状態を維持しているようです

正直、OAuthとOpenID Connectの違いもよく分かってなかったのですが、Daprでいろいろ試してみて、OAuthはあくまでもOAuthサーバ(今回であればGitHub)のAPIを利用するために使うものなんだなと理解しました。一方、OpenID Connectであれば認証/認可後にJWTトークンを用いるため、認証状態をステートレスに維持できますし、このJWTトークンからユーザ情報も取得できるため、目的がアプリケーションの認証なのであればOpenID Connectを使うほうが良いのでしょうね。

その理解が合っているかどうかあまり確信はないので、教えて詳しい人! という感じです。

そんなわけで、また明日!

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

こんにちは、Dapr Advent Calendar 8日目です。今週は会社で全エンジニアとの1 on 1などやっており、なかなかの忙しさなのですが、何とかブログを続けられています!

DaprとZipkinで分散トレーシングをしてみよう

今回はDaprとZipkinを使って分散トレーシングをしてみます。最近色々なサービスやミドルウェアが分散トレーシングに対応するようになったので、だいぶ世の中に認知される技術になりましたよね。使ってるかどうかは別として😏

分散トレーシングやZipkinについての解説をすると長くなるので、あまりご存じないという方は先に調べておいてくださいね。

今回作成するアプリケーションのソースコードgithubに置いてあります。

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

今回は、過去に作成した「hello」「publish」と「subscribe」モジュールを使い、また「tracing」モジュールを作成します。

Daprで分散トレーシングを使ってみる

いきなり分散トレーシング

まずはアクセスしてみる

いきなりですが、Advent Calendar 2日目で作ったHello Worldのアプリケーションを起動させます。

cd hello
dapr run --app-id hello-app --app-port 8080 --dapr-http-port 18080 ../mvnw spring-boot:run

起動したら、Dapr経由でアプリケーションにアクセスしてみます。

curl localhost:18080/v1.0/invoke/hello-app/method/hello

メッセージが表示されるはずです。

{"message":"Hello, world!"}

特に問題なくアクセスができたでしょう。

f:id:cero-t:20211202051328p:plain
いま呼び出したアプリケーション

Zipkinにアクセス

そしたら、ブラウザで次のURLにアクセスしてみます。

http://localhost:9411/

そうするとZipkinの画面が表示されるので、右上の方にある「RUN QUERY」ボタンを押すと、何やら一覧が出てきます。

f:id:cero-t:20211208062840j:plain
Zipkinのトレース一覧画面

3つ並んでいるうちの一番上の「SHOW」ボタンを押してみます。

f:id:cero-t:20211208062934p:plain
Zipkinのトレース詳細画面

何やら結果が表示され、いま行ったリクエストが記録されていることが分かりました。

Daprを起動しただけなのに、Zipkinにアクセスできるし、トレーシングも行われている様子が分かります。まずはこの環境から説明します。

Zipkinと分散トレーシングは最初から設定されている

Dapr Advent Calendar 1日目に、Dapr CLIを使って dapr init をすると設定ファイルとDockerコンテナがいくつかできるという説明をしました。

上でアクセスした localhost:9411 は、ここで作成されたzipkinのDockerコンテナです。Daprのローカル環境では、最初から分散トレーシングを行えるようZipkinがインストールされるのです。

また、dapr init で同じように作成された ~/.dapr/config.yaml も見てみましょう。

~/.dapr/config.yaml

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

設定ファイルの詳細はあとで説明しますが、ひとまず spec.tracing という設定が行われていることが分かります。

dapr run コマンドでアプリケーションを起動する際に、--config オプションでこの設定ファイルを指定することができますが、指定しない場合には ~/.dapr/config.yaml が参照されるようになっています。

つまりローカル環境で dapr init を行うと、zipkinと ~/.dapr/config.yaml が作成されて、分散トレーシングが行われるように設定されるのです。これまで作ってきたアプリケーションたちも、気づかないうちに裏でzipkinにリクエストが記録されていたのです。

もう少し分散トレーシングらしいアプリケーションを作る

Hello Worldを呼ぶコントローラークラスの作成

Hello Worldを直接呼ぶだけでは分散トレーシングをしている感じがしないため、せめてHello Worldを呼び出すアプリケーションを別に作って、まとめて分散トレーシングをしてみましょう。

(tracing) TracingController.java

@RestController
public class TracingController {
    private RestTemplate restTemplate;

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

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

    @GetMapping("/invokeHello")
    public Map<String, ?> invokeHello() {
        Map<?, ?> result = restTemplate.getForObject(helloUrl + "/hello", Map.class);
        return Map.of("baseUrl", helloUrl, "remoteMessage", result);
    }
}

呼び出す処理のエンドポイントを /invokeHello としました。Advent Calendar 3日目に作成したものと同じですね。

ポート番号をアプリケーション設定ファイルで指定

続いて、設定ファイルでアプリケーションの起動ポートを指定します。

(tracing) application.properties

server.port=8087

このアプリケーションは8087番ポートを利用します。

Daprを使ったアプリケーションの起動

それではDaprを使ってアプリケーションを起動します。

dapr run --app-id tracing-app --app-port 8087 --dapr-http-port 18087 ../mvnw spring-boot:run

アプリケーションが起動したら次のコマンドでアクセスしてみます。

curl localhost:18087/v1.0/invoke/tracing-app/method/invokeHello

次のような結果が表示されるはずです。

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

問題なくアクセスができました。

f:id:cero-t:20211208064331p:plain
いま呼び出したアプリケーション

Zipkinで確認する

いま行ったアクセスを確認するために、ブラウザでZipkinにアクセスします。

http://localhost:9411/

表示結果を時系列にするために「Start Time」をクリックして時間の降順に並べるようにしました。

f:id:cero-t:20211208064849j:plain
Zipkinのトレース一覧画面

結果を見ると、/v1.0/metadata へのアクセス以外に、ほぼ同じ時刻に tracing-app calllocal/hello-app/hellotracing-app calllocal/tracing-app/invokehello の2つが並んでいます。前者は「tracing-appからhello-appの/helloを呼んだ」という記録で、後者が「tracing-appの/invokeHelloが呼ばれた」という記録です。

分散トレーシングは、こういうものを1つにまとめる仕組みなのに、分かれているのはおかしいですね。なぜでしょうか。

トレースIDの伝播

複数のプロセスにまたがった分散トレーシングを行うためには「どのリクエストに紐付く処理なのか」を示すためのIDをリクエスト間で伝播させる必要があります。このIDはトレースIDとか、相関ID(correlation-id)と呼ばれています。

分散トレーシングを経験した人ほど、このトレースIDをどう伝播させるかに悩まされていると思うのですが、Daprの公式ドキュメントでもその辺りはきちんと触れられておらず、意外と話題にならないのが不思議です。

このトレースIDは様々な仕様や流派があるのですが、Daprでは W3C Trace-Context に準拠した traceparent が使われています。ちなみにDapr 0.6あたりまでは x-correlation-id という独自のトレースIDを利用しました。

traceparentを伝播させるようコントローラークラスを修正

それでは、この traceparent を伝播させるようなメソッドをコントローラークラスに追加しましょう。

(tracing) TracingController.java(抜粋)

    @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);
    }

Daprからリクエストヘッダで受け取った traceparent を取り出して、次のリクエストを呼ぶ際にリクエストヘッダに追加しています。RestTemplateの利用するメソッドが getForObject から exchange に変わりましたが、getForObject メソッドにはリクエストヘッダを渡す機能がないので、exchange メソッドを使っています。

このメソッドは /invokeHello2 というエンドポイントにしました。

Daprを再起動して、Zipkinで確認

それでは一度アプリケーションを停止させ、Daprを使ってアプリケーションを再起動します。

dapr run --app-id tracing-app --app-port 8087 --dapr-http-port 18087 ../mvnw spring-boot:run

アプリケーションが起動したら次のコマンドでアクセスしてみます。

curl localhost:18087/v1.0/invoke/tracing-app/method/invokeHello2

次のような結果が表示されるはずです。

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

いま行ったアクセスを確認するために、ブラウザでZipkinにアクセスします。

http://localhost:9411/

今度はリクエストが1つにまとめられるようになりました。

f:id:cero-t:20211208065241j:plain
Zipkinのトレース一覧画面

詳細を見ると、3つのリクエストが1つにまとまっていることが分かります。

f:id:cero-t:20211208065329p:plain
Zipkinのトレース詳細画面

上から順に「tracing-appの/invokeHello2が呼ばれた」「tracing-appからhello-appの/helloを呼んだ」「hello-appの/helloが呼ばれた」という処理になっています。この3つの記録は、ちょうどDaprを通過した場所とも一致しますね。

f:id:cero-t:20211208065611p:plain
Daprを通過するリクエス

Daprを通過するところで分散トレーシングを行っているということが分かりました。

pubsubでも分散トレーシング

ここまではInvoke APIを利用した処理で分散トレーシングを行いましたが、キューを経由するような処理でも分散トレーシングができるかどうかを試してみましょう。いったんキューを経由するため、Daprで保持していたような情報が失われるかも知れませんよね。

publishのコントローラークラスを修正

まずは以前のエントリーで作成したpublishモジュールの PublishController に、トレースIDを伝播するようなメソッドを追加します。

(publish) PublishController(抜粋)

    @PostMapping("/publish2")
    public void publish(@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(pubsubUrl, HttpMethod.POST, request, Void.class);
    }

上で作成した invokeHello2 と同様の方法で、トレースIDである traceparent を伝播させています。また確認するためにトレースIDを標準出力に出力しています。このメソッドは /publish2 というエンドポイントにしました。

続いて、tracingモジュール側に、これを呼び出すためのメソッドを追加します。

(tracing) TracingController.java(抜粋)

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

    @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);
    }

上で作成した /publish2traceparent を伝播させながら呼び出しています。こちらでもトレースIDを標準出力に出力しています。こちらは /invokePublish というエンドポイントにしました。

Daprを使ったアプリケーションの起動

それでは作成したアプリケーションをDaprを使って起動させましょう。

まずはtracingアプリケーションをいったん停止させ、再起動します。

dapr run --app-id tracing-app --app-port 8087 --dapr-http-port 18087 ../mvnw spring-boot:run

続いて、publishアプリケーションを起動します。

cd publish
dapr run --app-id publish-app --app-port 8083 ../mvnw spring-boot:run

さらに、subscribeアプリケーションを起動します。

cd subscribe
dapr run --app-id subscribe-app --app-port 8084 ../mvnw spring-boot:run

pubsubは今回は標準で用意されたRedisを使うため、特に components の指定などはしていません。

f:id:cero-t:20211208070016p:plain
ここで起動したアプリケーション

アクセスして分散トレーシングの結果を見てみる

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

curl -XPOST "localhost:18087/v1.0/invoke/tracing-app/method/invokePublish" -H "Content-type:application/json" -d '{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}'

そしてzipkinにアクセスしてみます。

http://localhost:9411/

いま送ったリクエストが一つにまとまって出力されているはずです。

f:id:cero-t:20211208070451p:plain
invokePublishのトレース詳細

tracing-appからpublish-appを経由してsubscribe-appまで一連の処理が流れたことがよく分かります。

また、tracingアプリケーション、publishアプリケーションそれぞれのコンソールには、トレースIDが出力されているはずです。

== APP == 00-8c991c3d443d34906f76f86a1b55fc03-205e7c2adf46ae1b-01

そして、subscribeアプリケーションのコンソールには、次のようなメッセージが出力されるはずです。

== APP == {"specversion":"1.0","source":"publish-app","type":"com.dapr.event.sent","traceid":"00-8c991c3d443d34906f76f86a1b55fc03-6d547142bb715869-01","data":{"name":"Shin Tanimoto","twitter":"@cero_t"},"id":"8318fa00-c587-46fc-aba6-53513feac2c6","datacontenttype":"application/json","topic":"my-message","pubsubname":"pubsub"}

このうち traceid のところに、同じトレースIDが出力されているはずです。どうやらpubsubではこの traceid という項目で traceparent を伝播させるということが分かりましたね。

同期処理だけではなく、非同期のpubsubでも問題なく分散トレーシングができることが分かりました。

今回やったことの解説

それでは、Daprがどのように分散トレーシングを行うかの仕組みについて説明します。

Daprの分散トレーシングに関するドキュメントはここにあります。

docs.dapr.io

Daprによる分散トレーシングの概要

f:id:cero-t:20211208070753p:plain
Tracing Overview

公式ドキュメントの図に示されているようにDaprの中でTracing Middlewareが動いており、Daprをリクエストが経由するたびに、Tracing Backend(今回はzipkin)にその情報が蓄積されるようになっています。逆に言えば、Daprを経由しないアクセスでは情報は蓄積されません。

上では、Daprを経由する形でアクセスしたため、Zipkinに情報が記録されました。

curl localhost:18080/v1.0/invoke/hello-app/method/hello

ここで、Daprを経由せずに直接アプリケーションにアクセスすると

curl localhost:8080/hello

何度アクセスしても、Zipkinには情報が記録されません。あくまでもDaprを経由したリクエストのみがZipkinに記録されるのです。

DaprのMiddleware機能

上で「Tracing Middleware」と簡単に言いましたが、このMiddlewareについてもう少し詳しく説明します。

Middlewareのドキュメントはここにあります。

docs.dapr.io

f:id:cero-t:20211208070856p:plain
Middleware Overview

上の画像はMiddlewareのドキュメントのものです。Daprは外部からリクエストを受け、アプリケーションを呼び出す際に、Middlewareをいくつか通過するというパイプライン処理を行っていることが分かります。

Middlewareは共通的な処理を行うフィルタのようなもので、Javaの開発者であれば、ServletFilterやInterceptorのようなものだと捉えれば分かりやすいと思います。このMiddlewareのひとつがTracing Middlewareです。

Middlewareには他にも認証認可を行うものやレートリミット(アクセス数の制限)を行うものなどがあります。Middlewareの一覧はここにあります。

https://docs.dapr.io/reference/components-reference/supported-middleware/

この一覧には掲載されていませんが、Daprでは「Tracing Middleware」と「CORS Middleware」が最初から組み込まれています。

Daprのパイプライン設定ファイル

このパイプライン設定をするのが config.yaml です。今回は dapr init で作成されたものをそのまま使いました。

~/.dapr/config.yaml

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

spec.tracing が分散トレーシングの設定です。

samplingRate はDaprを経由するリクエストのうち何割をTracing Backend(zipkin)に送るかを指定するものです。分散トレーシングはたいていパフォーマンス測定に使われるため、わざわざ全リクエストを送る必要はなく、たとえば 0.010.1 などを指定して一部のリクエストだけ記録することで、分散トレーシングそのものがアプリケーションに負荷を掛けないようにします。

ただし、分散トレーシング機能そのものを確認したい場合や、登壇でデモをする場合などは、常に記録されていたほうが嬉しいので 1 を指定したほうが良いでしょう。また、分散トレーシングをパフォーマンス計測ではなく、たまに発生する障害の分析などに使いたい場合も 1 を指定すると良いと思います。その辺りは目的や負荷とのバランスで決定します。

zipkin.endpointAddress はZipkinサーバのアドレスを指定します。Zipkinだけでなく、ZipkinとAPI互換のあるJaegerやNew Relicなどのアドレスを指定することもできます。

パイプライン設定ファイルは、これまで使ってきたコンポーネント設定ファイル(/components に置くファイル)とは異なり、1つしかありません。Daprにどのようなパイプライン処理をさせるかをまとめてここに記載するため、1つしか指定できないのです。

このパイプラインの設定をすることで、分散トレーシングが有効になりました。

まとめ

  • Daprは最初からzipkinを使った分散トレーシングが有効になっている
  • 複数のアプリケーション間での分散トレーシングをするためには、traceparent というヘッダを伝播させる必要がある
  • Invoke APIでもPub/sub APIでも分散トレーシングが使える
  • DaprにはMiddlewareというパイプライン処理を行うための仕組みがある
  • 分散トレーシングはTracing Middlewareという機能で提供されている
  • ローカル環境で dapr init するとZipkinのDockerコンテナが起動し、分散トレーシングを有効にするためのパイプライン設定ファイルが作成されるため、最初から分散トレーシングが有効になる

ところで、今回は分かりやすさのため、トレースIDを伝播させる実装をそれぞれのメソッドに追加しました。実際にはこんなものを全メソッドに入れるのは無理筋ですよね。このような処理は共通的に実装するべきで、たとえばSpring BootであればRestTemplateのInterceptor機能や、WebClientのFilter機能を使って実装ができます。その辺りについてはDaprの範疇ではなくSpring Bootの範疇となるため、今回の説明からは割愛しました。

また機会があれば、この辺りの実装についても紹介したいと思います。

それでは、また明日!

Dapr Advent Calendar 7日目 - Daprでシークレットストアを使う

こんにちは、Dapr Advent Calendar 7日目です。とうとう1週間続きました! これでもまだ全体の1/3も行ってないんですからね。先は長い!

Daprでシークレットストアを使ってみよう

今回はDaprを使って、パスワードなどを秘匿するシークレットストアを利用します。シークレットストアと言えば、AWS Secrets ManagerやAzure Key Vault、あるいはk8sのsecrets機能などがありますが、これらをDaprから利用することができます。

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

シークレットについてのドキュメントはこちらにあります。

docs.dapr.io

Daprからシークレットを利用する(読み取る)ことはできますが、シークレットを作成する(書き込む)機能はないため、シークレットは先に自分で作っておく必要があります。

Daprがサポートしているシークレットストアはここにまとめられています。

docs.dapr.io

今回は、最も簡単にできるローカルファイルを利用しましょう。

今回作成するアプリケーションのソースコードgithubに置いてあります。

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

シークレットを取得するためのアプリケーション作成

コントローラークラスの作成

まずはWebアプリケーションの作成です。シークレットを取得するコントローラーを作成します。

SecretsController.java

@RestController
public class SecretsController {
    private RestTemplate restTemplate;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/secrets/my-secret")
    private String secretsUrl;

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

    @GetMapping("/{key}")
    public String read(@PathVariable String key) {
        return restTemplate.getForObject(secretsUrl + "/" + key, String.class);
    }
}

DaprのSecrets APIを呼び出しているだけの簡単な処理です。

Secrets API/v1.0/secrets/(シークレットストア名)/(シークレットのキー) という形式になります。シークレットストア名は my-secret として、シークレットのキーはリクエストのパスで渡されたものをそのまま使います。

ポート番号をアプリケーション設定ファイルで指定

続いて、設定ファイルでアプリケーションの起動ポートを指定します。

application.properties

server.port=8086

このアプリケーションは8086番ポートを利用します。

シークレットストアを参照するためのDapr設定ファイルを作成

さらに、シークレットストアを参照するためのDapr設定ファイルを作成します。

components/my-secret.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: my-secret
  namespace: default
spec:
  type: secretstores.local.file
  version: v1
  metadata:
  - name: secretsFile
    value: ./components/my-secret.json

metadata.name はこのコンポーネントの名称です。この値がSecrets APIのシークレットストア名として利用されるため、上のソースコードと同じ /my-secret を指定しています。

typesecretstores.local.file を指定しており、これはシークレットストアとしてローカルファイル(平文のJSON)を使うという指定です。

secretsFile はシークレットストアに利用するローカルファイルのファイル名です。このyamlファイルと同じ場所に置きたいので ./components/my-screts.json を指定しました。本来、シークレットファイルはもう少し安全な場所に置くべきかも知れませんが、ローカル開発環境のパスワードなどは雑に共有することも多いでしょうから、あまり気にせずここに置くことにしました。

もう少し慎重を期するなら、ユーザーディレクトリに .dapr/secrets ディレクトリなどを作成して他のユーザーに読み取れないようにするのが良いと思います。

シークレットファイルの作成

最後に、シークレットストアに使うファイルを作成します。

components/my-secret.json

{
  "password1": "p@ssw0rd",
  "password2": "12345678"
}

世界で最も著名なパスワード2種を指定しました。

Daprを使ったアプリケーションの起動

それではDaprを使ってアプリケーションを起動します。

dapr run --app-port 8086 --components-path ./components ../mvnw spring-boot:run

アプリケーションが起動したら次のコマンドでアクセスしてみます。

curl localhost:8086/password1

結果が取得できます。

{
  "password1": "p@ssw0rd"
}

もう一つのパスワードも取得してみましょう。

curl localhost:8086/password2

問題なく取得できるはずです。

{
  "password1": "12345678"
}

ここまでで作成したアプリケーションでは平文のパスワードが書かれたjsonファイルを読み込んだだけで、シークレット感も何もありませんね。実際に運用する際にはDaprの設定を変えて、k8sのsecretsやクラウドサービスのシークレットストアを利用する形になるでしょう。

f:id:cero-t:20211207053523p:plain
Daprを使ってシークレットストアにアクセス

Daprの設定にシークレットを使う

上の例はシークレットの値をそのままレスポンスとして返すものでしたが、実際のアプリケーションでそんな利用をすることもないでしょう。

シークレットを使うユースケースは、データベースへの接続パスワードを秘匿するとか、外部のWebサービスのアカウント情報を秘匿するといったものが多いと思います。それに近い例として、Daprからデータストアに接続する情報をシークレットから取得するという方法を試してみます。

Daprの設定ファイルでシークレットを利用する方法についてのドキュメントはここにあります。

docs.dapr.io

このドキュメントで指定されている方法を試してみましょう。

データストアにアクセスするコントローラークラスの作成

以前のエントリーで作成したデータストアにアクセスするコントローラーを利用します。

StateController.java

@RestController
public class StateController {
    private RestTemplate restTemplate;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/state/postgresql-store")
    private String stateUrl;

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

    @PostMapping("/write")
    public void write(@RequestBody Object message) {
        restTemplate.postForObject(stateUrl, message, Void.class);
    }

    @GetMapping("/read/{key}")
    public Object read(@PathVariable String key) {
        return restTemplate.getForObject(stateUrl + "/" + key, Object.class);
    }
}

完全にコピペですが、データストア名だけ分かりやすいように postgresql-store にしています。

シークレットストアのDapr設定ファイルを作成

次に、シークレットストアの設定ファイルを作ります。

components/postgresql-secret.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: postgresql-secret
  namespace: default
spec:
  type: secretstores.local.file
  version: v1
  metadata:
  - name: secretsFile
    value: ./components/postgresql-secret.json

上で作成したシークレットの設定ファイルと同様ですね。

続いて、シークレットストアとして利用するファイルを作りましょう。

components/postgresql-secret.json

{
  "connectionString": "host=localhost user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres"
}

PostgreSQLに接続する情報をすべて記述しています。この接続文字列も以前のエントリーで指定したものと同じです。

本来はパスワードだけシークレットを利用したかったのですが、DaprのPostgreSQL設定ファイルでは接続文字列をすべて指定する形になるため、接続文字列全体をシークレットに保存することにしました。

データストアのDapr設定ファイルを作成

最後に、データストアに接続するための設定ファイルを作成します。この接続設定がシークレットストアを参照するようにします。

components/postgresql-store.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: postgresql-store
spec:
  type: state.postgresql
  version: v1
  metadata:
  - name: connectionString
    secretKeyRef:
      name: connection
      key: connection
auth:
  secretStore: postgresql-secret

これも以前作ったものと同様ですが connectionString にシークレットを使うように設定しました。

auth.secretStore でシークレットストア名を指定します。上で作成したシークレットストア設定の postgresql-secret を使いました。

connectionStringsecretKeyRef を指定してシークレットストアを参照できるようにしています。ここで namekey の両方を connection としていますが、シークレットが単一の文字列である場合は、このように namekey の両方に同じキー名を指定します。シークレットがJSONになっている場合には、name にシークレットキーを指定し、keyJSONのキーを指定する形になります。

GitHubのサンプルコードではファイル名を postgresql-store.yaml.disabled として、起動時にPostgreSQLにアクセスしないようにしていました。こちらを利用する場合はファイル名を postgresql-store.yaml に変更してください。

これでアプリケーションの修正は完了です。

PostgreSQLの起動

まずはPosgreSQLを起動しましょう。Dockerを使って起動します。

docker run -d --name dapr_postgres -p 5432:5432 -e POSTGRES_PASSWORD=secretpassword postgres

もし以前のエントリーで起動した時のDockerコンテナが起動したままであれば、それをそのまま使っても構いません。

コンテナが残っていて停止していた場合は docker start dapr_postgres で起動してください。

Daprを使ったアプリケーションの(再)起動

それではDaprを使ってアプリケーションを起動します。いったん上で起動したアプリケーションを停止させた後、再起動してください。

dapr run --app-id secrets-app --app-port 8086 --components-path ./components ../mvnw spring-boot:run

State mangement APIapp-id ごとにデータを保存するので、お行儀良く app-id を指定しています。マナーです。

起動を確認できたら、次のコマンドでデータを登録します。

curl -XPOST "localhost:8086/write" -H "Content-type:application/json" -d '[
{
  "key": "cero_t",
  "value": {
    "name": "Shin Tanimoto",
    "twitter": "@cero_t"
  }
}
]'

登録ができたら、次のコマンドでデータを取得します。

curl localhost:8086/read/cero_t

登録したデータが取得できるはずです。

{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}

正しくPostgreSQLでデータの読み書きができることを確認できました。

PostgreSQLの接続設定をシークレットに保存することで、接続設定ファイルに平文のパスワードを書くことなく接続できるようになりました。この機能はどちらかと言えば開発時よりは運用時に効いてくる機能ですので、また今後のAdvent Calendarの中で良いユースケースと共に改めて紹介できればと思います。

f:id:cero-t:20211207053718p:plain
シークレットストアの情報を使ってPostgreSQLにアクセス

まとめ

  • Secrets APIを用いてシークレットストアから値の取得ができます
  • クラウドサービスやk8sの提供するシークレットストアに加え、ローカルファイルなどもシークレットストアとして利用できます
  • Daprのコンポーネント設定ファイルの中でもシークレットを利用することができます

ちなみに僕自身は、このDaprのSecrets機能を使ったことはなく(というか存在を認識しておらず)、k8sのSecrets機能を使ってDaprの設定をしていました。僕の場合はローカル環境でもk8sを使うか、あるいはそもそもk8sもDaprを使わずに開発するかのいずれかの方針を採っていたためです。

ちょうどその間の「k8sは使わないけど、Daprは使う」ような状況での開発だと、このSecrets機能は有用になりそうですね。その辺りの「どうやって開発するか?」みたいなところはまた今後のAdvent Calendarの中でテーマにしたいと思います。

それでは!