Dockerを活用したMongoDBレプリカセットのローカル起動ガイド

## やりたいこと
Dockerコンテナ(docker-compose)を使ってローカルで起動しているMongoDB+自分用Webアプリで、FirebaseのRealtime Databaseのようにデータ更新を同期的に画面に反映してくれる **[Change Streams](https://www.mongodb.com/docs/manual/changeStreams/)** を使いたい。

## 問題
MongoDBのChange Streamsを使うには、MongoDBをレプリケーションした状態で起動させる必要がある。しかし、docker-compseを利用したレプリケーションの設定方法については、Web検索でも一筋縄ではいかない情報が多かった。
以下にこの問題について議論されている。（2023/7時点でも未解決？）
https://github.com/docker-library/mongo/issues/339

## 解決策とその手順

### ディレクトリ構造
まず、以下のディレクトリ構造を作成する。docker-compose.ymlファイルとMongoDBの初期化スクリプトを配置する。

```
.
├── app(Webアプリ）
├── docker
│   └── db
│       └── init
│           └── init.js
├── docker-compose.yml
└── README.md
```

### docker-compose設定
次にdocker-compose.ymlファイルを作成する。このファイルではMongoDBのレプリケーション設定や、ヘルスチェック、ボリュームの設定を行う。

```yaml:docker-compose.yml
version: '3'
services:
  #web:
  #略
  mongo:
    image: mongo:6.0.6
    environment:
      - AUTH=no
    command: [--replSet, my-replica-set, --noauth, --bind_ip_all]
    ports:
      - 27017:27017
    healthcheck:
      test: mongosh mongo-init.js
      interval: 10s
      start_period: 30s
    volumes:
      - mongodb_data:/data/db
      - ./docker/db/init/db_init.js:/mongo-init.js
    restart: always
  mongo-express:
    image: mongo-express
    container_name: mongo_express
    restart: always
    ports:
      - 8081:8081
    environment:
      #ME_CONFIG_MONGODB_URL: mongodb://@mongo:27017/
      ME_CONFIG_MONGODB_ADMINUSERNAME: root
      ME_CONFIG_MONGODB_ADMINPASSWORD: password
      ME_CONFIG_MONGODB_SERVER: mongo
      ME_CONFIG_MONGODB_PORT: 27017
    depends_on:
      - mongo
# mongodbのデータはdocker volumeで管理されるので、消すときは docker volume rm mongodb_data
volumes:
  mongodb_data:
```

### MongoDB初期化スクリプト
docker-compose.ymlで指定したパスに、MongoDBの初期化スクリプトを作成する。このスクリプトでは、レプリケーションの設定と管理ユーザーの作成を行う。

```bash:docker/db/init/db_init.js
init = false;
print("Init script ...")

try {
  if (!db.isMaster().ismaster) {
    print("Error: primary not ready, initialize ...")
    rs.initiate(
      {
        _id:'my-replica-set',
        members: [
          { _id:0,
            host: "mongo:27017"
          }
        ]
      }
    )
    quit(1);
  } else {
    if (!init) {
      admin = db.getSiblingDB("admin");
      admin.createUser(
        {
          user: "root",
          pwd: "password",
          roles: ["readWriteAnyDatabase"]
        }
      );
      init = true;
    }
  }
} catch(e) {
  rs.status().ok
}
```

これらの設定により、docker-composeを使ってMongoDBをレプリカセットで起動し、Change Streamsを使うことができるはずだ。

## 各コンテナの詳細
上記の設定で使用しているコンテナの詳細とその役割について説明する。

### Web
自分のWebアプリを記述する部分。今回の記事の内容からは外れる内容なので省略している。

### MongoDB

使用するMongoDBのバージョンは6.0.6で、レプリケーションを利用するために `--replSet` オプションをつけて起動する。ローカルでの開発用なので最小構成としてプライマリーノードを1つだけ起動する。

`--bind_ip_all` はローカルでの開発用として外部からの接続を許可するためにつけている。

初期設定スクリプト（init.js）では管理者ユーザーの作成を行う。管理者ユーザーはGUIでMongoDBの操作を行えるようにmongo-expressから参照できるように作成する。
起動に使うスクリプトは `volumes` にて `- ./docker/db/init/db_init.js:/mongo-init.js` として指定している。

`volumes` をdocker volumeにしている理由は、パーミッションの問題でMongoDBを起動できなかったため。

`healthcheck `で起動している `db-init.js` こそ今回の肝。
上述したとおりレプリカセットでMongoDBのコンテナイメージを実行すると、docker-entrypoint-initdb.dがうまく動作しない問題（仕様）のせいでユーザーの作成やレプリケーションの初期設定をdocker-entrypoint-initdb.dでは実装できない。
そこでハック的に `healthcheck` で無理やりこれらの作業を実施しているというわけである。

また、初回のコンテナ起動時はレプリケーションの初期化が完了していないためエラーで落ちるので、`restart: always` が必要。

### mongo-express

mongo-expressはMongoDBのGUIクライアントで、MongoDBの操作をGUIで行いたい場合に利用する。環境変数を用いてMongoDBへの接続設定を行う。
これのおかげでデータの操作（追加・削除・変更）がGUIからできるのでとても便利。

## 参考資料
* https://stackoverflow.com/questions/76013265/how-to-do-a-mongodb-6-single-node-replicaset-with-docker-compose
* https://n-laboratory.jp/articles/mongodb-replicaset-docker
* https://github.com/docker-library/mongo/issues/339

## まとめ

ローカルでMongoDBを使ってレプリケーションを試すくらいであればシングルノードでも問題ないはず。公式のどきゅめんとにもあるように本番では3台以上構成で起動することを推奨する。
関連記事: https://gihyo.jp/dev/serial/01/mongodb/0004

なお、実はここまで苦労せずともローカルから [MongoDB Atlas Database](https://www.mongodb.com/ja-jp/atlas/database) に繋いでしまえばすぐにでもレプリケーションされたMongoDBを利用することができる。
しかし、開発に使う場合はローカル環境を使いたいんやで……という場合もあると思うので、参考にしてもらえると幸いである。