Git
docker
git-lfs

データはどう管理するべきなのか with Docker

More than 1 year has passed since last update.

※ これは、Docker による開発をまとめた記事 の続きです。

Immutability と Data

上記記事の中で、周辺システム疑似インテグレーションテスト を試みているという話をした。

これは、開発するアプリケーションの依存する周辺サービス・ミドルウェアをモック化 & コンテナ化し、開発段階から擬似的にインテグレーションテストを行いながら進めてくというやり方。

周辺.png
※ Service2 を動かすには、Database と Task2 と Message Queue が必要なので、これをモック化

コンテナ化するということは、Immutable であるということであり、それはすなわち 起動する度に真っさらになる ということである。

アプリケーションを Stateless にするのは利点が多いが、それは State を一元管理する Node がシステムの何処かにいることと表裏であり、それは 一般的にはデータベースが担う

それでは、データベースを Immutable にしつつ Stateful にすることができるのか。

Docker 以外の解決策

の前に、Docker で管理するのはアプリケーションだけで、ストレージは他の建て方でも良いんじゃないか、という考えもある。

データベース.png

  • 社内イントラ
  • RDS, Cloud SQL 等クラウドサービス
  • ホストマシン上
  • VirtualBox , Hyper-V の上

■ 共有データベース

社内イントラやクラウドサービスの場合、チームで一つのデータベースにアクセスすることになる。

  • Pros
    • データベース管理者が管理してくれる
    • 全員の環境が揃う
    • 本番環境が RDS だったり、用意されたデータベースサーバなら、開発環境との差異が小さい
  • Cons
    • 誰かの変更が他の誰かに影響
    • 息が長くなり、データベースが仕様になると大変
      • Infrastructure as Code ができていれば問題ない

■ 個人データベース

ホストマシン上やホストマシンに建てた VM の上にデータベースを置いた場合、自分のためのデータベースが用意できる。

  • Pros
    • 各開発者が独立して作業ができる
      • テスト用に、ちょっとデータいじったり
      • 場合によっては、スキーマをいじったり
        • スキーマ変更の提案前に影響範囲が分かる
  • Cons
    • 自分で管理しないといけない
    • 全員の環境が揃わない
      • 私はできるのに、あなたはできない
    • 息が長くなると、結局大変
      • Infrastructure as Code ができていれば問題ない

じゃあここで、Docker 化することによって何を期待するのか。

データベース4.png

  • データベース管理者が管理してくれる
    • イメージを誰かが作れば、後はそれを使うだけ
    • 建てるのも一瞬
  • 各開発者が独立して作業ができる
    • 当然
  • 環境が揃う
    • 全員が同じイメージを使えば実現できる
  • 本番との差異
    • 本番も Docker で行ければ 差異はない
      • 本当に ?
  • 息が長くなると、結局大変
    • Immutable な使い方をすれば、毎度破棄 / 生成されるので、Infrastructure as Code が徹底される

ということで、以下は Docker によってどう解決するかを見ていく。

Docker としての解決策

データベース系の公式コンテナ

まず、データベースのコンテナイメージをどう作れば良いのか。

PostgreSQL や MySQL 等の主要なリレーショナルデータベースも、MongoDB や Redis 等のいわゆる NoSQL も、ほぼ公式に Docker イメージが存在している。

これらは、当然全てデータが空の基本的イメージである。
我々は、特別な理由がない限り公式イメージをベースに使っている。

ただし、チューニングが必要な場合、Dockerfile で一から構築はあり得る。

データベース設計・構築

ということで、データベースの設計・構築に入る。

設計者がデータベース、ユーザ、テーブル等を設計して。

ここで、安易に Dockerfile に psql -c ~ ( PostgreSQL と仮定 ) を連々と書いたり、立ち上げたデータベースコンテナに接続して create database ~ し出すと、困ったことになる。

Docker は、そう簡単に 状態を残してはくれない のだ。

Docker におけるデータ永続化

Docker は基本的には Immutable である事を求める。
それでいて、イメージの中にデータ (状態) を含むことを嫌がる慣習がある。

以下、PostgreSQL を前提に考えていく。

1. データを含んだ状態でのイメージ化

データベース7.png

誰もが初めに考える、スキーマ・データが全て入った状態のイメージを作ること。

◆ Pros

  • バージョン管理が可能
  • Immutability も完璧
  • Docker Registry に登録すれば、登録 / 配布 のエコシステムに乗れる

◆ Cons

  • Docker コミュニティが推奨していない ? (後述)

実現方法

大まかな実現方法としては、以下が思いつく。

A. Dockerfile に記述し、docker build でイメージ化
B. 立ち上げたコンテナに変更を加え、docker commit / save / export でイメージ化
- docker run ~
- docker exec psql -c "~~"
- docker commit or save or export ~

Infrastructure as Code 的には A を押したい所だが。
以下両方を見ていく。

1.A Dockerfile で追加するパターン

ビルド4.png

Dockerfile
FROM postgres:9.6

RUN psql -d postgres --username postgres -c "create table public.user (name varchar)" \
  && psql -d postgres --username postgres -c "insert into public.user (name) values ("taro")"
PS> docker build -t test .

Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM postgres:9.6
 ---> f8d91fbcfa35
Step 2/2 : RUN psql -d postgres --username postgres -c "create table public.user (name varchar)"   && psql -d postgres --username postgres -c "insert into public.user (name) values ("taro")"
 ---> Running in 9af317a881ca
psql: could not connect to server: No such file or directory
        Is the server running locally and accepting
        connections on Unix domain socket "/var/run/postgresql/.s.PGSQL.5432"?

あれ、そもそも繋がらない。

● ミドルウェア初期化の難しさ

実は Docker は、一つのコンテナで一つのプロセスを Run or Start 時に立ち上げる のがベストプラクティスとなっている。
psql している段階では、PostgreSQL は立ち上がってもいないし、initdb もされていない。
MySQL も MarriaDB も同様の動作をする。

これは多分、Microservice 的なベストプラクティスであるという事、そして Docker がそれを前提に設計されていることがあると思う ( docker run ~:stdout → docker log, docker run -it ~:stdin ← tty )

この原理原則に則るならば、起動後に状態を注入する 2 のやり方のほうが良さそうだ。

docker build 時に docker-entrypoint.sh ( docker run 時に動くシェル ) を 無理やり動かしている 事例もあるが、やはり少し無理がある。

1.B 立ち上げたコンテナに変更を加えるパターン

ビルド3.png

立ち上げてから、変更を加えて保存する方法を試してみる。

PS> docker run -d --name db -e POSTGRES_PASSWORD=password postgres:9.6
PS> docker exec db psql -U postgres -c "create table public.user (name varchar)"
PS> docker exec db psql -U postgres -c "insert into public.user (name) values ('taro')"
PS>  docker exec db psql -U postgres -c "select * from public.user"
 name
------
 taro
(1 row)

これを、initeddb という名前の新しいイメージとして登録する。

PS> docker commit db initeddb
PS> docker images

REPOSITORY                 TAG                 IMAGE ID            CREATED             SIZE
initeddb                   latest              1tiedea87262        7 seconds ago       269MB
...

で、改めて立ち上げてみると、

PS> docker stop db ; docker rm -f db

PS> docker run -d --name new_db initeddb:latest
PS> docker exec new_db psql -U postgres -c "select * from public.user"
ERROR:  relation "public.user" does not exist
LINE 1: select * from public.user

無くなっている…

● VOLUME の罠

実は、PostgreSQL の公式イメージの Dockerfile を見ると、VOLUME /var/lib/postgresql/data という記述がある。

これは、PostgreSQL への変更は Data Volume に保存されていて、本体のコンテナには無いという事 (詳細)。

これはつまり、データは含めさせないぞ との公式からの強い意思なのである。
Dockerfile を自分で書いて回避することもできるが、何故ここまで含めさせたくないのかが気になる。

何故、データベースの状態をイメージに含めさせないのか

WIP...

2. ダンプをマウント

mount10.png

実は、公式にデータを復元する方法が提供されている。
それが、指定されたフォルダに置いた *.sql, *.sql.gz は起動時に読み込んでくれるというもの。

PostgreSQL なんかだと、特定のフォルダ ( /var/lib/postgresql/data ) にダンプファイルを置くと、docker run 時に読み込んでくれる。

◆ Pros

  • 公式に推奨された手法
  • イメージをいくつも管理する必要が無い
    • その代わり、ダンプはいくつも管理する必要がある
  • ファイル共有することでチームで環境共有が可能

◆ Cons

  • 起動時に読み込まれるのが遅い
    • docker run が終わっても、裏ではRestore が走っていて、それが終わるまで接続できない
  • 完全な Immutable じゃない
  • ダンプファイル管理を自分たちでしなくては行けない

3. Data Volume のまま永続化・共有

mount11.png

Volume 機能を使ってデータフォルダをマウントポイントにすることで、アプリケーションコンテナが落ちてもデータを残すことができる。

◆ Pros

  • 起動が早い

◆ Cons

  • 2 との違いが無い
    • 共有・バージョン管理が必要
    • そのくせ
      • 公式が推奨したやり方ではない
      • 手順が多い

積極的に選ぶ理由は無さそうだ

Docker としての解決策だけど、違ったアプローチ

Docker の機能を使いながらも、少し違った面から攻める方法もある。
Remote ファイルシステムや分散ファイルシステムを利用するのだ。

Remote File System

Docker の Data Volume に NFS や Samba/CIFS を直接マウントしようという試みもある。

【Dockerの最新機能を使ってみよう】Dockerのボリュームプラグインとストレージドライバについて知る

Distributed File System

Ceph, GlusterFS, flocker 等で Data Volume を分散化しようという試みもある。

解決策となりうるか

ただ、これも全体としては Docker 以外の解決策 と同じ問題を持っている為、今回は除外した。

結局、どれがベストなのか

今の所、2 の方法を取っている

開発フローはどうなるのか

では、これを実際にどう使っているのかをまとめる。

1. データベース管理プロジェクト作成

p1.png

まずは、データベースを管理する専用のプロジェクトを作成する。

そこでは、主に3つの役割を管理する

役割 1. スキーマ管理

p2.png

データベースのスキーマ等をどう管理するのが良いか。
試行錯誤した結果、現在は ridgepole を使っている。

Ridgepole のコードによって、スキーマのバージョン管理を行う

Schemafile
create_table "users", force: :cascade do |t|
  t.string   "name"          ,null: false
  t.string   "display_name"  ,null: false

  t.datetime "created_at"    ,null: false
  t.datetime "updated_at"    ,null: false
end

create_table "offices", force: :cascade do |t|
  t.string   "name"           ,null: false
  t.string   "address"           ,null: false

  t.datetime "created_at"     ,null: false
  t.datetime "updated_at"     ,null: false
end
...

役割 2. ダミーデータダンプの管理

次に、データベース起動時に与える開発用ダミーデータダンプの作成を行っている。

01_schema.sql : ベースとなる Schema ダンプ

Ridgepole の Schema は、CI 時に SQL 化している。

p3.png

02_dummy.sql : データダンプ

ここについては、まだ自分の中で固まりきれていない。
現在は、2つのフェーズに分けて実施している。

p6.png

● フェーズ 1 : CSV で COPY

マスター系であれば量も少なく済むため、CSV で管理して、COPY で入れている。手動である。

● フェーズ 2 : Python + Pandas でデータ作成

データ系であれば量もそれなりに必要なので、別プロジェクトを切り出して、Python使ってダミーデータを量産している。しかし、手動である。

役割 3. ダンプの共有

役割 2 で作ったダンプファイルをどう共有するのか。
現在は、チーム全員がアクセスでき、且つアクセス管理もできるという理由で Google Drive で共有している。手動である。

S3 でも Dropbox でも同じだと思うが、これは辛い

2. 各プロジェクトでダミーダンプファイルを取得

各開発者が各プロジェクト毎にダウンロードしてきて、特定の場所に置き起動している。

docker-compose.yml
services:
  db:
    container_name: postgres
    image: postgres:9.5
    ports:
      - "5432:5432"
    volumes:
      - ./dump/db:/docker-entrypoint-initdb.d
    environment:
      - POSTGRES_USER=test
      - POSTGRES_PASSWORD=test
      - POSTGRES_DB=test_db
    networks:
      - default

今検討していること

ダンプの共有方法を変えたい

大きなデータを、自動で、チーム間共有もできて、バージョン管理もできる方法は無いものか。
いくつか候補はあるが、まだまだ調査・検討段階である。

git lfs を使う

いっそ、Git で管理できてば良いんじゃないかと

WIP...

noms を使う

もう、データベース自体がバージョン管理してくれねーかな、と思っていたら、norm というそれっぽいのがあった。

norm

まだ使ってないけど、これが Production で使えるなら、Docker 以外の解決策 の問題をいくつか解決できるんじゃないだろうか。

参考 : データのバージョン管理が可能な分散データベースNomsをさわってみる

参考情報

◆ Docker による Volume 管理

厳密には、Volume 管理には大きく分けて2つのやり方がある。

1. ホスト上で自分で管理する

ホストの指定したディレクトリを、コンテナにマウントする方法。

mount02.png

$ docker run -d --name db -v "/path/to/data:/var/lib/postgresql/data" postgres:9.6

これによって、ホストの /path/to/data/var/lib/postgresql/data にマウントされ、データが /path/to/data 以下に保存され永続化される。

$ docker exec db psql -U postgres -c "create table public.user (name varchar)"
$ docker exec db psql -U postgres -c "insert into public.user (name) values ('taro')"
$ docker stop db ; docker rm -f db

# 同じホストディレクトリをマウントして実行
$ docker run -d --name new_db -v "/path/to/data:/var/lib/postgresql/data" postgres:9.6
$ docker exec new_db psql -U postgres -c "select * from public.user"
 name
------
 taro
(1 row)

※ Ownership の関係で windows では動かないが、雰囲気だけでも感じて

  • 保存先を明示するので、直感的で分かりやすい
  • データの管理は、利用者に任される
    • 『どれが何のデータか』などのメタデータ管理
    • ゴミデータの削除

2. Data Volume

Docker の管理する領域にフォルダを作り、そこをコンテナでマウントする。

mount03.png

PS> docker volume create --name my_data
PS> docker run -d -v my_data:/var/lib/postgresql/data postgres:9.6

これで、my_data という名前の Data Volume が作成され、コンテナの /var/lib/postgresql/data にマウントされる。

Windows であれば、Data Volume は Moby Linux VM の File system 上にある /var/lib/docker/volumes/ 以下に、<<ハッシュ>>/_data という名前で作られる

そして、Data Volume を利用する場合でも、いくつかの使い方がある

2.a docker volume に名前をつけて管理

自分で名前を指定でき、且つ管理は docker に任せられるので、便利に使える。

PS> docker volume create --name my_data
PS> docker volume ls

DRIVER              VOLUME NAME
local               my_data

PS> docker volume rm my_data

◆ 何に使う?

  • データの永続化
  • コンテナ間で volume の共有

2.b -v <<path>> で Temporary Data Volume を作成

PS> docker run -d -v /path/to/dir postgres:9.6

これを実行すると、Temporary Data Volume が作られ、/path/to/dir にマウントされる。
コンテナを落とすと、誰も参照しない削除を待つだけの 野良 Data Volume となる。
( 正確には、ハッシュ値を指定すれば参照はできる。興味があったら、docker inspect <container name> | jq ".[0].Mounts" をしてみよう )

◆ 何に使う?

色々調べて見るに、どうやら

  • docker commit / save/ export する上で邪魔な一時ファイルを捨てる
  • アプリケーションコンテナを圧迫する程に一時ファイルをできる場合、それを隔離する

為に使うようだ。

2.c Dockerfile の中で、Volume コマンドを使う

Dockerfile
...
VOLUME /var/lib/postgresql/data`
...

これは、やっていることは 2.b と同じで、docker run 時に Temporary Data Volume が作られてマウントされる。

◆ 何に使う?

目的も 2.b と近いんだけど、もう一つ重要な役割として、docker build 時にイメージに含まれないようにする というのがある。

2.d volume-from で、他のコンテナを Data Volume Container としてマウント

Data Volume Container というやり方がある。
これは、データを公開するためだけのコンテナを立て、それを利用するコンテナが --volume-from で利用すると言うもの。

PS> docker run -d --name data_proxy -v /path/to/data -v /path/to/config busybox:latest

PS> docker run --name db1 --volume-from data_proxy postgres:9.6
PS> docker run --name db2 --volume-from data_proxy postgres:9.6
PS> docker run --name db3 --volume-from data_proxy postgres:9.6

これで、db1, db2, db3 は /path/to/data/path/to/config を共有したことになる。

◆ 何に使う?

分からない。