谷本 心 in せろ部屋

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

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上で運用する、なんて体制にすることもできるでしょう。実際、僕はそれに近い体制で運用をしていますし、それで大きくハマることもありませんでした。なかなか魅力的、ですよね。

それでは、また明日!