bitnami/rabbitmq で load_definition したら user/password 設定にハマった話

k8s で RabbitMQ を構築したいみなさんこんにちは。
LAPRAS Advent Calendar 2020 の2日目の記事です。

重要なことなので最初と最後で二度言いますが、**今からRabbitMQを運用したい方はAWSのマネージドサービスを使うことをおすすめします**。
2020年11に発表されました。https://aws.amazon.com/jp/about-aws/whats-new/2020/11/announcing-amazon-mq-rabbitmq/

k8s で helm を使って RabbitMQ を動かすときに、ユーザとパスワード設定で2箇所ハマったので、マネージドサービスを使うのが嫌いな人のためにそれについて書いていきます。
これ以外にもいろいろ苦労してたくさん調べものをしたのですが、ほとんど日本語の情報がなくて、皆あまり k8s で RabbitMQ の管理してないのかな…と思ったり思わなかったりしています。

まず注意しないといけないのが [helm/charts](https://github.com/helm/charts) の [rabbitmq](https://github.com/helm/charts/tree/master/stable/rabbitmq) が名前からすると一番支持されているように見えますが、こちらは Helm ver 2 から 3 へのバージョンアップに伴い deplicated になっていて、[bitnami/charts](https://github.com/bitnami/charts/tree/master/bitnami/rabbitmq) がメインストリームになっています。(2020年12月現在)
YAMLの書き方もそれぞれで違います。以降 bitnami/rabbitmq の helm chart を使う前提で記述しています。

## Load Definitions せずに初期ユーザ/パスワードを設定する

[Load Definitions](https://github.com/bitnami/charts/tree/master/bitnami/rabbitmq#load-definitions) といって、JSON形式で吐き出した RabbitMQ の設定ファイル(definitions)を起動時に読み込む機能があるのですが、それを使わないのであれば [README](https://github.com/bitnami/charts/tree/master/bitnami/rabbitmq#rabbitmq-parameters) に書かれている通り `auth.username` と `auth.password` でデフォルトのユーザ名とパスワードを作成できます。

helmfile を使う場合こんな感じのYAMLになります。

```yaml
  - name: rabbitmq
    chart: bitnami/rabbitmq
    values:
      - auth:
          username: your_name
          password: raw_password
```

これは素直なので特にハマることはないです。

## ハマリポイント1: Load Definitions したら初期ユーザが作られない

起動時に vhost の作成や、その権限設定をしたいと思いこんな感じでパラメータを設定します。
`showwin` というユーザを作って、`/` や `/hello` の権限を付与したい訳です。

```yaml
  - name: rabbitmq
    chart: bitnami/rabbitmq
    values:
      - auth:
          username: showwin
          password: showwin_password
      - extraSecrets:
          load-definition:
            load_definition.json: |
              {
                "vhosts": [
                  {
                    "name": "/"
                  },
                  {
                    "name": "/hello"
                  }
                ],
                "permissions": [
                  {
                    "user": "showwin",
                    "vhost": "/",
                    "configure": ".*",
                    "read": ".*",
                    "write": ".*"
                  },
                  {
                    "user": "showwin",
                    "vhost": "/hello",
                    "configure": ".*",
                    "read": ".*",
                    "write": ".*"
                  }
                ]
              }
      - loadDefinition:
          enabled: true
          existingSecret: load-definition
      - extraConfiguration: |
          load_definitions = /app/load_definition.json
```

これで `$ helmfile sync` すると失敗して、showwin なんてユーザいないぜ！ってエラーがでます。
(エラーメッセージちゃんと残してなくてごめんなさい)

先に答えを書くと

```yaml
  - name: rabbitmq
    chart: bitnami/rabbitmq
    values:
      - extraSecrets:
          load-definition:
            load_definition.json: |
              {
                "users": [
                  {
                    "name": "showwin",
                    "password_hash": "hcT66xoYpmmZQqzjH934IWfEdSdBZYLhIwP0oULa7DyecDFI",
                    "hashing_algorithm": "rabbit_password_hashing_sha256",
                    "tags": ""
                  }
                ],
                "vhosts": [
                  {
                    "name": "/"
                  },
                  {
                    "name": "/hello"
                  }
                ],
                "permissions": [
                  {
                    "user": "showwin",
                    "vhost": "/",
                    "configure": ".*",
                    "read": ".*",
                    "write": ".*"
                  },
                  {
                    "user": "showwin",
                    "vhost": "/hello",
                    "configure": ".*",
                    "read": ".*",
                    "write": ".*"
                  }
                ]
              }
      - loadDefinition:
          enabled: true
          existingSecret: load-definition
      - extraConfiguration: |
          load_definitions = /app/load_definition.json
```

このように definitions.json の中にユーザの情報を書かないと作成されません。

その理由を探るためにコードを追いかけてみます。

`auth.username` や `auth.password` は [ここ](https://github.com/bitnami/charts/blob/e4fb3b01ca0e232594c98efea718ba7b56947b29/bitnami/rabbitmq/templates/secrets.yaml#L12) で secret config にされて、 [statefulset](https://github.com/bitnami/charts/blob/e4fb3b01ca0e232594c98efea718ba7b56947b29/bitnami/rabbitmq/templates/) で `RABBITMQ_USERNAME` と `RABBITMQ_PASSWORD ` という環境変数に設定されます。
(それらの環境変数が設定されていない場合は Docker の [entrypoint.sh](https://github.com/bitnami/bitnami-docker-rabbitmq/blob/53dc9918253c95807431c5d39f26fe7dbda1689d/3.8/debian-10/rootfs/opt/bitnami/scripts/rabbitmq/entrypoint.sh#L12) の中で呼ばれる [librabbitmq.sh](https://github.com/bitnami/bitnami-docker-rabbitmq/blob/2c17afa10a2c097885d5b140b07bcdc500304a1c/3.8/debian-10/rootfs/opt/bitnami/scripts/librabbitmq.sh#L62-L63) で `user`, `bitnami` というユーザ名、パスワードにデフォルトでなるようです。)
`RABBITMQ_USERNAME` は [ここ](https://github.com/bitnami/bitnami-docker-rabbitmq/blob/2c17afa10a2c097885d5b140b07bcdc500304a1c/3.8/debian-10/rootfs/opt/bitnami/scripts/librabbitmq.sh#L160) で config file に `default_user` として設定され、RabbitMQ server 起動時に [ここ](https://github.com/rabbitmq/rabbitmq-server/blob/2259039b173735761cd803e7e95a58f681ddc0e8/deps/rabbit/src/rabbit.erl#L1022) でそれが読まれてユーザが作成されるはずなのですが、[すぐ上のコード](https://github.com/rabbitmq/rabbitmq-server/blob/2259039b173735761cd803e7e95a58f681ddc0e8/deps/rabbit/src/rabbit.erl#L1010-L1019)を読むと、読み込む definitions がない場合にそのデフォルトユーザを作成して、 definitions がある場合にはデフォルトユーザ(やvhost)を作成しないような動きになっています。
2020年6月の [Import definitions in the post-launch phase](https://github.com/rabbitmq/rabbitmq-server/pull/2396) のPRで入った挙動のようですが、[公式のドキュメント](https://www.rabbitmq.com/definitions.html#import-on-boot)も更新されていて
>The definitions in the file will not overwrite anything already in the broker. However, if a blank (uninitialised) node imports a definition file, it will not create the default virtual host and user.

ときちんと書かれているので、ちゃんと読めという話でした。

## ハマリポイント2: password_hash が毎回変わる問題

先程の設定でこのような部分があります。

```yaml
- extraSecrets:
    load-definition:
      load_definition.json: |
        {
          "users": [
            {
              "name": "showwin",
              "password_hash": "hcT66xoYpmmZQqzjH934IWfEdSdBZYLhIwP0oULa7DyecDFI",
              "hashing_algorithm": "rabbit_password_hashing_sha256",
              "tags": ""
            }
          ],
```

ここの `password_hash` ですが、どうパスワードをハッシュ化すればよいのかわからなかったので、適当に立ち上げたRabbitMQコンテナの中に入って

```bash
$ rabbitmqctl change_password showwin raw_password
$ rabbitmqctl export_definitions definitions.json
$ cat definitions.json
```

をして、吐き出された definitions ファイルにかかれている `password_hash` をコピペして設定しました。
ところが、これで立ち上げると `showwin` / `raw_password` で認証が通りません。

おかしいなと思い、上の手順を再度実行すると、異なる password_hash が得られます。毎回saltが変わっているようですね。

どうも `load_definition.json` に `password_hash` で定義するのは悪手のようで、


```yaml

- extraSecrets:
    load-definition:
      load_definition.json: |
        {
          "users": [
            {
              "name": "showwin",
              "password": "raw_password",
              "hashing_algorithm": "rabbit_password_hashing_sha256",
              "tags": ""
            }
          ],
```

と `password` のキーを使うとそのパスワードが設定できるようでした。
これは [bitnami/rabbitmq の REAMDE](https://github.com/bitnami/charts/tree/e4fb3b01ca0e232594c98efea718ba7b56947b29/bitnami/rabbitmq#load-definitions) の例も `password` を使っていたので簡単に気づけたかもしれませんが、rabbitmqctl が吐き出す definitions はハッシュ化されたものだったのでそちらに気を取られてしまいました。
(生パスワードを定義する場合はきちんと secret 化しましょう)

以上、ぼくがユーザ、パスワード設定できない…と5,6時間格闘した結果でした。


## おまけ: RabbitMQ運用辛い話

高可用性な構成にしたく、replica数3にしてRabbitMQクラスタを組んでいたのですが、1つpodが死ぬと再起動時に `Error while waiting for Mnesia tables` というログを吐いて正常に立ち上がらなくなります。
https://github.com/bitnami/charts/issues/2868 など複数箇所で同様の報告が上がっており、それに対するPRもマージされているのですが自分の環境では未だに発生し続けています。
bitnami/rabbitmq だけではなくて、その前に使っていた stable/rabbitmq でも同じことが起きていました。

正しく理解できている自信はないですが、起動時にクラスタの他のpodの設定を読んで同期する処理があり、そのときにエラーになっているようです。

AWSのスポットインスタンスで構成されたnode群の上で動かしているので、割と頻繁にnodeが死ぬのですが、そこにRabbitMQのpodが乗っていると、再起動時に立ち上がらなくなり、3,4日に1回RabbitMQが死んでしまう状況でした。
(手動で全podを消して同時に立ち上げれば正常に起動します)

最終的に今は(起動時に同期処理が走らない)1podで動かしており、なんとクラスタを組むよりも高い可用性で運用ができている状況です…クラスタ化とは…

とはいえ使う場所によっては、1podな構成ではさすがに信頼性に欠けると思うので、高い可用性が必要であればAWSのマネージドサービスを使いましょう。
負けた感じもしますが…