【MongoDB】MongoDBのDockerコンテナ起動時に認証用ユーザーを作成する

MongoDBのDockerコンテナを使って開発を行う際に本番環境での使用も考えて、アプリケーションやターミナルから接続する時に認証するように設定しました。
コンテナを起動してからコマンドでの設定やShell Scriptを自作してやるのはスマートでないと思い色々調べていると、公式のイメージが初期データを挿入できるように作られていました。その方法と裏で何をしているのかを調べたので備忘録として書いておきます。

# 環境
- [Docker Desktop for Mac](https://hub.docker.com/editions/community/docker-ce-desktop-mac) ver 2.0.0.3
    - Docker Engine ver 18.09.2
- [MongoDB Docker Image](https://hub.docker.com/_/mongo) tag 4.1-bionic


# 認証用ユーザーの作成方法
## ユーザーを作成するJavaScript

```createUser.js
const user = {
  user: 'staff',
  pwd: 'staff_password',
  roles: [{
    role: 'readWrite',
    db: 'test'
  }]
};

db.createUser(user);
```

このスクリプトによって、ユーザー名`staff`、パスワード`staff_password`、`test`DBの`readWrite`権限が付与されたユーザーが作成されます。ちなみにロールは複数付与することができます。
MongoDBに接続には`mongo test -u staff -p staff_password`コマンドを実行します。

## 認証の有効化
MongoDBは

>MongoDB does not enable access control by default. You can enable authorization using the --auth or the security.authorization setting.

と書かれている通り、デフォルトでは認証・認可は有効化されていません。認証用のユーザーを作ったところで認証・認可を有効にしないと意味がありません。
有効にする方法は、引用文に書かれているように`--auth`コマンドオプションを使用するか、コンフィグの`security.authorization`を`enabled`にする２通りあります。

## Dockerfile
認証用ユーザーを起動時に作成するためには、createUser.jsを`/docker-entrypoint-initdb.d`下に配置する必要があります。
そして、認証・認可を有効化したDockerfileは以下のようになります。

```Dockerfile
FROM mongo:4.1-bionic
COPY createUser.js /docker-entrypoint-initdb.d/
EXPOSE 27017
CMD ["mongod", "--auth"]
```


# docker-entrypoint.shのカラクリ
MongoDBのDockerイメージではコンテナ起動時にdocker-entrypoint.shが実行されて初期データが挿入されます。そのShell Scriptが何をやっているのかを説明していこうと思います。

## rootユーザーの生成
環境変数に`MONGO_INITDB_ROOT_USERNAME`と`MONGO_INITDB_ROOT_PASSWORD`が設定されているとrootユーザーが作成されます。このrootユーザーはroleがrootとなっており、全てのリソースにアクセスできるようになっています。なので、rootユーザーが必要ない状況であれば作成しない方がいいです。

以下がrootユーザーを生成している該当箇所です。

```docker-entrypoint.sh
mongo=( mongo --host 127.0.0.1 --port 27017 --quiet )

if [ "$MONGO_INITDB_ROOT_USERNAME" ] && [ "$MONGO_INITDB_ROOT_PASSWORD" ]; then
	rootAuthDatabase='admin'
	"${mongo[@]}" "$rootAuthDatabase" <<-EOJS
	db.createUser({
		user: $(_js_escape "$MONGO_INITDB_ROOT_USERNAME"),
		pwd: $(_js_escape "$MONGO_INITDB_ROOT_PASSWORD"),
		roles: [ { role: 'root', db: $(_js_escape "$rootAuthDatabase") } ]
	})
	EOJS
fi
```

|変数|値|説明|
|:--:|:--|:--|
|$mongo|mongo --host 127.0.0.1 --port 27017 --quiet|localhostの27017でリッスンしているMongoDBに接続するためのコマンド|
|$rootAuthDatabase|admin|adminというデータベース|

スクリプトでは、adminデータベースにユーザー名が`MONGO_INITDB_ROOT_USERNAME`、パスワードが`MONGO_INITDB_ROOT_PASSWORD`、ロールが`root`であるユーザーを作成しています。

## 初期データの挿入
初期データを挿入するためには、それ用のShell ScriptかJavaScriptを書く必要があります。この２つ以外のスクリプトであると無視されます。
挿入用スクリプトが`/docker-entrypoint-initdb.d`に配置されていれば、スクリプトが実行されます。もちろん他のディレクトリに配置したら実行されません。←当たり前ですねw

前置きはこの辺にしてdocker-entrypoint.shの中身を見ていきましょう。
注) 行番号は説明のため１から連番にしてありますが、本物のソースコードの行番号と違います。

```docker-entrypoint.sh
1  export MONGO_INITDB_DATABASE="${MONGO_INITDB_DATABASE:-test}"
2
3  for f in /docker-entrypoint-initdb.d/*; do
4	case "$f" in
5		*.sh) echo "$0: running $f"; . "$f" ;;
6		*.js) echo "$0: running $f"; "${mongo[@]}" "$MONGO_INITDB_DATABASE" "$f"; echo ;;
8		*)    echo "$0: ignoring $f" ;;
9	esac
10	echo
11  done
```

### １行目
操作対象となるDBを環境変数`MONGO_INITDB_DATABASE`に格納してエクスポートしています。デフォルトでは`test`というDBが対象となっています。

### 3行目
`/docker-entrypoint-initdb.d`下にある全てのスクリプトを実行できるようにfor文で回しています。なので、実行内容ごとにスクリプトを分けて複数のスクリプトを`/docker-entrypoint-initdb.d`に配置することも可能です。１つのスクリプトにごちゃごちゃいろんなことを書かずに済みますね。

### 5行目
Shell Scriptを実行しています。`.`はスクリプトを読み込んで実行するという意味です。`source`コマンドと同様の役割を果たします。

### 6行目
JavaScriptを実行します。mongoコマンドには

>You can specify a .js file to the mongo shell, and mongo will execute the JavaScript directly. 

とあるようにJavaScriptを直接実行できる機能があります。わざわざ`node`コマンドとかで実行している訳ではありません。
`${mongo[@]}`はrootユーザーの作成のところでも出てきましたが、`mongo --host 127.0.0.1 --port 27017 --quiet`を指しています。


# 最後に
これで、初期データを挿入する方法とそのからくりがわかったと思います。
開発の場面では接続用のユーザーを作成したり、テストデータを挿入する時などに役に立つと思います。初期データを挿入できるのはMongoDBだけでなく、MySQLやPostgreSQLなどでも可能です。
便利な機能を使って環境構築は楽に素早く行って、メインの開発に注力していきましょう！
それでは:wave:

# 参考資料
- [mongo - Docker Hub](https://hub.docker.com/_/mongo)
- [Built-In Roles — MongoDB Manual](https://docs.mongodb.com/manual/reference/built-in-roles/)
- [Write Scripts for the mongo Shell — MongoDB Manual](https://docs.mongodb.com/manual/tutorial/write-scripts-for-the-mongo-shell/#execute-a-javascript-file)
- [mongod — MongoDB Manual](https://docs.mongodb.com/manual/reference/program/mongod/#cmdoption-mongod-auth)
- [mongo/docker-entrypoint.sh](https://github.com/docker-library/mongo/blob/3f117ac35a947ddff307aee85ecd5abe9e3a0fe1/4.1/docker-entrypoint.sh)
- [mongo/Dockerfile](https://github.com/docker-library/mongo/blob/3f117ac35a947ddff307aee85ecd5abe9e3a0fe1/4.1/Dockerfile)