ホストからDockerのMySQLコンテナに接続する時の権限エラーを解消する

# はじめに
Dockerでローカル環境に構築したMySQLコンテナへ、ホストマシン上のアプリから接続しようとした際に以下のような権限エラーになりました。MySQLへ接続時のホストは`localhost`を指定しています。

```
(1130, "Host '172.18.0.1' is not allowed to connect to this MySQL server")
```

コンテナ起動時にユーザ作成・権限の設定を行うことで上記エラーを解消することができたので、本記事はその備忘録です。

## 環境
* Windows11
* docker desktop（WSL使用）
* DockerHubのMySQL公式イメージ（mysql/mysql-server:8.0）を使用。（mysql 8.0.32）

https://hub.docker.com/_/mysql


# 結論（解消方法）
MySQLコンテナの`/docker-entrypoint-initdb.d`フォルダにDockerネットワークのゲートウェイ（上記エラーの環境では'172.18.0.1'）からの接続を許可する初期化クエリを追加します。  
MySQL公式のコンテナイメージでは、`/docker-entrypoint-initdb.d`配下に配置されたクエリがコンテナ初回起動時に実行されます。

以下、docker composeでコンテナ起動する際の例です。  

## フォルダ構成
``` tree
.
├ docker-compose.yml
└ mysql-init # 任意の名称
  └ init.sql  # 任意の名称
```

## docker-compose.yml
`volumes:`にてmysql-initフォルダをMySQLコンテナの`/docker-entrypoint-initdb.d`へバインドマウントしています。

``` yaml:docker-compose.yml
services:
  mysql:
    image: mysql/mysql-server:8.0
    container_name: mysql-container
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: test_db
    volumes:
      - ./mysql-init:/docker-entrypoint-initdb.d # bindマウントでコンテナへ初期化クエリを配置
      - mysql:/var/lib/mysql

volumes:
  mysql:
```

## 初期化クエリ
コンテナの初回起動時に以下クエリが実行されます。

以下は全ホストを対象としたユーザー`'root'@'%'`を作成して、全権限を付与しています。  
必要に応じて、ホスト、権限を絞ってください。

``` SQL:init.sql
CREATE USER 'root'@'%' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION;
FLUSH PRIVILEGES;
```

## コンテナ起動
```sh
docker compose up -d
```

## ホストマシン上のアプリから接続
以下pythonの例。

``` python
import pymysql

conn = pymysql.connect(
    host="localhost",
    port=3306,
    user="root",
    password="password",
    database="test_db",
)
```

## コンテナ停止
ボリュームをマウントして既に起動している場合は、ボリュームを削除して再度コンテナを立ち上げないと、初期化クエリが実行されないので注意ください。

```sh
docker compose down -v # ボリュームごと削除
```

# エラーの原因
dockerでコンテナを構築した場合、dockerネットワーク上にコンテナが作成され、**dockerネットワークのゲートウェイを経由して**コンテナへアクセスされます。  
この**dockerネットワークのゲートウェイをホストとしたMySQLのユーザーが作成されていない**ことが原因です。

## dockerネットワーク
エラー文からもわかるように、アプリからlocalhost（127.0.0.1）に対して接続しているにもかかわらず、ホストが'172.18.0.1' になっています。

これは、dockerでコンテナを構築した場合、dockerネットワーク上にコンテナが作成され、**dockerネットワークのゲートウェイを経由してコンテナへアクセスされる**ためです。  
ゲートウェイのIPは、上記エラー発生時の環境では'172.18.0.1'に指定されていました。

dockerネットワークに関しては、以下記事がわかりやすいです。

https://qiita.com/Toyo_m/items/52fa81948d5746dd2afc

### ゲートウェイのIPの確認方法
`docker inspect {コンテナID}`を実行します。コンテナIDは`docker ps`コマンドで確認できます。  
長いので必要な個所のみ抽出していますが、`"Gateway": "172.18.0.1"`となっているのがわかります。

``` json:docker inspect {コンテナID}
[
    {
        ...
        "NetworkSettings": {
            ...
            "Networks": {
                "{dockerネットワーク名}": {
                    ...
                    "Gateway": "172.18.0.1",
                    "IPAddress": "172.18.0.2",
                    ...
                }
            }
        }
    }
]
```
dockerネットワーク名の箇所は、ネットワークを指定せず起動した場合、docker composeでは`{フォルダ名}_default`のようなネットワーク名で自動作成されます。docker runで起動した場合は、デフォルトで用意されている`bridge`ネットワークが使用されます。

`docker network ls`でネットワークの一覧確認、`docker network inspect {ネットワークID}`でネットワークの詳細確認も可能です。

## MySQLのユーザー、権限
デフォルトでは、以下のようにホストを`localhost`とするユーザーのみ存在しています。  
そのためdockerネットワークのゲートウェイのIPからの接続は許可されていません。

``` sh
# docker exec でコンテナに乗り込み
PS > docker exec -it {コンテナID} sh

# mysqlへ接続
sh-4.4# mysql -u root -p
Enter password:

# ユーザーリスト表示
mysql> SELECT host, user FROM mysql.user;
+-----------+------------------+
| host      | user             |
+-----------+------------------+
| localhost | healthchecker    |
| localhost | mysql.infoschema |
| localhost | mysql.session    |
| localhost | mysql.sys        |
| localhost | root             |
+-----------+------------------+
```

一方で、上記初期化クエリを設定した場合は、以下のように全てのホスト（`%`）に対するユーザー`root`が作成されています。

``` sh
mysql> SELECT host, user FROM mysql.user;
+-----------+------------------+
| host      | user             |
+-----------+------------------+
| %         | root             |
| localhost | healthchecker    |
| localhost | mysql.infoschema |
| localhost | mysql.session    |
| localhost | mysql.sys        |
| localhost | root             |
+-----------+------------------+
```

ユーザーに設定されている権限は、`show grants for 'ユーザー名'@'ホスト名'`で確認できます。

# 参考記事

https://qiita.com/Toyo_m/items/52fa81948d5746dd2afc

https://qiita.com/moaikids/items/f7c0db2c98425094ef10

https://github.com/docker-library/mysql/issues/275

https://qiita.com/sugimount-a/items/a13ae09627309a05728e

https://zenn.dev/ryo7/articles/create-mysql-on-docker

https://www.digitalocean.com/community/tutorials/how-to-create-a-new-user-and-grant-permissions-in-mysql-ja