谷本 心 in せろ部屋

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

Dapr Advent Calendar 17日目 - Dapr + k8sの環境が壊れたので作り直し

こんにちは Dapr Advent Calendar 17日目です。今日はもともと分散トレーシングの話を書くつもりだったのですが、その前に環境でトラブってしまったので、その辺りの経緯や対策などを書きたいと思います。

TL;DR

「私はminikubeをメモリ割り当て2GBで動かそうとしていました」

  • Daprの環境が壊れた時は、dapr unintall -k minikube stop Dockerの再起動 minikube start dapr init -k という手順で復旧できる
  • minikubeはデフォルトで2CPU、2GBのメモリで環境を作るが、Javaアプリケーションを5〜6つくらい作成するだけでメモリが足りなくなる
  • minikube start--cpus--memory オプションで、割り当てるリソースを指定ができる
    • ただし既に作成したminikubeはリソース割り当てを変更できないため、一度 minikube delete してから再作成する必要がある
  • docker container stats でDockerのリソース状況を確認できる
  • eval $(minikube docker-env)docker container stats でminikubeのリソース状況を確認できる

問題の経緯

問題が起きる前

数日前ですが、このAdvent Calendarの連載を書くために、ローカル環境のminikubeに色々とアプリケーションをデプロイしたり何だりしていたら、急にkubectlコマンドの反応が極端に遅くなったことがありました。その時はMacのCPU使用率が200%になっていて、2CPUを使いきって遅くなっていたため、「podが無限ループでもしたかなぁ?」などと思い、とりあえず kubectl delete -f k8s で順番にpodを削除しようとしました。

ただpodの削除も途中で止まったので minikube stop コマンドでk8sごと停止させ、改めて minikube start コマンドで起動して、残ったpodなどを削除などしてから作業を続けていました。

その後は特に問題なくminikubeを使うことができたので、まぁそんなこともあるのかなと思い、あまり気にしていませんでした。

なぜかpodを4つ以上起動できない!

昨晩、またブログを書くためにminikube上にアプリケーションをデプロイしていたら、3つか4つ以上のアプリケーションをデプロイしたところで、なぜか「アプリケーションのpodのうち1つがランダムにERRORになる」という問題が起きました。kubectl describe pod で詳細を見ると、Liveness ProbeとReadiness Probeが500エラーを返しているようです。

何かDapr関連のポートが被って通信エラーが起きているのか、サンプルのアプリケーションに問題があるのか、複数のServiceを起動しているのが問題なのか、などいくつか仮説を立て、とりあえず問題切り分けのためにHello WorldアプリケーションをServiceなしで5つほど起動してみたところ、1つは必ず同じエラーが起きるようになっていました。

Daprをダウングレードしてみようとしたものの

いま仕事でAmazon EKS上でDaprを使ってもっとたくさんのアプリケーションを運用しているので、ポートが被って通信エラーが起きるというのも、ちょっと考えにくい話です。ただ、いま運用はDapr 1.4系を使っているのですが、このブログはDapr 1.5を使って書いているので、1.5の問題かも知れないと思い、一度バージョンを落とそうと思いました。

dapr uninstall -k
dapr install -k --runtime-version 1.4.4

これでバージョンがどうなったかなと思い、dapr status -k コマンドを実行したところ、何かおかしい。

  NAME                   NAMESPACE    HEALTHY  STATUS                       REPLICAS  VERSION  AGE  CREATED              
  dapr-placement-server  dapr-system  False    Waiting (ContainerCreating)  1         1.5.1    5d   2021-12-11 21:08.30  
  dapr-sidecar-injector  dapr-system  False    Waiting (ContainerCreating)  2         1.5.1    5d   2021-12-11 21:08.30  
  dapr-operator          dapr-system  False    Terminated                   2         1.5.1    5d   2021-12-11 21:08.30  
  dapr-dashboard         dapr-system  False    Terminated                   2         0.9.0    5d   2021-12-11 21:08.30  
  dapr-sentry            dapr-system  False    Waiting (ContainerCreating)  2         1.4.4    7s   2021-12-16 23:15.34  

1.4.4のコンテナを作れていないだけでなく、1.5.1のコンテナもまだ残っている、、というか作成途中で止まっていて、まともに動いていません。どうやらminikube上のDapr環境が壊れているようです。おそらく、冒頭に書いたCPU使用率が上がった時に、環境の一部が壊れてしまったのでしょう。

環境の再構築

これはもうDaprのバージョンの問題ではなく、Dapr環境が壊れているせいだと確信したので、環境を再構築することにしました。

dapr uninstall -k
minikube stop

この後、Docker Desktopを再起動して、また環境を構築しました。

minikube start
dapr init -k

そしてまたHello Worldアプリケーションを5つほどデプロイすると、今度は5つともきちんと起動できたので、問題解決。無事にトラブルシューティングが完了しました。さすが俺!

またCPU使用率が200%になる問題が発生

環境が治ったので、とりあえず全アプリケーションをデプロイすることにしました。

kubectl apply -f k8s

そして起動するのを待っていると、、、またkubectlの反応がなくなり、CPU使用率が200%になる問題が起きました。えぇぇ、なんで。。。

Javaエンジニアの経験と勘から、これはリソースが足りていなくてメモリ不足になってフルGCでも起きているのかなと思い、とりあえずDockerのリソース状況を確認することにしました。

docker container stats
CONTAINER ID   NAME       CPU %     MEM USAGE / LIMIT    MEM %     NET I/O          BLOCK I/O       PIDS
cbe04e02f2d1   minikube   202.36%   1.894GiB / 1.94GiB   97.60%    691kB / 2.11MB   102GB / 168MB   1100

いやー、もう完全に足りてないですね。メモリの割り当て2GBを完全に使いきっています。そりゃ中のアプリケーションもフルGCを頻発するでしょうよ。そもそもメモリ2GBでminikubeを運用しようとするなんて無茶、誰がしたんだよ。。。まぁ僕なんですが。

そんなわけで、環境をすべて作り直すことにしました。

環境の再構築

そんなわけで、ここから環境の再構築手順を説明します。このブログを読んで手を動かしていた人は、この手順に従って環境を作り直してください。

minikubeの削除

minikubeはstartする際の引数でCPU数やメモリ割り当て量を変えることできるのですが、既に作成してあるminikubeに対して変更することはできないため、まずminikubeそのものを削除する必要があります。

次のコマンドで削除します。

minikube stop
minikube delete

これでDaprのコンテナや、作成したイメージなどもすべて削除されます。

minikubeとDapr環境の再構築

続いて環境の再構築です。次のコマンドで構築します。

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

環境が構築できたら、念のためリソース状況を確認します。

docker container stats

メモリのLIMITが4GiBになっていることを確認してください。

続いて、Dapr環境の構築です。

dapr init -k

環境が構築できたら、念のためpodが正常に動いていることを確認します。

dapr status -k

5つのpodのHEALTYがTRUEになっていて、STATUSがRunningになっていればOKです。

イメージの再作成

これまでのブログで作ってきたアプリケーションのイメージは、minikubeをdeleteした際に一緒に削除されてしまったので、すべて作り直す必要があります。

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

これですべてのアプリケーションのイメージが再作成されます。

ちなみに最後に親モジュールのイメージも作ろうとしてmavenビルドがエラーになりますが、その時点で子モジュールのイメージはすべて作成されているためヨシとします。

アプリケーションのデプロイ

イメージの作成が完了したら、アプリケーションをまとめてデプロイしてしまいましょう。

kubectl apply -f k8s

デプロイする順番など気にせず、一気にデプロイしても特に問題などは起きません。

念のため kubectl get pods コマンドで、起動に成功していることを確認しておきましょう。

NAME                                READY   STATUS    RESTARTS   AGE
hello-app-fb644cd5d-j2z7b           2/2     Running   0          1m
invoke-app-745b5f4bf6-2hf6l         2/2     Running   0          1m
postgresql-deploy-658df4946-lm6gp   1/1     Running   0          1m
publish-app-5b4c66c9-mhj67          2/2     Running   0          1m
rabbitmq-5d474484f5-rhjzd           1/1     Running   0          1m
state-app-849f6d6944-2mtr5          2/2     Running   0          1m
subscribe-app-cdc58446-n82px        2/2     Running   0          1m

問題ありませんね。

minikubeのリソース確認

最後に、minikubeのリソースを確認しておきましょう。

新しいコンソールを開いて、docker container stats コマンドを実行します。

CONTAINER ID   NAME       CPU %     MEM USAGE / LIMIT   MEM %     NET I/O          BLOCK I/O        PIDS
079bf48016ec   minikube   53.21%    2.339GiB / 4GiB     58.48%    1.56GB / 250MB   106MB / 9.87GB   1062

メモリは4GiB中、2.3GiBほど使っているようです。そりゃ2GBしか割り当ててなければ、メモリが足りなくて止まっちゃいますよね、、、

なお、minikube内部のpodごとのリソース状況は、次のコマンドで確認できます。

eval $(minikube docker-env)
docker container stats

次のような結果が出るはずです。

CONTAINER ID   NAME                                                                                                                  CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O         PIDS
bb2650777c7a   k8s_daprd_state-app-849f6d6944-2mtr5_default_fa0cc253-ddab-4026-9328-fc3a29d2151e_0                                   0.14%     15.75MiB / 7.773GiB   0.20%     1.59MB / 1.51MB   0B / 0B           11
e51e7d17a744   k8s_daprd_publish-app-5b4c66c9-mhj67_default_9f1de14f-0498-4f12-ab17-931771d7f0b3_0                                   0.21%     15.68MiB / 7.773GiB   0.20%     1.59MB / 1.51MB   0B / 0B           12
b9c2c68edd1b   k8s_daprd_hello-app-fb644cd5d-j2z7b_default_114f279b-313b-4763-b779-3a1128986b9e_0                                    0.09%     15.74MiB / 7.773GiB   0.20%     1.59MB / 1.5MB    8.19kB / 0B       12
3d7590c40e30   k8s_daprd_invoke-app-745b5f4bf6-2hf6l_default_852d6fd1-582d-44ea-b06f-cbaa95a7af2b_0                                  0.08%     15.69MiB / 7.773GiB   0.20%     1.59MB / 1.51MB   0B / 0B           12
52769d846edb   k8s_subscribe_subscribe-app-cdc58446-n82px_default_b0d3ebfa-8a4f-4dec-ad6e-4296c9dfaa74_0                             0.12%     172.4MiB / 7.773GiB   2.17%     1.59MB / 1.52MB   0B / 1.29MB       32
5daf791beb83   k8s_state_state-app-849f6d6944-2mtr5_default_fa0cc253-ddab-4026-9328-fc3a29d2151e_0                                   0.14%     164.7MiB / 7.773GiB   2.07%     1.59MB / 1.51MB   16.4kB / 1.32MB   32
9c162e211125   k8s_publish_publish-app-5b4c66c9-mhj67_default_9f1de14f-0498-4f12-ab17-931771d7f0b3_0                                 0.12%     180.6MiB / 7.773GiB   2.27%     1.59MB / 1.51MB   8.19kB / 1.3MB    32
4652afb3deef   k8s_postgresql_postgresql-deploy-658df4946-lm6gp_default_f080e2f5-f9ec-41f3-a8b3-752b59445d8b_0                       0.00%     52.89MiB / 7.773GiB   0.66%     12.5kB / 12kB     32.7MB / 55MB     12
39ba4e76702f   k8s_POD_subscribe-app-cdc58446-n82px_default_b0d3ebfa-8a4f-4dec-ad6e-4296c9dfaa74_0                                   0.00%     212KiB / 7.773GiB     0.00%     1.59MB / 1.52MB   0B / 0B           1
6b06c40a04b6   k8s_POD_state-app-849f6d6944-2mtr5_default_fa0cc253-ddab-4026-9328-fc3a29d2151e_0                                     0.00%     208KiB / 7.773GiB     0.00%     1.59MB / 1.51MB   0B / 0B           1
2f3b07bc26e6   k8s_rabbitmq_rabbitmq-5d474484f5-rhjzd_default_f39f8fcd-47af-420a-aebd-d4dbf4385d8d_0                                 0.25%     126.4MiB / 7.773GiB   1.59%     2.5kB / 354B      33.8MB / 819kB    33
b2bc5b2bcd5c   k8s_POD_publish-app-5b4c66c9-mhj67_default_9f1de14f-0498-4f12-ab17-931771d7f0b3_0                                     0.00%     208KiB / 7.773GiB     0.00%     1.59MB / 1.51MB   0B / 0B           1
78778711254e   k8s_POD_rabbitmq-5d474484f5-rhjzd_default_f39f8fcd-47af-420a-aebd-d4dbf4385d8d_0                                      0.00%     208KiB / 7.773GiB     0.00%     2.5kB / 354B      0B / 0B           1
90754eca8f01   k8s_hello_hello-app-fb644cd5d-j2z7b_default_114f279b-313b-4763-b779-3a1128986b9e_0                                    0.20%     194.5MiB / 7.773GiB   2.44%     1.59MB / 1.5MB    0B / 1.56MB       32
0ccbd01ea780   k8s_POD_postgresql-deploy-658df4946-lm6gp_default_f080e2f5-f9ec-41f3-a8b3-752b59445d8b_0                              0.00%     204KiB / 7.773GiB     0.00%     12.5kB / 12kB     0B / 0B           1
986d6f27779d   k8s_invoke_invoke-app-745b5f4bf6-2hf6l_default_852d6fd1-582d-44ea-b06f-cbaa95a7af2b_0                                 0.11%     179.7MiB / 7.773GiB   2.26%     1.59MB / 1.51MB   143kB / 1.64MB    32
0361dd7cf5bb   k8s_POD_hello-app-fb644cd5d-j2z7b_default_114f279b-313b-4763-b779-3a1128986b9e_0                                      0.00%     208KiB / 7.773GiB     0.00%     1.59MB / 1.5MB    0B / 0B           1
1f374ad3ef7c   k8s_POD_invoke-app-745b5f4bf6-2hf6l_default_852d6fd1-582d-44ea-b06f-cbaa95a7af2b_0                                    0.00%     204KiB / 7.773GiB     0.00%     1.59MB / 1.51MB   0B / 0B           1
6d1ef557c15a   k8s_dapr-placement-server_dapr-placement-server-0_dapr-system_677cb5fd-ae57-41eb-918c-58d42e6c7652_0                  0.33%     7.207MiB / 7.773GiB   0.09%     6.06MB / 6.09MB   0B / 0B           11
23acf6a45fe2   k8s_dapr-operator_dapr-operator-5b4b68b5c5-mptxp_dapr-system_c9d399ef-c82c-4781-9ae4-624a6933abc5_0                   0.17%     18.25MiB / 7.773GiB   0.23%     27.1MB / 14.1MB   0B / 0B           13
9ff28f354a81   k8s_dapr-sidecar-injector_dapr-sidecar-injector-6f749dbf87-9vn96_dapr-system_0ab8e63d-8073-411c-9c87-d9abd03083b7_0   0.11%     8.184MiB / 7.773GiB   0.10%     1.55MB / 1.37MB   0B / 0B           12
116fb67e12ec   k8s_dapr-sentry_dapr-sentry-c6b746cdf-hp2n9_dapr-system_e30b08ca-9060-4853-8c26-a37351e41c26_0                        0.06%     7.883MiB / 7.773GiB   0.10%     1.53MB / 1.39MB   0B / 0B           14
9ef9efefb9c2   k8s_dapr-dashboard_dapr-dashboard-57b4db56fc-pr62l_dapr-system_4ba3a32e-afe9-4c7e-a8c2-ba2ac4745da9_0                 0.00%     5.734MiB / 7.773GiB   0.07%     2.89kB / 0B       0B / 0B           8
9bc3f4edc295   k8s_POD_dapr-placement-server-0_dapr-system_677cb5fd-ae57-41eb-918c-58d42e6c7652_0                                    0.00%     200KiB / 7.773GiB     0.00%     6.06MB / 6.09MB   0B / 0B           1
db607d3df442   k8s_POD_dapr-sidecar-injector-6f749dbf87-9vn96_dapr-system_0ab8e63d-8073-411c-9c87-d9abd03083b7_0                     0.00%     204KiB / 7.773GiB     0.00%     1.55MB / 1.37MB   0B / 0B           1
2f2426091427   k8s_POD_dapr-operator-5b4b68b5c5-mptxp_dapr-system_c9d399ef-c82c-4781-9ae4-624a6933abc5_0                             0.00%     208KiB / 7.773GiB     0.00%     27.1MB / 14.1MB   0B / 0B           1
351e4b9d4e76   k8s_POD_dapr-sentry-c6b746cdf-hp2n9_dapr-system_e30b08ca-9060-4853-8c26-a37351e41c26_0                                0.00%     204KiB / 7.773GiB     0.00%     1.53MB / 1.39MB   0B / 0B           1
292b25653234   k8s_POD_dapr-dashboard-57b4db56fc-pr62l_dapr-system_4ba3a32e-afe9-4c7e-a8c2-ba2ac4745da9_0                            0.00%     204KiB / 7.773GiB     0.00%     2.89kB / 0B       0B / 0B           1
da405d04f18f   k8s_storage-provisioner_storage-provisioner_kube-system_3526d87b-7f1f-4e57-a5c4-6a9f054e7ec2_1                        0.05%     9.297MiB / 7.773GiB   0.12%     0B / 0B           0B / 0B           8
4d7fe3d17eb4   k8s_coredns_coredns-558bd4d5db-t4gxq_kube-system_ec6ebffa-abce-45b9-9978-b3349311791f_0                               0.36%     11.11MiB / 170MiB     6.53%     2.23MB / 719kB    0B / 0B           12
7e4d83af4783   k8s_kube-proxy_kube-proxy-tmzwt_kube-system_29295269-e286-48b4-84e0-eb2583132419_0                                    0.02%     12.56MiB / 7.773GiB   0.16%     0B / 0B           0B / 16.4kB       10
52cab24b1823   k8s_POD_coredns-558bd4d5db-t4gxq_kube-system_ec6ebffa-abce-45b9-9978-b3349311791f_0                                   0.00%     204KiB / 7.773GiB     0.00%     2.23MB / 719kB    0B / 0B           1
1ec5d319864e   k8s_POD_kube-proxy-tmzwt_kube-system_29295269-e286-48b4-84e0-eb2583132419_0                                           0.00%     280KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
23e447125876   k8s_POD_storage-provisioner_kube-system_3526d87b-7f1f-4e57-a5c4-6a9f054e7ec2_0                                        0.00%     148KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
d0b6da913375   k8s_etcd_etcd-minikube_kube-system_be5cbc7ffcadbd4ffc776526843ee514_0                                                 1.65%     39.15MiB / 7.773GiB   0.49%     0B / 0B           0B / 235MB        19
094635d3e682   k8s_kube-apiserver_kube-apiserver-minikube_kube-system_cefbe66f503bf010430ec3521cf31be8_0                             3.95%     314.8MiB / 7.773GiB   3.96%     0B / 0B           0B / 0B           12
4c8cac060a66   k8s_kube-scheduler_kube-scheduler-minikube_kube-system_a2acd1bccd50fd7790183537181f658e_0                             0.20%     16.75MiB / 7.773GiB   0.21%     0B / 0B           0B / 0B           11
a638324aa7b6   k8s_kube-controller-manager_kube-controller-manager-minikube_kube-system_a5754fbaabd2854e0e0cdce8400679ea_0           1.61%     48.86MiB / 7.773GiB   0.61%     0B / 0B           0B / 0B           10
a21fdf45e9ab   k8s_POD_etcd-minikube_kube-system_be5cbc7ffcadbd4ffc776526843ee514_0                                                  0.00%     144KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
4983ebfc71a4   k8s_POD_kube-scheduler-minikube_kube-system_a2acd1bccd50fd7790183537181f658e_0                                        0.00%     144KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
ffa6824f6939   k8s_POD_kube-controller-manager-minikube_kube-system_a5754fbaabd2854e0e0cdce8400679ea_0                               0.00%     140KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
6e78a62d73d0   k8s_POD_kube-apiserver-minikube_kube-system_cefbe66f503bf010430ec3521cf31be8_0                                        0.00%     140KiB / 7.773GiB     0.00%     0B / 0B           0B / 0B           1
19977ecb1ab2   k8s_daprd_subscribe-app-cdc58446-n82px_default_b0d3ebfa-8a4f-4dec-ad6e-4296c9dfaa74_0                                 0.21%     15.47MiB / 7.773GiB   0.19%     1.59MB / 1.52MB   0B / 0B           12

何やよく分かりませんけど、わりとたくさんpodが動いてるんですね(今さら)

JVM系のpodは、たとえばこんな感じです。

90754eca8f01   k8s_hello_hello-app-fb644cd5d-j2z7b_default_114f279b-313b-4763-b779-3a1128986b9e_0                                    0.20%     194.5MiB / 7.773GiB   2.44%     1.59MB / 1.5MB    0B / 1.56MB       32

アプリケーション1つで200MiBほどメモリを使うのですから、そりゃ5つも6つも立ち上げれば2GBでは足りませんね。

そんなわけで、無事に環境を作り直すことができました。

ドキュメントはちゃんと読もうね

ちなみにDaprのminikube環境構築のドキュメントを見ると、、、

docs.dapr.io

minikube startのコマンドは、このようになっています。

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

ドキュメントで、メモリ4GiBにしろって書いてあるんですから、それをきちんと守れって話ですよね・・・。

いや、別にオプションなしで実行しても問題ないんだったら、それに越したことはないのかなって、、、(言い訳)

まとめ

ということで今回のまとめです。

  • Daprの環境が壊れた時は、dapr unintall -k minikube stop Dockerの再起動 minikube start dapr init -k という手順で復旧できる
  • minikubeはデフォルトで2CPU、2GBのメモリで環境を作るが、Javaアプリケーションを5〜6つくらい作成するだけでメモリが足りなくなる
  • minikube start--cpus--memory オプションで、割り当てるリソースを指定ができる
    • ただし既に作成したminikubeはリソース割り当てを変更できないため、一度 minikube delete してから再作成する必要がある
  • docker container stats でDockerのリソース状況を確認できる
  • eval $(minikube docker-env)docker container stats でminikubeのリソース状況を確認できる

いやぁ、こういうことを経験して、使いこなしレベルって上がっていくもんですよね!

それでは、また明日!

Dapr Advent Calendar 16日目 - Dapr + k8sでステート保存とシークレットストア

こんにちは Dapr Advent Calendar 16日目です。最近ちょくちょく周りの人からDaprに興味があるとか、Daprのメリットに魅力を感じるという話を耳にするようになりました。とは言えまだ「業務で使う」ところまで行く人はなかなかいないのですが、来年にはv1.0リリースから1周年を迎えますし、ぼちぼち使う人も出てくるんじゃないかなと思っています。

k8s + Dapr + PostgreSQLでステート保存。あとシークレットストア

今回はk8s + Dapr上のアプリケーションからPostgreSQLへの読み書きを行います。ただ、単純に読み書きするだけなら、過去に作ったアプリケーションを使い、k8s上にデプロイするためのマニフェストファイルを作って kubectl apply するだけで済んでしまうので、あまり面白くありません。せっかくなのでシークレットストアも一緒に使い、アカウント情報などを秘匿する形にしてみましょう。

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

アプリケーションの作成

今回動かすアプリケーションは Dapr Advent Calendar 4日目 - Daprでデータストアにアクセス で作成したものです。そちらを先に読んでからこのエントリーを読んでください。

シークレットストアについては Dapr Advent Calendar 7日目 - Daprでシークレットストアを使う でも解説しているため、そちらも参考にしてください。

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

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

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

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

以前のエントリーで作成したアプリケーションのソースコードをおさらいします。データの読み書きを行うためのコントローラークラスです。

StateController.java

@RestController
public class StateController {
    private RestTemplate restTemplate;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/state/statestore")
    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);
    }
}

ステートを保存する /write と、それを読み込む /read というエンドポイントを提供しています。いずれもデータストア名に statestore を指定して、DaprのStatemangent APIを呼び出しているだけです。

このアプリケーションは8082番ポートで起動するように設定してあります。

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

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

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

eval $(minikube docker-env)

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

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

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

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

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

k8s/state-app.yaml

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

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

以前作成した hello.yaml と同じ構成ですので、説明は割愛します。

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

続いて、データストアとしてPostgreSQLを起動するためのマニフェストファイルを作成します。

パスワードが平文で書かれたマニフェストを作成する

PostgreSQLを起動するためのマニフェストファイルは次のような内容になります。パスワードは平文で指定しています。

k8s/postgresql.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgresql-deploy
  labels:
    app: postgresql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgresql
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      containers:
      - name: postgresql
        image: postgres:11-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_PASSWORD
          value: secretpassword

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

イメージを使ってPostgreSQLをデプロイするマニフェストファイルです。

パスワードは POSTGRES_PASSWORDsecretpassword を指定しています。

DB名とユーザ名は省略しているため、いずれも postgres が使われます。

また、このPostgreSQLに他のpodからアクセスできるよう postgresql-svc という名前のサービスを作り、5432番ポートにpod外からアクセスできるようにしています。

さて、上のマニフェストファイルにはパスワードが平文で書かれているため、このパスワードをシークレットストアに持っていきましょう。k8s自身がシークレットストアの機能を持っているため、今回はそれを使います。

base64エンコードされたパスワードを作成する

まずはパスワードをbase64エンコードするため、次のコマンドを実行します。

echo -n "secretpassword" | base64

-n は、echoの表示時に改行を入れないためのオプションです。これを入れないと、secretpassword の後ろに改行が入ったものがbase64エンコードされてしまいます。僕は一度それでヤラカシたことがあるので気をつけてください。

実行結果は、次のようになります。

c2VjcmV0cGFzc3dvcmQ=

念のため、base64デコードして復元できるかを確認しておきましょう。

echo c2VjcmV0cGFzc3dvcmQ= | base64 --decode
secretpassword

元の文字列が復元できました。

シークレットストアのマニフェストファイルを作成する

続いて、シークレットストアのマニフェストファイルを作成します。上でbase64エンコードしたパスワードを利用します。

k8s/postgresql-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: postgresql-secret
type: Opaque
data:
  postgres_password: c2VjcmV0cGFzc3dvcmQ=

typeOpaque にすると、シークレットの値としてbase64エンコードされた文字列を指定することができます。type には他にも kubernetes.io/tlskubernetes.io/service-account-token などを指定することができますが、通常はこの Opaque で問題なさそうです。

data には key: value 形式でシークレットのキーとbase64エンコードした値を指定します。

これで、シークレットストアの準備ができました。

ちなみにbase64エンコードしているとは言え、これではパスワードを平文と保存していることと大差ないですよね。よりセキュアにするためには、シークレットストアをマニフェストファイルではなくkubectlコマンドで作成するとか、作成したシークレットの詳細を閲覧できるユーザを絞るなどする必要があります。その辺りはk8sの使いこなしの話になるため、今回は割愛したいと思います。

PostgreSQLのマニュフェストファイルからシークレットストアを参照する

続いて、上で作成したPostgreSQLを起動するマニフェストファイルを修正し、シークレットストアを参照するようにします。次のような内容になります。

k8s/postgresql.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgresql-deploy
  labels:
    app: postgresql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgresql
  template:
    metadata:
      labels:
        app: postgresql
    spec:
      containers:
      - name: postgresql
        image: postgres:11-alpine
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgresql-secret
              key: postgres_password

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

変更箇所は、パスワードの部分だけです。

この部分が、

        - name: POSTGRES_PASSWORD
          value: secretpassword

次のようになります。

        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: postgresql-secret
              key: postgres_password

パスワードを平文で書くのではなく valueFrom.secretKeyRef としてシークレットストアを参照するようにしました。

name にはシークレットストア名、key にはシークレットのキーを指定します。

これでPostgreSQLのパスワードとしてシークレットストアの値を利用できるようになりました。今のところDaprは全く関係なくて、ただただk8sマニフェストを作成しているだけです。

DaprからPostgreSQLへの接続設定ファイルの作成

続いて、DaprからPostgreSQLにアクセスするための設定ファイルを作成します。

接続文字列が平文で書かれた接続設定ファイルを作成する

DaprからPostgreSQLに接続するための設定ファイルは次のようになります。

k8s/statestore.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.postgresql
  version: v1
  metadata:
  - name: connectionString
    value: host=postgresql-svc user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres

これは Dapr Advent Calendar 4日目 - Daprでデータストアにアクセス で作成した設定ファイルとほとんど同じです。ローカル環境向けに作ったファイルを、ほぼそのままk8s向けに流用できるのがDaprの良いところです。

metadata.name にデータストア名として statestore を指定しています。これはソースコードで指定した値に合わせます。

このファイルにもやはりパスワードが平文で書かれているため、接続文字列の部分をシークレットストアに移動させましょう。

接続文字列をbase64エンコードする

まずは接続文字列をbase64エンコードします。

echo -n "host=postgresql-svc user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres" | base64
aG9zdD1wb3N0Z3Jlc3FsLXN2YyB1c2VyPXBvc3RncmVzIHBhc3N3b3JkPXNlY3JldHBhc3N3b3JkIHBvcnQ9NTQzMiBjb25uZWN0X3RpbWVvdXQ9MTAgZGF0YWJhc2U9cG9zdGdyZXM=

少し長いですが、base64エンコードされた文字列ができました。

シークレットストアのファイルに追加する

先ほど作ったシークレットストアのファイルに、接続文字列の値を追加します。

k8s/postgresql-secret.yaml

apiVersion: v1
kind: Secret
metadata:
  name: postgresql-secret
type: Opaque
data:
  postgres_password: c2VjcmV0cGFzc3dvcmQ=
  connection: aG9zdD1wb3N0Z3Jlc3FsLXN2YyB1c2VyPXBvc3RncmVzIHBhc3N3b3JkPXNlY3JldHBhc3N3b3JkIHBvcnQ9NTQzMiBjb25uZWN0X3RpbWVvdXQ9MTAgZGF0YWJhc2U9cG9zdGdyZXM=

最後の行に connection というキーで、base64エンコードされた接続文字列を追加しました。

PostgreSQLへの接続設定ファイルからシークレットを参照する

それでは、DaprのPostgreSQL接続設定ファイルからシークレットストアを参照するように修正しましょう。次のようになります。

k8s/statestore.yaml

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

最後の connectionString の部分を修正しました。

修正前

  - name: connectionString
    value: host=postgresql-svc user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres

修正後

  - name: connectionString
    secretKeyRef:
      name: postgresql-secret
      key: connection

シークレットストアを参照するようにしました。こっちは valueFrom: は挟まなくて良いんですね。ちょっと設定ファイルの文法をよく分かってないのですが、これで良いみたいです。

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

イメージをk8sにデプロイ

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

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

まずシークレットストアを作成し、PostgreSQLをデプロイし、そのPostgreSQLにDaprからアクセスするための設定(コンポーネント)をデプロイし、最後にアプリケーションをデプロイする、という順番になっています。

ちなみに複数のファイルを指定してデプロイする際は、次のようにディレクトリを指定してデプロイすることも可能です。

kubectl apply -f k8s

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

また、k8s上にデプロイされたpodやコンポーネントと、手元にあるマニフェストファイルの差分を確認する際には次のコマンドが使えます。

kubectl diff -f k8s

差分を確認してから kubectl apply することで、意図しない設定の更新などを避けることができますね。

まとめて全部削除したい時は、次のようにできます。

kubectl delete -f k8s

これも便利ですね。

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

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

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

kubectl port-forward service/state-svc 8082:8082

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

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

curl -XPOST "localhost:8082/write" -H "Content-type:application/json" -d '[
  {
    "key": "cero_t",
    "value": {
      "name": "Shin Tanimoto",
      "twitter": "@cero_t"
    }
  },
  {
    "key": "BABYMETAL",
    "value": [
      {
        "name": "SU-METAL",
        "alias": "Suzuka Nakamoto"
      },
      {
        "name": "MOAMETAL",
        "alias": "Moa Kikuchi"
      },
      {
        "name": "YUIMETAL",
        "alias": "Yui Mizuno"
      }
    ]
  }
]'

エラーなく応答が返ってくれば成功です。エラーが発生した場合は kubectl logskubectl describe pods コマンドなどで詳細を確認してみてください。

次のコマンドで、保存したデータを取得します。

curl localhost:8082/read/cero_t

次のようなデータが取得できます。

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

問題なくPostgreSQLにデータが保存され、取得できることが分かりました。

f:id:cero-t:20211216120656p:plain
k8sのシークレットストアを参照するアプリケーション

どのシークレットストアを使うべきか

今回はk8sでシークレットストアを作成し、k8sマニフェストファイルと、Daprの接続設定ファイルからそれぞれ利用する形にしました。一方 Dapr Advent Calendar 7日目 - Daprでシークレットストアを使う で利用したDaprのシークレットストア機能については出番がありませんでした。この機能はどういう時に使うべきなのでしょうか。

僕自身の使い分けとしては、正味の話、Daprのシークレットストアを使う機会は今のところありません。ローカル環境で(k8sなしで)開発する時はシークレットストアを使うことがなく、設定ファイルに直接パスワードなどを書いていますし、k8sを利用する検証環境や運用環境では、k8sのシークレットストアを使うためです。

「アプリケーション内から外部サービスを利用する際に必要となるアカウント情報」などは、Daprのシークレットストアに問い合わせて取得する可能性があるのですが、僕自身の経験としては、そのような情報は開発時には application.yaml に書いておき、k8s環境ではk8sのシークレットストアから取得した情報を使って、application.yamlの値を環境変数で上書きするようにしています。そうするとDaprのシークレットストアの出番がなくなるのですよね。

Daprをローカル環境でもk8s環境でもなく、個別のサーバにデプロイして運用する際には、もしかしたらDaprのシークレットストア機能の出番になるかも知れませんね。そのように使ってみることがあれば、またブログなどで紹介したいと思います。

まとめ

  • Daprの設定ファイルからk8sのシークレットストアを参照することができます
  • k8sで運用するシステムにおいては、Dapr側のシークレットストア機能を使うことはないかも知れません

今回の話を要約すると、ずいぶんシンプルになっちゃいましたね。

それでは、また明日!

追記

まきんぐさんから、平文のままシークレットを作ることもできるよと教えてもらいました。知らなかった!

世のサイトの例ではだいたいbase64エンコードされた文字列を使っているようですが、base64にすることで「カジュアルに目に触れてしまった時にパッとは値が分からない」くらいの効果ならあるものの、そんな頼りない効果が不要であれば、平文で作ってしまっても良いでしょうね。

Dapr Advent Calendar 15日目 - Dapr + k8sでメッセージング

こんにちは Dapr Advent Calendar 15日目です。急に冷え込みましたが皆さんお風邪など召されてないでしょうか。

k8s + Dapr + RabbitMQのメッセージング

今回はk8s + Dapr上でRabbitMQを介したメッセージングを行います。

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

アプリケーションの作成

動かすアプリケーションは Dapr Advent Calendar 5日目 - Daprでメッセージング で作成したものです。そちらを先に読んでからこのエントリーを読んでください。

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

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

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

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

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

メッセージを送る側(publish)のアプリケーション

メッセージを送る側のアプリケーションは、DaprのPub/sub APIを使ってメッセージを送ります。

(publish) PublishController.java

@RestController
public class PublishController {
    private RestTemplate restTemplate;

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

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

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

メッセージを送信する /publish というエンドポイントで、DaprのPub/sub APIを呼び出してエンキューする処理を書いています。

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

(publish) application.properties

server.port=8083
pubsubUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/publish/pubsub/my-message

ポート8083番で起動します。

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

(publish) application-rabbitmq.properties

pubsubUrl=http://localhost:${DAPR_HTTP_PORT}/v1.0/publish/rabbitmq-pubsub/my-message

Springのプロファイルが rabbitmq の場合は pubsubUrlhttp://localhost:${DAPR_HTTP_PORT}/v1.0/publish/rabbitmq-pubsub/my-message となります。pubsub名が rabbitmq-pubsub で、メッセージトピックが my-message となっています。

メッセージを受ける側(subscribe)のアプリケーション

メッセージを受ける側のアプリケーションは、ただWeb APIのエンドポイントを作成するだけです。

(subscribe) SubscribeController.java

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

メッセージを受ける /subscribe というエンドポイントで、受け取ったメッセージを標準出力に出力しています。

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

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

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

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

eval $(minikube docker-env)

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

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

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

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

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

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

まずはpublish側のアプリケーションをデプロイするためのマニフェストファイルです。

k8s/publish-app.yaml

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

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

publish側は以前作成した hello.yaml と同じく、podを作る Deployment と、pod外からpodにアクセスできるようにするための Service という構成です。

また環境変数として、次のような値を指定しています。

        env:
          - name: spring.profiles.active
            value: rabbitmq

Springのプロファイルを rabbitmq にすることで application-rabbitmq.yaml が利用されるようにしています。

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

続いて、subscribe側のアプリケーションをデプロイするためのマニフェストファイルです。

k8s/subscribe-app.yaml

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

subscribe側はServiceを作成しません。内部通信のみ行い、外部からアクセスする必要がないためです。

RabbitMQのマニフェストファイル

次に、メッセージブローカーであるRabbitMQを起動するためのマニフェストファイルを作成します。

k8s/rabbitmq.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rabbitmq
  labels:
    app: rabbitmq
spec:
  replicas: 1
  selector:
    matchLabels:
      app: rabbitmq
  template:
    metadata:
      labels:
        app: rabbitmq
    spec:
      containers:
      - name: rabbitmq
        image: rabbitmq:3-management
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 5672
        - containerPort: 15672

---
kind: Service
apiVersion: v1
metadata:
  name: rabbitmq-svc
  labels:
    app: rabbitmq
spec:
  type: LoadBalancer
  selector:
    app: rabbitmq
  ports:
  - name: queue
    protocol: TCP
    port: 5672
    targetPort: 5672
  - name: management
    protocol: TCP
    port: 15672
    targetPort: 15672

DockerHubにあるイメージを使ってRabbitMQをデプロイするマニフェストファイルです。

spec.template.spec.containers.ports に2つのポートを指定しています。

        - containerPort: 5672
        - containerPort: 15672

これは、キューが5672番ポート、管理コンソールが15672番ポートをそれぞれ利用するためです。

またpodの外部から接続できるよう、Serviceとして rabbitmq-svc を作成し、5672番ポートと15672番ポートでアクセスできるようにしています。

DaprからRabbitMQに接続するためのマニフェストファイル

さらに、DaprからRabbitMQにアクセスするための設定ファイルを作成します。

k8s/rabbitmq-pubsub.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: rabbitmq-pubsub
spec:
  type: pubsub.rabbitmq
  version: v1
  metadata:
  - name: host
    value: amqp://rabbitmq-svc:5672

これは host の値を除き Dapr Advent Calendar 5日目 - Daprでメッセージング で作成した設定ファイルと全く同じです。その際には --components-path 引数で設定ファイルの場所を指定して利用しました。

しかしk8sで運用する際には、このファイルを kubectl apply でそのまま利用する形となります。初めて動かした時は「なんでDaprで作成したファイルをそのままk8sで使えるの!?」って思ったのですが、Daprは設定ファイルの形式をk8sと合わせることで、そのままk8sで使えるようにしている、という理解で良いのでしょうかね。

metadata.name として rabbitmq-pubsub という名前をつけました。また hostamqp://rabbitmq-svc:5672 を指定しています。podからpodへのアクセスはServiceを利用するのですが、ここでホストのアドレスにServiceの名前を指定することで、k8sDNSを利用して名前解決が行われ、目的のpodにアクセスできるようになります。

subscribeするためのマニフェストファイル

そして、subscribeアプリケーションがRabbitMQからメッセージを受け取るための設定ファイルを作成します。

k8s/subscription.yaml

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

やはりこれも Dapr Advent Calendar 5日目 - Daprでメッセージング で作成した設定ファイルと全く同じです。Componentファイルだけでなく、Subscriptionファイルもそのままk8sにapplyできるのです。

rabbitmq-pubsubコンポーネントを利用し、メッセージトピックである my-message に対してデキューし、メッセージを取得したら subscribe-app/subscribe を呼び出すという設定となっています。

設定に関してもう少し詳しく知りたい場合は Dapr Advent Calendar 5日目 - Daprでメッセージング を読んでください。

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

イメージをk8sにデプロイ

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

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

RabbitMQをデプロイし、そのRabbitMQにDaprからアクセスするためのコンポーネントをデプロイし、RibbitMQコンポーネントに対するsubscribeの設定をする、という順番です。

次のコマンドでRabbitMQが起動したことを確認します。

kubectl get pods

次のように表示されていればRabbitMQの起動は完了です。

NAME                           READY   STATUS        RESTARTS   AGE
rabbitmq-5d474484f5-krnwj      1/1     Running       0          10s

また、pubsubの設定などが本当に反映されているかも気になるところでしょうから、それらも確認しておきます。

kubectl get components
NAME              AGE
rabbitmq-pubsub   1m

問題なくコンポーネントが作成されていますね。

subscriptionも確認しておきましょう。

kubectl get subscriptions
NAME           AGE
subscription   1m

やはり問題なく反映されています。

ちなみに -o yaml オプションをつければ、それぞれの設定をyaml形式で表示することができます。ここでは出力結果は割愛しますが、適用したyamlファイル(+いくつかのフィールドに初期設定値が適用されたもの)が表示されるはずです。

続いて、アプリケーション2種類をデプロイします。

kubectl apply -f k8s/publish-app.yaml
kubectl apply -f k8s/subscribe-app.yaml

これでデプロイは完了です。

Serviceをポートフォワードしてアクセスできるようにする

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

publishアプリケーションにアクセスするため、次のコマンドを実行します。

kubectl port-forward service/publish-svc 8083:8083

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

また、RabbitMQの管理コンソールにアクセスできるよう、別のコンソールで次のコマンドを実行します。

kubectl port-forward service/rabbitmq-svc 15672:15672

これでローカルPCの15672番ポート経由でRabitMQの管理コンソールにアクセスできるになりました。

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

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

RabbitMQのコンソールを確認

まずはRabbitMQのコンソールからアクセスします。ブラウザで次のURLを開いてください。

http://localhost:15672/

ログインユーザの初期アカウントは guest / guest です。

ExchangeタブやQueueタブを開けば、my-message というExchangeと、subscribe-app-my-message というQueueができていることが分かります。

f:id:cero-t:20211215020650p:plain:w600
Exchangeの一覧

f:id:cero-t:20211215020707p:plain:w800
Queueの一覧

これらは、Dapr Advent Calendar 5日目 - Daprでメッセージング で説明したとおり、subscribe側のアプリケーションがデプロイされた時点で作られます。

メッセージを送る

それでは別のコンソールからアクセスしてみましょう。次のコマンドでメッセージを送ってみます。

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

特にエラーなく応答が返ってくれば成功です。

受け取ったメッセージの確認

受け取ったメッセージをログで確認してみましょう。

ログを確認するために、podの一覧を取得します。

kubectl get pods
NAME                           READY   STATUS        RESTARTS   AGE
publish-app-5b4c66c9-7gl4j     2/2     Running       0          4m37s
rabbitmq-5d474484f5-krnwj      1/1     Running       0          4m50s
subscribe-app-cdc58446-66m22   2/2     Running       0          4m36s

これでsubscribe-appの名称が分かったため、次のコマンドでログを確認します。

kubectl logs subscribe-app-cdc58446-66m22 subscribe

ログの一番下に、次のようなメッセージが表示されているはずです。

{"id":"e1429fce-9875-45b8-b8d1-5a93d452523f","topic":"my-message","pubsubname":"rabbitmq-pubsub","data":{"name":"Shin Tanimoto","twitter":"@cero_t"},"specversion":"1.0","datacontenttype":"application/json","source":"publish-app","type":"com.dapr.event.sent","traceid":"00-01f98d679d9373efa8206d82d3d2c293-c01c8ed071c12a1a-00"}

data の部分で送ったメッセージをきちんと受け取れていることが分かりましたね!

f:id:cero-t:20211215090713p:plain
ちょっと複雑になってきたk8sで動くアプリケーション

ローカル環境向けに開発していたファイルをそのまま使える

今回は、ローカル環境でDaprを動かす際に作成したpublishとsubscribeのアプリケーションをデプロイして、メッセージングを行いました。

前回、前々回のエントリーでもそうでしたが、今回もソースコードは一文字も変更していません。しかも、ComponentやSubscriptionの設定ファイルも、ホスト名部分のみ変えたものの、他は一切変更していません。

このように、動かす環境が変わってもソースコードを変更する必要がなく、またk8sでデプロイするためのマニフェストファイルを作成する必要があるものの、既存の設定ファイルをほぼそのまま使えるというのが、Daprの大きなメリットの1つです。

Daprというランタイム上で動くように開発できれば、Dapr自身がクラウド上で動いていようが、ローカル環境で動いていようが構わないのです。

まとめ

  • ローカル環境向けに作ったメッセージングのアプリケーションを、そのままk8s上で動かすことができました
  • ソースコードは全く変更しませんでした
  • ComponentやServiceの設定ファイルも、ほぼそのままマニフェストファイルとして流用できました

インフラにあまり詳しくないエンジニアがアプリケーションを開発し、インフラに詳しいエンジニアがそのアプリケーションをクラウドk8s上で運用する、なんて体制にすることもできるでしょう。実際、僕はそれに近い体制で運用をしていますし、それで大きくハマることもありませんでした。なかなか魅力的、ですよね。

それでは、また明日!

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の弱点のようにも思っていたところだったので、どんなエントリーなのか楽しみです!

それでは、また次回!