本家のチュートリアルに従ってRabbitMQ Operatorをデプロイしてみた

# はじめに

Helmを利用してRabbitMQを稼動させていましたが、いろいろ問題が発生していました。

別クラスターを構築したタイミングで本家のドキュメントに従ってRabbitMQをK8s環境で稼動させたので、その際のメモを残します。

# 参考文書

* [Deploying RabbitMQ to Kubernetes: What's Involved?](https://www.rabbitmq.com/blog/2020/08/10/deploying-rabbitmq-to-kubernetes-whats-involved/)
* [Installing RabbitMQ Cluster Operator in a Kubernetes cluster](https://www.rabbitmq.com/kubernetes/operator/install-operator.html)
* [Using RabbitMQ Cluster Kubernetes Operator](https://www.rabbitmq.com/kubernetes/operator/using-operator.html)
  * [Override Resource Properties](https://www.rabbitmq.com/kubernetes/operator/using-operator.html#override)

## Operator v2.xへバージョンアップする際に参考にした資料

* [Github - Pause Reconsiliation](https://www.rabbitmq.com/kubernetes/operator/using-operator#pause)
* [Github - RabbitMQ Operator v1.6.0 Release Notes](https://github.com/rabbitmq/cluster-operator/releases/tag/v1.6.0)
* [RabbitMQ - Erlang Version Requirements](https://www.rabbitmq.com/which-erlang.html)
* [RabbitMQ - v3.11.x Required feature flags](https://blog.rabbitmq.com/posts/2022/07/required-feature-flags-in-rabbitmq-3.11/)
* [RabbitMQ - Upgrading RabbitMQ (Main guide)](https://www.rabbitmq.com/upgrade.html)

# 環境

K8sクラスターは以下のような環境です。

* Hardware: TX1320 M4 (富士通製 Xeon E-2234)
  * 64GB Memory (DDR4-21300 ECC)
  * 4TB HDD (/dev/sda)
  * 500GB SSD (/dev/sdb)
* K8s: v1.19.7 (Kubesprayで構築) → v1.29.5
  * Rook/Ceph v1.5.5 → v1.13.10

バージョンアップを続けて、現在はRabbitMQ v3.13.7を運用しています。

# 方針

今回はRabbitMQ Operatorを使用します。これはK8sのバージョンがv1.17以上、Dockerイメージが3.8.8以上という条件があって、以前構築したクラスターでは利用できないものでした。

本番に移行させることを目的に、テスト環境を構築したので、この手順を試してみることにします。

# 作業メモ

あらかじめRabbitMQ Cluster Operatorを導入しておきます。

```bash
$ sudo kubectl apply -f https://github.com/rabbitmq/cluster-operator/releases/latest/download/cluster-operator.yml
```

ここからの作業は参考文書に掲載したUsing RabbitMQ Cluster Kubernetes Operatorに従っていきます。

## CRDsについて

Custom Resources (CRDs)を確認するよう指示があります。

```bash:
$ sudo kubectl get crd/rabbitmqclusters.rabbitmq.com    
NAME                            CREATED AT
rabbitmqclusters.rabbitmq.com   2021-01-26T05:59:59Z
```

確認するだけであればこれで十分ですが、どのようなCRDsが定義されているかは```-o yaml```などの冗長出力によって確認することができます。

```bash
$ sudo kubectl get crd/rabbitmqclusters.rabbitmq.com -o yaml
```

長々と出力されますが定義されているリソースは、"RabbitmqCluster"だけのようです。


## RabbitMQインスタンスの実行

書いてあるとおり、definition.yamlファイルを準備して適用します。

```bash:一連の作業ログ
$ cat > 01.definition.yaml
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
  name: definition

$ sudo kubectl -n rabbitmq-system apply -f 01.definition.yaml
rabbitmqcluster.rabbitmq.com/definition created

$ sudo kubectl -n rabbitmq-system get all
NAME                                             READY   STATUS    RESTARTS   AGE
pod/definition-server-0                          0/1     Pending   0          2s
pod/rabbitmq-cluster-operator-7bbbb8d559-884lc   1/1     Running   0          3h40m

NAME                       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)              AGE
service/definition         ClusterIP   10.233.7.222   <none>        5672/TCP,15672/TCP   2s
service/definition-nodes   ClusterIP   None           <none>        4369/TCP,25672/TCP   2s

NAME                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/rabbitmq-cluster-operator   1/1     1            1           3h40m

NAME                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/rabbitmq-cluster-operator-7bbbb8d559   1         1         1       3h40m

NAME                                 READY   AGE
statefulset.apps/definition-server   0/1     2s
```

Podは現時点ではPendingのままです。原因についてはdescribeで確認します。

```bash:Pendingとなっている理由をPodから確認する
$ sudo kubectl -n rabbitmq-system describe pod definition-server-0 
...
Events:
  Type     Reason            Age   From               Message
  ----     ------            ----  ----               -------
  Warning  FailedScheduling  22h   default-scheduler  0/3 nodes are available: 3 pod has unbound immediate PersistentVolumeClaims.
```

PVCを確認すると、次のようになっていました。

```bash:PVCの状態を確認する
$ sudo kubectl -n rabbitmq-system get pvc
NAME                              STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistence-definition-server-0   Pending                                                     10m
```

## Pending状態の解消

PVはRook/Cephで構成しているために、PVC定義に```storageClassName: rook-ceph-block```を指定することが必要です。
RabbitMQのドキュメントにstorageClassNameを指定する方法が掲載されているので、これを参照し、01.definition.yamlを編集し、再度適用します。

```bash:ファイルを更新し、再度適用する
$ cat 01.definition.yaml
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
  name: definition
spec:
  persistence:
    storageClassName: rook-ceph-block
    storage: 20Gi

$ sudo kubectl -n rabbitmq-system delete -f 01.definition.yaml
$ sudo kubectl -n rabbitmq-system apply -f 01.definition.yaml
```

これで無事にサービスが起動するところまでは確認できました。

```bash:PVCとPodがRunningになっている事を確認する
$ sudo kubectl -n rabbitmq-system get all
NAME                                             READY   STATUS    RESTARTS   AGE
pod/definition-server-0                          0/1     Running   0          27s
pod/rabbitmq-cluster-operator-7bbbb8d559-884lc   1/1     Running   0          26h

NAME                       TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)              AGE
service/definition         ClusterIP   10.233.49.46   <none>        5672/TCP,15672/TCP   27s
service/definition-nodes   ClusterIP   None           <none>        4369/TCP,25672/TCP   27s

NAME                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/rabbitmq-cluster-operator   1/1     1            1           26h

NAME                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/rabbitmq-cluster-operator-7bbbb8d559   1         1         1       26h

NAME                                 READY   AGE
statefulset.apps/definition-server   0/1     27s

$ sudo kubectl -n rabbitmq-system get pvc
NAME                              STATUS   VOLUME                                     CAPACITY   ACCESS MODES  STORAGECLASS      AGE
persistence-definition-server-0   Bound    pvc-5cf12c25-aeb0-45f5-9ce6-b9825ec0946d   20Gi       RWO           rook-ceph-block   40s
```

## LoadBalancerの有効化

このままだとサービスにWebブラウザからアクセスすることが難しいので、LoadBalancerを有効にします。

```bash:
$ cat 01.definition.yaml 
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
  name: definition
spec:
  persistence:
    storageClassName: rook-ceph-block
    storage: 20Gi
  service:
    type: LoadBalancer


$ sudo kubectl -n rabbitmq-system apply -f 01.definition.yaml 
rabbitmqcluster.rabbitmq.com/definition configured

$ sudo kubectl -n rabbitmq-system get svc
NAME               TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)                          AGE
definition         LoadBalancer   10.233.49.46   192.168.1.110  5672:30104/TCP,15672:30706/TCP   59m
definition-nodes   ClusterIP      None           <none>         4369/TCP,25672/TCP               59m
```

ドキュメントを眺める限りはLBにIPを割り当てるための設定(loadBalancerIP)がないようだと思って初稿を書いたのですが、spec.override.service が定義可能だと分かって次のような設定を試しました。

```yaml:02.definition.yaml
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
  name: definition
spec:
  replicas: 3
  persistence:
    storageClassName: rook-ceph-block
    storage: 20Gi
  service:
    type: LoadBalancer
  override:
    service:
      spec:
        loadBalancerIP: 192.168.1.139
```

この設定を適用すると、EXTERNAL-IPが指定した値になります。


### EXTERNAL-IPを指定するもう一つの方法

> このセクションは初稿でLoadBalancerにExternal-IPを割り当てる唯一の方法として記述したものです。オンプレミスであれば直接IPを指定する前述の方法が楽だと思いますが、環境に応じて使い分けてください。

annotationsに項目を追加することはできるので、address-poolを指定する方法で通常とは違うip-rangeを指定することはできそうです。

address-poolから割り当てられると、再構築したタイミングで変更されてしまう可能性があります。
サービス用のIPアドレスは固定したいので、loadBalancerIPを指定する以外の方法を検討します。

あらかじめmetallbの設定で、割り当てたいIPを別のpoolとして定義しておきます。
下記の例では元々設定していたip-rangeの最後の1つ(192.168.1.139)を"rabbitmq-pool"として別に定義しました。

今回のk8sクラスターではmetallbはkubesprayのaddons.ymlから設定しているので、あらかじめkubesprayからaddress-poolを追加しています。

```bash:metallbe-systemの設定を確認
$ sudo kubectl -n metallb-system get cm -o yaml
apiVersion: v1
items:
- apiVersion: v1
  data:
    config: |
      address-pools:
      - name: loadbalanced
        protocol: layer2
        addresses:
        - 192.168.1.110-192.168.1.138
      - name: rabbitmq-pool
        protocol: layer2
        addresses:
        - 192.168.1.139-192.168.1.139
        auto-assign: False
  kind: ConfigMap
...
```

この"rabbitmq-pool"をannotationsで指定するよう01.definition.yamlファイルを編集し、適用します。

```bash:自動で割り当てられたIPを192.168.1.139に変更する様子
$ cat 01.definition.yaml 
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
  name: definition
spec:
  persistence:
    storageClassName: rook-ceph-block
    storage: 20Gi
  service:
    type: LoadBalancer
    annotations:
      metallb.universe.tf/address-pool: rabbitmq-pool

$ sudo kubectl -n rabbitmq-system apply -f 01.definition.yaml 
rabbitmqcluster.rabbitmq.com/definition configured
```

設定が反映されると、1つしかIPが定義されていないrabbitmq-poolからEXTERNAL-IPが割り当てられた事を確認します。

```bash:EXTERNAL-IPが切り替わった事を確認
$ sudo kubectl -n rabbitmq-system get svc
NAME               TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                          AGE
definition         LoadBalancer   10.233.18.226   192.168.1.139  15672:31925/TCP,5672:30675/TCP   14h
definition-nodes   ClusterIP      None            <none>         4369/TCP,25672/TCP               36h
```

直接loadBalancerIPが割り当てられると良いのですが、これでも同じような事ができたので、しばらく使ってみます。

## Web UIにログインするためのID,Passwordの確認

自動的にランダムな文字列が割り当てられているので、ログインに必要なID, Passwordをドキュメントに書かれているように確認します。

手元では次のようなMakefileのタスクを設定しています。

```makefile:管理者IDを確認するためのMakefileタスク
show-adminuser:
        (i=`sudo kubectl -n rabbitmq-system get secret definition-default-user -o jsonpath="{.data.username}" 
| base64 --decode | tee /dev/null` ; echo user: $$i)
        (i=`sudo kubectl -n rabbitmq-system get secret definition-default-user -o jsonpath="{.data.password}" 
| base64 --decode | tee /dev/null` ; echo pass: $$i)
```

## Replica数の変更

デフォルトではPodが1つのRabbitMQクラスターとなっているので、複数のPodを実行します。

```bash:replicaの指定を追加
$ cat 01.definition.yaml 
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
  name: definition
spec:
  replicas: 3
  persistence:
    storageClassName: rook-ceph-block
    storage: 20Gi
  service:
    type: LoadBalancer
    annotations:
      metallb.universe.tf/address-pool: rabbitmq-pool
```

これを適用し、コンソールからNodeが増加したことを確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78296/f124cffc-231b-1bbc-c711-75a0bebffa64.png)

# RabbitMQ Operatorのバージョンアップについて

この記事で導入したRabbitMQ Operatorのバージョンはv1.4.0でした。これを出来るだけ最新にしていきたいと思います。

基本的にRabbitMQ自体のバージョンアップポリシーは、n+1世代にバージョンアップする(v3.8.x→v3.9.x、次にv3.9.x→v3.10.x) というものですので、これよりは細かくOperatorのバージョンを上げていきたいと思います。


```yaml:v1.4.0→v1.6.0への更新時の手順
## RabbitMQClustersのインスタンス名を確認
$ sudo kubectl -n rabbitmq-system get rabbitmqclusters

## v1.6.0のOperator YAMLファイルを適用
$ sudo kubectl -n rabbitmq-system apply -f https://github.com/rabbitmq/cluster-operator/releases/download/v1.6.0/cluster-operator.yml

## Operatorのログを確認し、"Finished reconciling"のメッセージを確認する
$ sudo kubectl -n rabbitmq-system logs -l app.kubernetes.io/component=rabbitmq-operator


## 無事に導入が終ったらRabbitMQClustersのインスタンス(e.g. "rabbitmq" or definition)から.spec.imageを削除する
$ sudo kubectl -n rabbitmq-system edit rabbitmqclusters rabbitmq

## "image: rabbitmq:3.8.9-management" 行を削除し、編集結果を保存してからEditorを終了する
```

ここまで操作が終わると自動的にRabbitMQClusterの各ノードが再起動し、RabbitMQ Operatorがサポートする最新のバージョンに更新されます。

Operatorの更新によるPodが再起動された時点ではRabbitMQ自体のバージョンは変化していませんRabbitMQのコンテナのバージョンを新しくするためにはrabbitmqclusters/rabbitmqの.spec.image行を削除する必要があります。

今回は再起動を伴う更新は順番に適用する方針で、"v1.4.0" → "v1.6.0" → "v1.8.3" → "v1.9.0" → "v1.11.1" → "v1.12.1" → "v1.14.0" → "v2.0.0" → "v2.1.0" → "v2.2.0" → "v2.3.0" → "v2.4.0" とOperatorを適用していき、デフォルトのバージョンが上がった場合は、その都度.spec.image行を削除しています。(v1.9.0とv1.11.1、v2.0.0、v2.1.0はRabbitMQのコンテナ・バージョンに変化がないためそのまま)

特に"v1.12.x"はv3.8.xからv3.9.xに切り替わるタイミングなので必ずimageは最新にする必要があります。RabbitMQのバージョンを順番に上げていけばErlangのバージョンは特に気にする必要はありません。

またYAMLファイルの適用を省略する場合でも、"v2.0.0"はRabbitMQv3.9.9以上を要求する点に注意が必要です。

"v2.4.0"はRabbitMQ v3.11.18以降を要求するため、適用前に必ず"v2.3.0"を適用する必要があります。

これらの挙動をみると全般的に"v2.0.0"以降はバージョンをスキップせずに順番に適用することが望ましいと思われます。

## RabbitMQ v3.11.x(Opeartor v2.2.0)アップグレード前の必須作業

RabbitMQ Operator v1.14.0を適用した後からv2.2.0を適用してPodを再起動するまでの間に、RabbitMQ v3.11.xで必須になったfeature flagsの状況について、Pod内部で ``rabbitmqctl list_feature_flags`` コマンドで確認する必要があります。

* quorum_queue
* implicit_default_bindings
* virtual_host_metadata
* maintenance_mode_status (ability to switch RabbitMQ to a maintenance mode)
* user_limits (ability to configure connection and channel limits for a user)

enabledに変更する場合には、``rabbitmqctl enable_feature_flag <name>`` を利用すること。

これを怠るとPodのRollingUpdateに失敗します。この場合は rabbitmqclusters の定義から images:行をそれまで使用していた ``image: rabbitmq:3.10.2-management`` などとfallbackしてから問題のPodを再起動すれば元のバージョンで正常に稼動します。

テスト系では問題なかったものの、本番系ではこの確認をしていなかったため、問題が発生しました。

## RabbitMQ v3.12.x(Operator v2.4.0)アップグレード前の必須作業

v3.11.xと同様にfeature_flagsについてですが、v3.12.xからは**全てのfeature_flags**を更新することが求められています。

```bash:Pod内部にexecで入ってからコマンドを実行
$ rabbitmqctl enable_feature_flag all
```

これをv3.11.18で実行してからRabbitMQ Operator v2.4.0を適用しRabbitMQのバージョンアップを実行します。
 
## RabbitMQ v3.13.x(Operator v2.8.0-v2.11.0)アップグレード前の必須作業 (特になし)

v3.12.xで既に対応しているためenable_feature_flagについては事前に必要な変更はありません。

ただし2024年後半に予定されているv4.xへのアップグレードを見据えて、v3.13.xについては様々な変更が取り込まれています。feature_flagsへの項目追加はそれなりにあります。

v4.xへのバージョンアップのため必ず公式サイトのドキュメントを一読しておきましょう。

* [https://www.rabbitmq.com/blog/2024/03/11/rabbitmq-3.13.0-announcement](https://www.rabbitmq.com/blog/2024/03/11/rabbitmq-3.13.0-announcement)

以下の作業では積極的に安定版のfeature flagsは有効にしています。

### Operator v2.9.0〜v2.11.0 (RabbitMQ v3.13.x)の適用

Operator v2.8.0を適用した時点で、RabbitMQ v3.13.1が動作していますが、いくつかfeature flagsが追加されているので、v2.9.0を適用する前に安定版を全て有効にしておきます。

この作業は必須ではありませんが、将来のRabbitMQ v4.0.xへの更新時には全ての安定版(stable)feature flagsを有効にする必要があるとアナウンスされています。

```bash:v2.9.0を適用する前に、execコマンドでPodからコマンドを実行する
## 現状の確認
$ sudo kubectl -n rabbitmq-system exec -it rabbitmq-server-0  -- rabbitmqctl list_feature_flags

## 有効化
$ sudo kubectl -n rabbitmq-system exec -it rabbitmq-server-0  -- rabbitmqctl enable_feature_flag all
```

:::note
``all``を指定すると実験版のfeature flagとされている``khepri_db``だけがdisableのままで、他をenableに変更します。
:::

次にURLに含まれるOperatorのバージョンを確認して、YAMLファイルを適用します。

```bash:新しいOperatorバージョンのYAMLファイルを適用する
sudo kubectl -n rabbitmq-system apply -f https://github.com/rabbitmq/cluster-operator/releases/download/v2.9.0/cluster-operator.yml
```

Operatorを更新するだけではバージョンは上がらないので、``rabbitmqclusters``リソースのオブジェクト(``rabbitmq``)のimage:行を削除して、PodのRabbitMQのバージョンをOperatorのデフォルトに更新します。

この作業をv2.9.0からv2.11.0まで繰り返します。

#### RabbitMQ v3.13.7 (Operator v2.10.0) 更新後のfeature flagsの変化

v3.13.7に更新すると安定板のfeature flagsの中に``message_containers_deaths_v2``が追加されています。

これに対するドキュメントは全然ないのですが、v3.13.7のコードをみると次のような記述がありました。

```erlang:rabbitmq-server/deps/rabbit/src/rabbit_core_ff.erlから抜粋
-rabbit_feature_flag(
   {message_containers_deaths_v2,
    #{desc          => "Bug fix for dead letter cycle detection",
      doc_url       => "https://github.com/rabbitmq/rabbitmq-server/issues/11159",
      stability     => stable,
      depends_on    => [message_containers]
     }}).
```
これを有効にしても特に問題はなさそうなので、v2.10.0を適用してから再び``enable_feature_flag all``を実行しています。

#### Operator v2.11.0の適用

RabbitMQのバージョンは3.13.7のままなので、Operatorを更新した後のeditコマンドによるimage:行の削除は行っていません。

# さいごに

Operatorによる制御はかなり自然で、今後はHelmからRabbitMQをデプロイすることはないと思います。

MetalLBではloadBalancerIPによる制御を許可すると、(管理下の)任意のIPアドレスがアサインできてしまうので管理面からはあらかじめ準備しているaddress-poolからEXTERNAL-IPを割り当てる方が実際の場面では便利なのかなと感じています。

更新した記事では、spec.override.service によるloadBalancerIPを使用する方法も確認しました。両方確認しましたが、address-poolを利用する方法もそれなりに便利だと利用してみて思っているところなので、環境に応じて適切なものを選択していこうと思います。

RabbitMQをk8sで使い初めた当初はリスタートなどがあるとクラスターの起動に失敗していた事例がありJOBの滞留などをチェックする必要がありましたが、v2.12.xを稼動している現在ではそのようなトラブルもなく安定して稼動しています。クライアントのプログラミングに対するTipsも蓄積されてきたことも影響していると思いますが、MQが使えると便利です。

以上