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

※ これは、[Docker による開発をまとめた記事](https://qiita.com/kikuchi_kentaro/items/4eddd2bb119057e9652b) の続きです。

# Immutability と Data

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

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

![周辺.png](https://qiita-image-store.s3.amazonaws.com/0/20198/52f5b31e-da18-16d0-0d56-4cdd796b33ea.png)
※ Service2 を動かすには、Database と Task2 と Message Queue が必要なので、これをモック化

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

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

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

# Docker 以外の解決策

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

![データベース.png](https://qiita-image-store.s3.amazonaws.com/0/20198/72760c99-215d-5ebe-338c-2cd7d6ba10bf.png)

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

■ 共有データベース

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

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

■ 個人データベース

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

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

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

![データベース4.png](https://qiita-image-store.s3.amazonaws.com/0/20198/e85a9944-e40c-eff0-a324-32bf045d5004.png)

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

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

# Docker としての解決策

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

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

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

- RDB
    - [PostgreSQL](https://hub.docker.com/_/postgres/)
    - [MySQL](https://hub.docker.com/_/mysql/)
    - [MariaDB](https://hub.docker.com/_/mariadb/)
- NoSQL
    - [Redis](https://hub.docker.com/_/redis/)
    - [memcached](https://hub.docker.com/_/memcached/)
    - [MongoDB](https://hub.docker.com/_/mongo/)
    - [Cassandra](https://hub.docker.com/_/cassandra/)

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

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

## データベース設計・構築

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

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

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

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

## Docker におけるデータ永続化

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

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

### 1. データを含んだ状態でのイメージ化
![データベース7.png](https://qiita-image-store.s3.amazonaws.com/0/20198/f3da9138-b4ac-4ac1-fd0a-460a09272a8b.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](https://qiita-image-store.s3.amazonaws.com/0/20198/1d0354a2-80d7-b515-baea-04f3edabf7b0.png)

```dockerfile: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")"
```

```ps1
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 のやり方のほうが良さそうだ。

※ <font color="Silver">`docker build` 時に `docker-entrypoint.sh` ( docker run 時に動くシェル ) を [無理やり動かしている](http://gongo.hatenablog.com/entry/2015/08/04/232650) 事例もあるが、やはり少し無理がある。</font>

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

![ビルド3.png](https://qiita-image-store.s3.amazonaws.com/0/20198/6b0c363b-7ae9-e6dd-cf35-cfe9652dcdf5.png)


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

```ps1
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` という名前の新しいイメージとして登録する。

```ps1
PS> docker commit db initeddb
PS> docker images

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

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

```ps1
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** に保存されていて、本体のコンテナには無いという事 ([詳細](#%2a%2a2%2ea%2a%2a%20docker%20volume%20%e3%81%ab%e5%90%8d%e5%89%8d%e3%82%92%e3%81%a4%e3%81%91%e3%81%a6%e7%ae%a1%e7%90%86))。

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

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

WIP...


### 2. ダンプをマウント

![mount10.png](https://qiita-image-store.s3.amazonaws.com/0/20198/c0dc6b00-3b94-a568-bba6-3b93bd46f138.png)

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

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

#### ◆ Pros

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

#### ◆ Cons

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


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

![mount11.png](https://qiita-image-store.s3.amazonaws.com/0/20198/e9834537-399b-fb4d-3af5-3ea7d7932dc4.png)

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

#### ◆ Pros

- 起動が早い

#### ◆ Cons

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

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

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

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

## Remote File System

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

[【Dockerの最新機能を使ってみよう】Dockerのボリュームプラグインとストレージドライバについて知る](https://mag.osdn.jp/17/04/03/170000)

## Distributed File System

[Ceph](http://ceph.com/), [GlusterFS](https://www.gluster.org/), [flocker](https://github.com/ClusterHQ/flocker) 等で Data Volume を分散化しようという試みもある。

## 解決策となりうるか

ただ、これも全体としては [Docker 以外の解決策](#Docker%20%e4%bb%a5%e5%a4%96%e3%81%ae%e8%a7%a3%e6%b1%ba%e7%ad%96) と同じ問題を持っている為、今回は除外した。

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

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

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

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

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

![p1.png](https://qiita-image-store.s3.amazonaws.com/0/20198/2e7d1b84-d1b4-ff53-2ae1-7a11b8757974.png)

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

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

### 役割 1. スキーマ管理

![p2.png](https://qiita-image-store.s3.amazonaws.com/0/20198/394d4846-78b9-845e-d1cd-d4d109727e80.png)

データベースのスキーマ等をどう管理するのが良いか。
試行錯誤した結果、現在は [ridgepole](https://github.com/winebarrel/ridgepole) を使っている。

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

```ruby: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](https://qiita-image-store.s3.amazonaws.com/0/20198/91b54f72-9aa8-aa3b-35dc-8908f48f9e37.png)

#### 02_dummy.sql : データダンプ

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

![p6.png](https://qiita-image-store.s3.amazonaws.com/0/20198/9053f4bb-73e0-b437-ba00-598a0fbcc498.png)

● フェーズ 1 : CSV で COPY

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

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

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

### 役割 3. ダンプの共有

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

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

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

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

```yaml: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](https://github.com/attic-labs/noms)

まだ使ってないけど、これが Production で使えるなら、[Docker 以外の解決策](#Docker%20%e4%bb%a5%e5%a4%96%e3%81%ae%e8%a7%a3%e6%b1%ba%e7%ad%96) の問題をいくつか解決できるんじゃないだろうか。

[参考 : データのバージョン管理が可能な分散データベースNomsをさわってみる](https://qiita.com/ykshr/items/ac470aa828703e7578e8)



# 参考情報

## ◆ Docker による Volume 管理

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

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

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

![mount02.png](https://qiita-image-store.s3.amazonaws.com/0/20198/045ad97c-1832-2797-5d8d-8b426d42bd98.png)

```bash
$ 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` 以下に保存され永続化される。

```bash
$ 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](https://qiita-image-store.s3.amazonaws.com/0/20198/8114b138-34d9-3f79-290f-41afa2aed8ed.png)

```ps1
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 をマウントして中身を tar で保存すると言う手法](https://qiita.com/74th/items/41393f506d223850f2c3#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%9C%E3%83%AA%E3%83%A5%E3%83%BC%E3%83%A0%E3%81%AE%E3%83%90%E3%83%83%E3%82%AF%E3%82%A2%E3%83%83%E3%83%97)もあるにはある

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

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

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

```ps1
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 を作成

```ps1
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: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` で利用すると言うもの。

```ps1
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 ` を共有したことになる。

◆ 何に使う？

分からない。