【Docker】MariaDBで初期化SQLを実行しようとして詰まった話

この投稿は [グレンジ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/grenge) の 7日目の記事です。

こんにちは、[株式会社グレンジ](https://www.grenge.co.jp/)でサーバーサイドエンジニアをしています。
新卒の島田（[@konnyaku256](https://twitter.com/konnyaku256)）です。

最近、Docker版のMariaDBを使う機会があり、その際に初期化SQLを実行しようとして詰まりました。
この記事では、その話についてまとめたいと思います。

# MariaDBの初期化について
> Initializing a fresh instance
> 
> When a container is started for the first time, a new database with the specified name will be created and initialized with the provided configuration variables. Furthermore, it will execute files with extensions .sh, .sql, .sql.gz, .sql.xz and .sql.zst that are found in /docker-entrypoint-initdb.d. Files will be executed in alphabetical order. .sh files without file execute permission are sourced rather than executed. You can easily populate your mariadb services by mounting a SQL dump into that directory and provide custom images with contributed data. SQL files will be imported by default to the database specified by the MARIADB_DATABASE / MYSQL_DATABASE variable.

引用：https://hub.docker.com/_/mariadb

Docker HubのDescriptionによると、Docker版のMariaDBでは、/docker-entrypoint-initdb.d に .sql,.sh, .sql.gzファイルを配置しておくと、コンテナの起動時にそれらのファイルを読み込んで実行してくれる、という説明が書かれています。

この仕組みを使えば、例えば、DBやユーザの作成といった初期化処理を実行することができそうです。

しかし...

# 初期化SQLが実行されない

次の構成でイメージを作成し、コンテナを起動します。

ディレクトリ構成

```sh
$ tree
.
├── Dockerfile
└── init
    └── init.sql
```

初期化SQL（最終行は改行です）

```sql:init.sql
CREATE DATABASE `test`;

```

Dockerfile

```Dockerfile:Dockerfile
FROM mariadb:5.5.40

COPY init/* /docker-entrypoint-initdb.d/

CMD ["mysqld"]
```

一見すると、良さそうに見えます。

イメージをビルドして、コンテナを起動してみます。

```sh
$ docker build -t mariadb-example ./ && docker run -d -e MYSQL_ROOT_PASSWORD=password --name mariadb-example mariadb-example
```

コンテナの状態を確認します。

```
$ docker ps -a --no-trunc -f name=mariadb-example

CONTAINER ID                                                       IMAGE             COMMAND                          CREATED         STATUS         PORTS      NAMES
6395ad036dfa74597853a2cf1e62d73f30b7446547d2a262a887dc1614c47b2c   mariadb-example   "/docker-entrypoint.sh mysqld"   6 minutes ago   Up 6 minutes   3306/tcp   mariadb-example
```

ステータスを見る限り、正常に起動できていそうです。

コンテナにログインして詳細を確認します。

```sh
$ docker exec -it mariadb-example /bin/bash
```

MariaDBにログインします。

```sh
root@0b4910572e26:/# mysql -ppassword
```

データベースの一覧を取得して、testというDBが作成されているか確認します。

```sql
MariaDB [(none)]> show databases;

+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)
```

あれ！？ 初期化SQLに記述したDBがありません！

# 原因はdocker-entrypoint.sh

`docker ps`の結果から、今回作成したMariaDBコンテナでは起動時に docker-entrypoint.sh が実行されていることがわかります。

```
COMMAND
"/docker-entrypoint.sh mysqld"
```

つまり、docker-entrypoint.sh を見れば、なぜ初期化SQLが実行されないかわかりそうです。

シェルスクリプトの中身を確認します。

```sh
root@0b4910572e26:/# cat docker-entrypoint.sh
```

```sh:docker-entrypoint.sh
#!/bin/bash
set -e

if [ ! -d '/var/lib/mysql/mysql' -a "${1%_safe}" = 'mysqld' ]; then
	if [ -z "$MYSQL_ROOT_PASSWORD" ]; then
		echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set'
		echo >&2 '  Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?'
		exit 1
	fi

	mysql_install_db --user=mysql --datadir=/var/lib/mysql

	# These statements _must_ be on individual lines, and _must_ end with
	# semicolons (no line breaks or comments are permitted).
	# TODO proper SQL escaping on ALL the things D:
	TEMP_FILE='/tmp/mysql-first-time.sql'
	cat > "$TEMP_FILE" <<-EOSQL
		DELETE FROM mysql.user ;
		CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
		GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
		DROP DATABASE IF EXISTS test ;
	EOSQL

	if [ "$MYSQL_DATABASE" ]; then
		echo "CREATE DATABASE IF NOT EXISTS $MYSQL_DATABASE ;" >> "$TEMP_FILE"
	fi

	if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
		echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" >> "$TEMP_FILE"

		if [ "$MYSQL_DATABASE" ]; then
			echo "GRANT ALL ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%' ;" >> "$TEMP_FILE"
		fi
	fi

	echo 'FLUSH PRIVILEGES ;' >> "$TEMP_FILE"

	set -- "$@" --init-file="$TEMP_FILE"
fi

chown -R mysql:mysql /var/lib/mysql
exec "$@"
```

今回配置した初期化SQLが実行されない原因がわかりました。
/docker-entrypoint-initdb.d に関する処理が何もありません。
公式ドキュメントの嘘つき！と言いたいところですが、おそらくこのバージョンが公開された時点では /docker-entrypoint-initdb.d を使った初期化機能は実装されていなかったのでしょう。

# docker-entrypoint.shを置き換える

/docker-entrypoint-initdb.d が使えないとわかったので、代替案を考えます。
今回は、手っ取り早く docker-entrypoint.sh を置き換えることにします。

ディレクトリ構成

```sh
$ tree
.
├── Dockerfile
├── docker-entrypoint.sh
└── init
    └── init.sql
```

次のようなdocker-entrypoint.shを追加します。

```sh:docker-entrypoint.sh
#!/bin/bash
set -e

if [ ! -d '/var/lib/mysql/mysql' -a "${1%_safe}" = 'mysqld' ]; then
	if [ -z "$MYSQL_ROOT_PASSWORD" ]; then
		echo >&2 'error: database is uninitialized and MYSQL_ROOT_PASSWORD not set'
		echo >&2 '  Did you forget to add -e MYSQL_ROOT_PASSWORD=... ?'
		exit 1
	fi

	mysql_install_db --user=mysql --datadir=/var/lib/mysql

	# These statements _must_ be on individual lines, and _must_ end with
	# semicolons (no line breaks or comments are permitted).
	# TODO proper SQL escaping on ALL the things D:
	TEMP_FILE='/tmp/mysql-first-time.sql'
	cat > "$TEMP_FILE" <<-EOSQL
		DELETE FROM mysql.user ;
		CREATE USER 'root'@'%' IDENTIFIED BY '${MYSQL_ROOT_PASSWORD}' ;
		GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION ;
		DROP DATABASE IF EXISTS test ;
	EOSQL

	if [ "$MYSQL_DATABASE" ]; then
		echo "CREATE DATABASE IF NOT EXISTS $MYSQL_DATABASE ;" >> "$TEMP_FILE"
	fi

	if [ "$MYSQL_USER" -a "$MYSQL_PASSWORD" ]; then
		echo "CREATE USER '$MYSQL_USER'@'%' IDENTIFIED BY '$MYSQL_PASSWORD' ;" >> "$TEMP_FILE"

		if [ "$MYSQL_DATABASE" ]; then
			echo "GRANT ALL ON $MYSQL_DATABASE.* TO '$MYSQL_USER'@'%' ;" >> "$TEMP_FILE"
		fi
	fi

	# 初期化SQLを実行
	cat ./initdb/init.sql >> "$TEMP_FILE"
	
	echo 'FLUSH PRIVILEGES ;' >> "$TEMP_FILE"

	set -- "$@" --init-file="$TEMP_FILE"
fi

chown -R mysql:mysql /var/lib/mysql
exec "$@"
```

置き換え前との差分は次の行です。
既存のシェルスクリプトではDBの初期化に変数`$TEMP_FILE`を使っているようだったので、初期化SQLの中身を`$TEMP_FILE`の末尾に追加しています。

```sh
# 初期化SQLを実行
cat ./initdb/init.sql >> "$TEMP_FILE"
```

ファイルの権限は755に変更しておきます。

```sh
$ chmod 755 docker-entrypoint.sh
```

Dockerfileを次のように修正します。

```Dockerfile:Dockerfile
FROM mariadb:5.5.40

COPY init/* /initdb/
COPY docker-entrypoint.sh /

CMD ["mysqld"]
```

イメージのビルドからMariaDBにログインまで再度実行します。

データベースの一覧を取得して、testというDBが作成されているか確認します。

```sql
MariaDB [(none)]> show databases;

+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| test               |
+--------------------+
4 rows in set (0.00 sec)
```

今度はうまく作成されました！🙌

# まとめ

- Docker版 MariaDB 5.5.40（かなり古い）では /docker-entrypoint-initdb.d ディレクトリにsqlファイルを配置しても初期化を実行できなかった
- 代わりに docker-entrypoint.sh を置き換えて、初期化SQLが実行されるようにした

構成が似ているので、MySQLでも古いバージョンでは類似の問題に遭遇するかもしれないですね。
なかなか踏む機会はないと思いますが、どなたかの参考になれば幸いです。

明日は、[gamu](https://qiita.com/gamu)さんの記事が投稿される予定です。 お楽しみに！

# 参考
- https://hub.docker.com/_/mariadb
- https://qiita.com/NagaokaKenichi/items/ae037963b33a85df33f5