Kubernetes環境でHelmを利用してRabbitMQクラスターを構築する

# はじめに

MQは名前のとおりQueueを扱うミドルウェアで、IBM MQのようにメインフレームと分散環境を接続するために使われたり、Webから検索クエリーをバックエンドに投げるために利用されるような比較的即時性を求める環境に適用されたり、一通のメッセージを多くの対象に配布するために利用されています。

今回は最も良く使われるであろう不定期にジョブを投入するための入口を作るためにMQが欲しくなったので、k8s環境にRabbitMQのクラスターを構築してみました。

> これから導入する場合、比較的新しいK8sクラスターを利用しなければいけない前提条件はありますが、Helmよりも、https://qiita.com/YasuhiroABE/items/7c1e82e006ea37e0fe25 のようにOperatorを利用してください。

# References

* https://github.com/rabbitmq/rabbitmq-peer-discovery-k8s
* https://zupzup.org/k8s-rabbitmq-cluster/
* https://stackoverflow.com/questions/51096003/how-to-install-rabbitmq-plugin-on-kubernetes
* https://www.rabbitmq.com/cluster-formation.html
* https://github.com/ruby-amqp/bunny
* https://hub.docker.com/_/rabbitmq
* https://github.com/helm/charts/tree/master/stable/rabbitmq
* https://github.com/bitnami/bitnami-docker-rabbitmq
* https://github.com/helm/charts/issues/13485

# 最初に試したこと

https://zupzup.org/k8s-rabbitmq-cluster/ に書かれているようにRabbitMQの設定をしてみましたが、StatefulSetのPodが1つだけしか稼動しない状況になってしまいました。それとPVCの構成はされていないので、Productionで利用するような構成とは違うかなと感じたところです。

この時に使用したimageはdockerhubに登録されているオフィシャルのrabbitmq:3.8.2を利用しました。

# Helmによる導入

PVCも利用するようなので、Helmを利用してみることにしました。あらかじめ```helm init```コマンドを実行しています。

```bash
$ kubectl create ns rabbitmq
$ helm install stable/rabbitmq --name prodmq --namespace rabbitmq --set persistence.storageClass=rook-ceph-block --set replicas=2 --set rabbitmq.erlangCookie=eefa49d4df0756718de40b0cd437f778 --set rabbitmq.password=U8cAed52fg

```

storageClassを指定する必要があり、replicasを変更したかったため、これらのパラメータを変更しています。

## 導入などに利用したMakefileの内容

次のようなMakefileを利用して、セットアップなどに利用しています。

```Makefile:Makefile
NAMESPACE = rabbitmq
REL_NAME = prodmq

.PHONY: init inspect install upgrade delete

init:
        kubectl create ns $(NAMESPACE)

inspect:
        helm inspect stable/rabbitmq

install:
        helm install stable/rabbitmq --name $(REL_NAME) --namespace $(NAMESPACE) --set persistence.storageClass=rook-ceph-block --set replicas=2 --set rabbitmq.erlangCookie=eefa49d4df0756718de40b0cd437f778 --set rabbitmq.password=U8cAed52fg

upgrade:
        helm upgrade --namespace $(NAMESPACE) $(REL_NAME) --set persistence.storageClass=rook-ceph-block --set replicas=2 --set rabbitmq.erlangCookie=eefa49d4df0756718de40b0cd437f778 --set rabbitmq.password=U8cAed52fg stable/rabbitmq

delete:
        helm del --purge $(REL_NAME)

```

# Helmによる導入と、手動による導入の比較

利用しているimageについては、bitnamiがbuildしたものを利用しています。公開しているDockerfile https://github.com/bitnami/bitnami-docker-rabbitmq/blob/master/3.8/debian-9/Dockerfile を確認してみると、localeの設定や、non-root userの利用など、より望ましい構成がされているといえるかもしれません。

細かい指定は違いますが、およその設定は参考にした記事にある手動での構成とほぼ同じなので、どこが悪かったのかは判明していません。Helmは便利ですが、必要以上の設定が行なわれてしまう事は良い事でもあり、後々の対応を考えると、自分で細かい部分の調整を難しくするネガティブな要素も持ち合わせているなと感じています。

ただ様々なアイデアが詰め込まれているのは事実なので、helm inspect/fetchをして、有名なChartの内部を確認しておくことは勉強になるでしょう。

# helm delete後の不整合について

helm del --purge <release-name> を実行しても、PVCは削除されないため、再度 helm install コマンドを実行するとPVCは再利用されます。このため下記のissueに掲載されているように起動時に Waiting for Mnesia tables のメッセージが表示されています。

* https://github.com/helm/charts/issues/13485

これはerlangCookieとpasswordをhelm install時に同じものを設定することで、ほぼ解決できます。

## RABBITMQ_PASSWORDの設定について

RABBITMQ_PASSWORDとRABBITMQ_ERL_COOKIEはSecretに保存されています。setオプションを指定しない場合は、helm installが実行されるタイミングで自動生成されるため、これを変更することは可能ですが、確実に不整合が発生します。


```text:Chartに記載されている注意事項
Note: please note it's mandatory to indicate the password and erlangCookie that was set the first time the chart was installed to upgrade the chart. Otherwise, new pods won't be able to join the cluster.
```

PVCの再利用も、token・passwordの再生成、いずれの問題もクラスター全体が再起動するタイミングでは大きなリスクになります。リスクを減らすためには、必ずsetオプションで、静的に同一のerlangCookie、passwordを設定する必要があると思われます。

## Waiting for Mnesia tables への対応

issuesで指摘されているように、サーバーを停止して ```$ rabbitmqctl force_boot``` を実行する事が対応の1つです。

## 方法1: helm upgradeによるforce_bootの実行

helm inspectをすると最後に、forceBoot.enabledオプションの記述があります。ほぼ同時にPodを削除し、強引にエラー状態にしてから、helm upgradeを実行します。

```bash:forceBoot.enabled=tureを指定した実行例
$ helm upgrade --namespace $(NAMESPACE) $(REL_NAME) --set forceBoot.enabled=true --set persistence.storageClass=rook-ceph-block --set replicas=2 --set rabbitmq.erlangCookie=eefa49d4df0756718de40b0cd437f778 --set rabbitmq.password=U8cAed52fg stable/rabbitmq

## 既に起動が始まっているPodの削除
$ kubectl -n rabbitmq delete pod/prodmq-rabbitmq-0
```

これによって問題が解決することは確認しています。

## 方法2: 手動によるforce_bootの実行

statefulsetの定義を編集し、.spec.template.spec.containers.command の中で exec rabbitmq-server を実行する手前で rabbitmqctlコマンドを実行します。

```bash:rabbitmqネームスペースを利用している場合の例
$ kubectl -n rabbitmq edit statefulset.apps/prodmq-rabbitmq  
```

エディタで編集しますが、viが苦手な場合は事前にEDITOR環境変数を編集する(e.g. $ env EDITOR="emacs -nw" kubectl -n rabbitmq edit statefulset.apps/prodmq-rabbitmq)などしておきます。

```yaml:編集例
         #replace the default password that is generated
          sed -i "/CHANGEME/cdefault_pass=${RABBITMQ_PASSWORD//\\/\\\\}" /opt/bitnami/rabbitmq/etc/rabbitmq/rabbitmq.conf
          rabbitmqctl force_boot
          exec rabbitmq-server
        env:
```

この状態で、全てのStatefulSetから生成されるPod(helmを利用した場合は、"<release-name>-rabbitmq-0"等のPod)をそれぞれ一回削除することで状態の回復が期待できます。全てのPodが再起動したら再度editし、追加した行は削除しておきます。

それでもシステム全体がshutdownした最悪のケースでは、立ち上げに失敗し、システム全体を再度helm installする必要があるだろうと想定しています。接続用のPasswordを再度定義する必要がないように-setオプションを利用するか、不足する場合にはカスタマイズしたChartを準備するといった作業が必要になるかもしれません。

今回はシステム全体がダウンした場合には、Queueのデータが失われてもre-runできるので、クリティカルにはならないかなと思っています。

## 方法3: PVC上にforce_loadファイルを作成する

https://github.com/helm/charts/issues/13485 の中で紹介されている方法で、"/opt/bitnami/rabbitmq/var/lib/rabbitmq/mnesia/rabbit@rabbitmq-pre-0.rabbitmq-pre-headless.pre.svc.cluster.local" ディレクトリに force_loadファイルを作成するという方法です。

```bash
$ kubectl -n rabbitmq exec -it pod/myrabbitmq-0 bash
> cd /opt/bitnami/rabbitmq/var/lib/rabbitmq/mnesia/
## この下にできるディレクトリ名は、namespace等によって変化します。
> cd *.src.cluster.local
> touch force_load 
## 作成するファイル名は、"force_boot"ではなく、"force_load"
> exit
## Podを停止し、再起動させる。
$ kubectl -n rabbitmq delete pod/myrabbitmq-0
```

この方法でも問題なく復旧しましたが、問題は少しあって、Podは一定時間で再起動されるので、その時間内にPod内でforce_loadファイルを作成する作業を完了させる必要があります。

# Helmで構築したRabbitMQへの接続

既存のServiceを変更するのではなく、amqpのポートだけを公開するようなService定義を追加しています。可能であれば同一のnamespaceに入れて、こういった設定は不要にするのが良いでしょう。

```yaml:helm-svc-lb.yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: rabbitmq
  name: prodmq-rabbitmq-lb
  namespace: rabbitmq
spec:
  ports:
  - name: amqp
    port: 5672
    protocol: TCP
    targetPort: amqp
  selector:
    app: rabbitmq
    release: prodmq
  type: LoadBalancer
  loadBalancerIP: 192.168.1.39
```

作成したファイルは、```$ kubectl -n rabbitmq apply -f helm-svc-lb.yaml``` で適用しています。

# 再起動後にQueueが起動しなくなった問題への対応

K8s全体を再起動する必要があり、最終的にRabbitMQだけが正常に起動しない状況になりました。
Queueの状態がRunningになっていないのですが、Podのログなど他の部分では特に問題が発生していません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/05f705f9-3a97-1c01-6545-5acbffb7e2aa.png)

内部状態を変更する良い方法はないようだったので、この状態の設定をExport definitionsからダウンロードして、一旦helm deleteしてから再度インストールして、設定をImport definitionsからアップロードすることにしました。

PVCも削除し、再導入したまっさらなRabbitMQクラスターに設定をimportしてみると内部エラーに遭遇しました。

```text:import時にブラウザに出力されたエラー
 ["amqp_error","internal_error",[67,97,110,110,111,116,32,100,101,99,108,97,114,101,32,97,32,113,117,101,117,101,32,....
```

erlangあるあるでどうせバイト列を数値で出力しているんだろうと思ったので、”Cannot dete..."ぐらいまでデコードして

```ruby:数値をCharに変換してみた時に使ったirbコード
irb(main):001:0> s = [67,97,110,110,111,116,32,100,101,99,108,97,114,101,32,97,32,113,117,101,117,101,32]
irb(main):002:0> s.each{|c|puts format("%c",c)}  ## Cannot declare a queue ...
```

素直にpodのlogを確認します。

```bash
$ kubectl -n rabbitmq logs statefulset.apps/myrabbitmq  rabbitmq
2020-09-07 14:30:35.193 [warning] <0.3790.0> ra: failed to form new cluster '/yasu_yasu.test.dlx'.
 Error: {timeout,{'/yasu_yasu.test.dlx','rabbit@myrabbitmq-1.myrabbitmq-headless.rabbitmq.svc.cluster.local'}}
2020-09-07 14:30:43.145 [info] <0.3812.0> queue 'yasu.test.dlx' in vhost '/yasu': terminating with shutdown in state candidate
2020-09-07 14:30:43.145 [info] <0.3790.0> Deleting server '/yasu_yasu.test.dlx' and its data directory.
2020-09-07 14:30:43.479 [error] <0.3790.0> Encountered an error when importing definitions: {amqp_error,internal_error,"Cannot declare a queue 'queue 'yasu.test.dlx' in vhost '/yasu'' on node 'rabbit@myrabbitmq-0.myrabbitmq-headless.rabbitmq.svc.cluster.local': cluster_not_formed",none}
```

cluster_not_formedはRa moduleがcluster_startやrestart時に発生するエラーのようなので、内部状態がまだ完全ではなかったのかもしれませんが、Import作業を繰り返すと構成が反映されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/797ac09f-e480-a98d-e62c-3f3f43545950.png)

Queueの状態は全てRunningになり、無事にメッセージをput/getすることができるようになりました。

今回はLong-running Jobに指示を与えるだけの短文をやりとりするだけなので、状態は無視できたのでメッセージがロストしても問題ありませんでしたが、前提としてロストした時に問題が発生するようだと怖いかなとは感じました。

以上