この記事は、Docker Advent Calendar 2019の19日目の記事です。
TL;DR
- Dockerを開発環境で使う際に、replica databaseを使うようなアプリケーションにて、書き込みバグを検知する
- MySQLのデータベースにreadOnlyユーザーを作成してアプリケーションから利用する
- レプリカラグの考慮などには対応できないが、最低限書き込みバグを防ぐことができる
ReadOnlyユーザーを作る
Webアプリケーション開発において、PostgreSQLやMySQLといったRDBMSをシステムのデータベースとして使うケースは多いです。そのような構成において、負荷対策で参照クエリを逃がすと行った用途で**Replicaインスタンス(読み込み専用)**を用意することは多いでしょう。
このシステム構成で、Docker開発環境を作る場合、データベースサービスをローカルのDockerコンテナとして立てる際、間違えてReplicaデータベースに書き込むバグ作っちゃったとはならないようにしたいですよね。この目的感のもと採用できる打ち手の一つが、ReadOnlyユーザーを作成する方法です。
この方法は、すぐに実践できる手軽さもあり、それなりの数の実践事例を観測しています。たとえば、CakeFest 2019というCakePHPの国際カンファレンスでの「Working with Database Replications in CakePHP」という発表の中でも、レプリカデータベースに対して書き込んでしまうようなアプリケーションバグを検知するために、ReadOnlyユーザーを作成するという話をしていました。
実際に、筆者の現場でも同様の方法を採用しています。この方法はレプリケーションラグの考慮などを踏まえると完全なシミュレーション方法ではありませんが、初手としては良い方法と考えています。
実現例
今回実現する例は、次のような構成です。
- MySQLを利用、MySQLのimageは公式のものを利用します
- 複数コンテナサービスが必要なため docker-compose を用います
例題として取り上げるものは下記のGitHub repositoryに公開しています。
├── docker-compose.yml
├── docker
│ └── db
│ ├── conf.d
│ │ └── custom_my.cnf
│ └── docker-entrypoint-initdb.d
│ ├── 1_initialize_tables.sql
│ └── 2_readuser.sql
└── src
└── Dockerfile
docker-compose.yml
早速、docker-compose.ymlを見てみましょう。Compose file formatはversion 3を用いています。
refs: https://docs.docker.com/compose/compose-file/
version: '3'
services:
health-api:
build:
context: "./src/"
ports:
- "8080:8080"
depends_on:
- db
restart: on-failure
env_file:
- ".env"
db:
image: mysql:5.6
ports:
- "3306:3306"
expose:
- 3306
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: healthy
MYSQL_USER: wruser
MYSQL_PASSWORD: password
volumes:
- ./docker/db/data:/var/lib/mysql:cached
- ./docker/db/conf.d:/etc/mysql/conf.d:cached
- ./docker/db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:cached
話を単純にするために2つのサービスのみを取り上げます。Web APIであるheath-api
とMySQLデータベースであるdb
を定義しています。
mysql公式イメージの使い方のおさらい
環境変数
mysql
の公式イメージの使い方をざっとおさらいすると、MYSQL_ROOT_PASSWORD
など特定の環境変数を指定することで作成されるrootユーザーのパスワードなどを指定することが出来ます。
https://hub.docker.com/_/mysql のEnvironment Variables
を見ることで環境変数で設定できる項目を確認することが出来ます。
データの永続化
コンテナが停止したときにデータが消えてしまうと開発環境としては少し物足りませんね。データをコンテナ停止しても永続化された状態にするために、次の記述でボリュームマウントします。
volumes:
- ./docker/db/data:/var/lib/mysql:cached
これは、 https://hub.docker.com/_/mysql のCaveats > Where to Store Data
に説明されています。Git管理下におくとMySQLのデータがそのままコミットされてしまうので、対象ディレクトリを.gitignore
に追加しておくのを忘れないようにしましょう。
# docker database local mount files
docker/db/data
Custom MySQL Configuration
デフォルトの設定ではなく、ユーザー自身でMySQLの設定をカスタムしたいという場合は、コンテナ内の/etc/mysql/conf.d
に設定をマウントします。これは、 https://hub.docker.com/_/mysql の Using a custom MySQL configuration file
に説明されています。
volumes:
- ./docker/db/conf.d:/etc/mysql/conf.d:cached
例えば、このケースではつぎのようなmy.cnf
を設定します。
[mysqld]
explicit_defaults_for_timestamp=1
symbolic-links=0
sql_mode=TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY
character-set-server=utf8
[client]
default-character-set=utf8
ReadOnlyユーザーを設定する
ReadOnlyユーザーを設定します。docker-compose.yml
では次の設定項目が該当します。
volumes:
- ./docker/db/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:cached
まずは、docker-entrypoint-initdb.d
についてですが、コンテナ内の/docker-entrypoint-initdb.d
に.sh
・.sql
・.sql.gz
拡張子のファイルをおくと、初期化の実行してくれます。
そのため、この初期化時にReadOnlyユーザーを作るSQLを実行すれば、ReadOnlyユーザーを作成できます。
GRANT SELECT, PROCESS ON *.* TO 'reader'@'%' IDENTIFIED BY 'password';
ファイル名ですが、
Files will be executed in alphabetical order.
という記述が、https://hub.docker.com/_/mysql の Initializing a fresh instance
にあることが理由です。アルファベット順にソートされ実行されるので、明示的に順番になるように数字を先頭にしています。
実際にReadOnlyユーザーが作れているか
実際に、rootアカウントでデータベースにアクセスし、ユーザーが作られているか確認します。mysql.user
テーブルに対して検索すると次のような結果が得られます。
SELECT
User
FROM
mysql.user;
reader
root
wruser
root
作成したReadOnlyユーザーであるreader
を確認することが出来ます。このユーザーには、GRANT
で権限を付けているので、SELECT
とPROCESS
が可能なユーザーになっています。
SELECT
User,
Select_priv,
Insert_priv,
Update_priv,
Delete_priv,
Process_priv
FROM
mysql.user
WHERE
User = 'reader';
reader Y N N N Y
これで、このデータベースを使うコンテナサービスが、レプリカ接続にReadOnlyユーザーを使うことで、誤って書き込みがおこなれても無事失敗するようになります。
INSERT INTO customers VALUES (1, now(), now())
INSERT command denied to user 'reader'@'172.27.0.1' for table 'customers'
余談: test_データベース
MySQL、とくに5.6の挙動に詳しい方であれば、ReadOnlyユーザーと聞いたときに、「テスト用データベースの場合の考慮はいらないだろうか」と疑問に思うかもしれません。MySQL 5.6の挙動として、次のような仕様があります。
さらに、mysql.db テーブルにはすべてのアカウントが test データベースおよび test_ で始まる名前を持つその他のデータベースにアクセスすることを許可する行が含まれます。これは、デフォルトの匿名アカウントのように、そうでなければ特別な権限を持たないアカウントにも当てはまります。
この仕様のままであれば、test
あるいはtest_
で始まるデータベースの場合、ReadOnlyユーザーの権限を作っても書き込めてしまうことになります。これに対して、MySQL管理者への推奨は、
データベースへのアクセスを、その目的のために明示的に許可を付与されたアカウントのみに制限する場合は、管理者は mysql.db テーブルのこれらの行を削除するとよいでしょう。
とある通り、mysql.dbテーブルから該当行を削除することでこの挙動を無効にする対応が必要と説明しています。
これは、MySQLの公式イメージでは実際意識する必要はありません。なぜなら、イメージのdocker-entrypoint.sh
にて、該当行の削除をしてくれているからです。
DELETE FROM mysql.db WHERE Db='test' OR Db='test\_%' ;
公式イメージを使用すると、このようなコミュニティのナレッジを対応してくれているものを利用できるのが利点ですね。
最後に
ReadOnlyユーザーを作る方法は、比較的手軽に実践できるので、まだやっていない方は、一つの選択肢として検討してみてはいかがでしょうか。