※ これは、Docker による開発をまとめた記事 の続きです。
Immutability と Data
上記記事の中で、周辺システム疑似インテグレーションテスト を試みているという話をした。
これは、開発するアプリケーションの依存する周辺サービス・ミドルウェアをモック化 & コンテナ化し、開発段階から擬似的にインテグレーションテストを行いながら進めてくというやり方。
※ Service2 を動かすには、Database と Task2 と Message Queue が必要なので、これをモック化
コンテナ化するということは、Immutable であるということであり、それはすなわち 起動する度に真っさらになる ということである。
アプリケーションを Stateless にするのは利点が多いが、それは State を一元管理する Node がシステムの何処かにいることと表裏であり、それは 一般的にはデータベースが担う。
それでは、データベースを Immutable にしつつ Stateful にすることができるのか。
Docker 以外の解決策
の前に、Docker で管理するのはアプリケーションだけで、ストレージは他の建て方でも良いんじゃないか、という考えもある。
- 社内イントラ
- RDS, Cloud SQL 等クラウドサービス
- ホストマシン上
- VirtualBox , Hyper-V の上
■ 共有データベース
社内イントラやクラウドサービスの場合、チームで一つのデータベースにアクセスすることになる。
- Pros
- データベース管理者が管理してくれる
- 全員の環境が揃う
- 本番環境が RDS だったり、用意されたデータベースサーバなら、開発環境との差異が小さい
- Cons
- 誰かの変更が他の誰かに影響
- 息が長くなり、データベースが仕様になると大変
- Infrastructure as Code ができていれば問題ない
■ 個人データベース
ホストマシン上やホストマシンに建てた VM の上にデータベースを置いた場合、自分のためのデータベースが用意できる。
- Pros
- 各開発者が独立して作業ができる
- テスト用に、ちょっとデータいじったり
- 場合によっては、スキーマをいじったり
- スキーマ変更の提案前に影響範囲が分かる
- 各開発者が独立して作業ができる
- Cons
- 自分で管理しないといけない
- 全員の環境が揃わない
- 私はできるのに、あなたはできない
- 息が長くなると、結局大変
- Infrastructure as Code ができていれば問題ない
じゃあここで、Docker 化することによって何を期待するのか。
- データベース管理者が管理してくれる
- イメージを誰かが作れば、後はそれを使うだけ
- 建てるのも一瞬
- 各開発者が独立して作業ができる
- 当然
- 環境が揃う
- 全員が同じイメージを使えば実現できる
- 本番との差異
-
本番も Docker で行ければ 差異はない
- 本当に ?
-
本番も 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. データを含んだ状態でのイメージ化
誰もが初めに考える、スキーマ・データが全て入った状態のイメージを作ること。
◆ 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 で追加するパターン
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 も MariaDB も同様の動作をする。
これは多分、Microservice 的なベストプラクティスであるという事、そして Docker がそれを前提に設計されていることがあると思う ( docker run ~
:stdout → docker log, docker run -it ~
:stdin ← tty )
この原理原則に則るならば、起動後に状態を注入する 2 のやり方のほうが良さそうだ。
※ docker build
時に docker-entrypoint.sh
( docker run 時に動くシェル ) を 無理やり動かしている 事例もあるが、やはり少し無理がある。
1.B 立ち上げたコンテナに変更を加えるパターン
立ち上げてから、変更を加えて保存する方法を試してみる。
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. ダンプをマウント
実は、公式にデータを復元する方法が提供されている。
それが、指定されたフォルダに置いた *.sql, *.sql.gz は起動時に読み込んでくれるというもの。
PostgreSQL なんかだと、特定のフォルダ ( /var/lib/postgresql/data
) にダンプファイルを置くと、docker run
時に読み込んでくれる。
◆ Pros
- 公式に推奨された手法
- イメージをいくつも管理する必要が無い
- その代わり、ダンプはいくつも管理する必要がある
- ファイル共有することでチームで環境共有が可能
◆ Cons
- 起動時に読み込まれるのが遅い
-
docker run
が終わっても、裏ではRestore が走っていて、それが終わるまで接続できない
-
- 完全な Immutable じゃない
- ダンプファイル管理を自分たちでしなくては行けない
3. Data Volume のまま永続化・共有
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. データベース管理プロジェクト作成
まずは、データベースを管理する専用のプロジェクトを作成する。
そこでは、主に3つの役割を管理する
役割 1. スキーマ管理
データベースのスキーマ等をどう管理するのが良いか。
試行錯誤した結果、現在は ridgepole を使っている。
Ridgepole のコードによって、スキーマのバージョン管理を行う
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 化している。
02_dummy.sql : データダンプ
ここについては、まだ自分の中で固まりきれていない。
現在は、2つのフェーズに分けて実施している。
● フェーズ 1 : CSV で COPY
マスター系であれば量も少なく済むため、CSV で管理して、COPY で入れている。手動である。
● フェーズ 2 : Python + Pandas でデータ作成
データ系であれば量もそれなりに必要なので、別プロジェクトを切り出して、Python使ってダミーデータを量産している。しかし、手動である。
役割 3. ダンプの共有
役割 2 で作ったダンプファイルをどう共有するのか。
現在は、チーム全員がアクセスでき、且つアクセス管理もできるという理由で Google Drive
で共有している。手動である。
S3 でも Dropbox でも同じだと思うが、これは辛い。
2. 各プロジェクトでダミーダンプファイルを取得
各開発者が各プロジェクト毎にダウンロードしてきて、特定の場所に置き起動している。
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 というそれっぽいのがあった。
まだ使ってないけど、これが Production で使えるなら、Docker 以外の解決策 の問題をいくつか解決できるんじゃないだろうか。
参考 : データのバージョン管理が可能な分散データベースNomsをさわってみる
参考情報
◆ Docker による Volume 管理
厳密には、Volume 管理には大きく分けて2つのやり方がある。
1. ホスト上で自分で管理する
ホストの指定したディレクトリを、コンテナにマウントする方法。
$ 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 の管理する領域にフォルダを作り、そこをコンテナでマウントする。
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
という名前で作られる
- Docker が管理してくれる
-
docker volume ~
サブコマンドで操作できる
-
- 通常のコンテナではないので、イメージの保存はできない
そして、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
コマンドを使う
...
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
を共有したことになる。
◆ 何に使う?
分からない。