こんにちは Dapr Advent Calendar 15日目です。急に冷え込みましたが皆さんお風邪など召されてないでしょうか。
k8s + Dapr + RabbitMQのメッセージング
今回はk8s + Dapr上でRabbitMQを介したメッセージングを行います。
アプリケーションの作成
動かすアプリケーションは Dapr Advent Calendar 5日目 - Daprでメッセージング で作成したものです。そちらを先に読んでからこのエントリーを読んでください。
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
の場合は pubsubUrl
が http://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側のアプリケーションをデプロイするためのマニフェストファイルです。
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側のアプリケーションをデプロイするためのマニフェストファイルです。
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を起動するためのマニフェストファイルを作成します。
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にアクセスするための設定ファイルを作成します。
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
という名前をつけました。また host
に amqp://rabbitmq-svc:5672
を指定しています。podからpodへのアクセスはServiceを利用するのですが、ここでホストのアドレスにServiceの名前を指定することで、k8sのDNSを利用して名前解決が行われ、目的のpodにアクセスできるようになります。
subscribeするためのマニフェストファイル
そして、subscribeアプリケーションがRabbitMQからメッセージを受け取るための設定ファイルを作成します。
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を開いてください。
ログインユーザの初期アカウントは guest
/ guest
です。
ExchangeタブやQueueタブを開けば、my-message
というExchangeと、subscribe-app-my-message
という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
の部分で送ったメッセージをきちんと受け取れていることが分かりましたね!
ローカル環境向けに開発していたファイルをそのまま使える
今回は、ローカル環境でDaprを動かす際に作成したpublishとsubscribeのアプリケーションをデプロイして、メッセージングを行いました。
前回、前々回のエントリーでもそうでしたが、今回もソースコードは一文字も変更していません。しかも、ComponentやSubscriptionの設定ファイルも、ホスト名部分のみ変えたものの、他は一切変更していません。
このように、動かす環境が変わってもソースコードを変更する必要がなく、またk8sでデプロイするためのマニフェストファイルを作成する必要があるものの、既存の設定ファイルをほぼそのまま使えるというのが、Daprの大きなメリットの1つです。
Daprというランタイム上で動くように開発できれば、Dapr自身がクラウド上で動いていようが、ローカル環境で動いていようが構わないのです。
まとめ
- ローカル環境向けに作ったメッセージングのアプリケーションを、そのままk8s上で動かすことができました
- ソースコードは全く変更しませんでした
- ComponentやServiceの設定ファイルも、ほぼそのままマニフェストファイルとして流用できました
インフラにあまり詳しくないエンジニアがアプリケーションを開発し、インフラに詳しいエンジニアがそのアプリケーションをクラウドのk8s上で運用する、なんて体制にすることもできるでしょう。実際、僕はそれに近い体制で運用をしていますし、それで大きくハマることもありませんでした。なかなか魅力的、ですよね。
それでは、また明日!