LoginSignup
9
4

More than 3 years have passed since last update.

Docker開発環境のDBにReadOnlyユーザーを作り、書き込みバグを検知する

Last updated at Posted at 2019-12-18

この記事は、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/

docker-compose.yml
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/_/mysqlEnvironment Variablesを見ることで環境変数で設定できる項目を確認することが出来ます。

データの永続化

コンテナが停止したときにデータが消えてしまうと開発環境としては少し物足りませんね。データをコンテナ停止しても永続化された状態にするために、次の記述でボリュームマウントします。

docker-compose.yml
    volumes:
      - ./docker/db/data:/var/lib/mysql:cached

これは、 https://hub.docker.com/_/mysqlCaveats > Where to Store Dataに説明されています。Git管理下におくとMySQLのデータがそのままコミットされてしまうので、対象ディレクトリを.gitignoreに追加しておくのを忘れないようにしましょう。

.gitignore
# docker database local mount files
docker/db/data

Custom MySQL Configuration

デフォルトの設定ではなく、ユーザー自身でMySQLの設定をカスタムしたいという場合は、コンテナ内の/etc/mysql/conf.dに設定をマウントします。これは、 https://hub.docker.com/_/mysqlUsing a custom MySQL configuration file に説明されています。

docker-compose.yml
    volumes:
      - ./docker/db/conf.d:/etc/mysql/conf.d:cached

例えば、このケースではつぎのようなmy.cnfを設定します。

docker/db/cnf.d/custom_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では次の設定項目が該当します。

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ユーザーを作成できます。

docker/db/docker-entrypoint-initdb.d/2_readuser.sql
GRANT SELECT, PROCESS ON *.* TO 'reader'@'%' IDENTIFIED BY 'password';

ファイル名ですが、

Files will be executed in alphabetical order.

という記述が、https://hub.docker.com/_/mysqlInitializing a fresh instanceにあることが理由です。アルファベット順にソートされ実行されるので、明示的に順番になるように数字を先頭にしています。

実際にReadOnlyユーザーが作れているか

実際に、rootアカウントでデータベースにアクセスし、ユーザーが作られているか確認します。mysql.userテーブルに対して検索すると次のような結果が得られます。

SELECT 
    User
FROM
    mysql.user;
reader
root
wruser
root

作成したReadOnlyユーザーであるreaderを確認することが出来ます。このユーザーには、GRANTで権限を付けているので、SELECTPROCESSが可能なユーザーになっています。

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ユーザーを使うことで、誤って書き込みがおこなれても無事失敗するようになります。

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ユーザーを作る方法は、比較的手軽に実践できるので、まだやっていない方は、一つの選択肢として検討してみてはいかがでしょうか。

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4