Docker Container におけるデータ管理の基礎と仕組み

Written by @ryysud

Aug 11, 2018 10:32 · 4212 words · 9 minutes read #docker

モチベーション

Docker を利用して、ローカルでの開発環境をコンテナで構築することが多いのですが、コンテナ内のデータ管理に関して「Docker ホストのディレクトリをマウントしてコンテナ内とデータ同期が取れる!嬉しい!」ぐらいの知識でずっとやってきて、最近やっと理解が浅いことに焦り始めたのがこの記事を書こうと思ったきっかけです。

2018年8月時点での公式の情報を元にまとめたものなので、今後のバージョンアップでいくつか変更が発生するかもしれませんが悪しからず。

とりあえず公式ドキュメントを読んで理解を深める

公式ドキュメント

Manage data in Docker - docker docs

雑メモ

Docker にはコンテナが Docker ホストにデータを永続化するために Volumes と Bind mounts という2つの仕組みがあり、それらのオプションを利用することで、コンテナが停止した後でも Docker ホストにデータを永続化することが可能になります。Linux 上で Docker を稼働させている場合には tmpfs mounts も利用可能なようです。

Volumes と Bind mounts と tmpfs mounts の違い

それぞれ Docker ホストでのデータの持たせ方に違いがあります。

種別 Docker ホスト内でデータが格納される場所 備考
Volumes Docker の管理下にある特定のファイルシステム
※ Linux だとデフォルトで /var/lib/docker/volumes
Docker プロセス以外はファイルに触らない方が良い
Docker でデータの永続化を図るならこれがベスト
Bind mounts コンテナ起動時に指定されたファイルシステム どんなプロセスがデータを修正しても問題なし
Docker ホストのディレクトリ構造に依存している
tmpfs mounts メモリ ファイルシステムに書かれることはない
Docker on Linux でのみ利用可能

上記の表でわかりづらければ、公式ドキュメントに載っている図がとてもわかりやすいのでおすすめです。ちなみに、どのデータ永続化の方法であろうと、コンテナからのデータの見え方に影響は及ぼしません。

動作検証で利用する環境

Docker for Mac の 2018年8月時点 での最新バージョン 18.06.0-ce で動作検証を進めていきます。

$ docker version
Client:
 Version:           18.06.0-ce
 API version:       1.38
 Go version:        go1.10.3
 Git commit:        0ffa825
 Built:             Wed Jul 18 19:05:26 2018
 OS/Arch:           darwin/amd64
 Experimental:      false

Server:
 Engine:
  Version:          18.06.0-ce
  API version:      1.38 (minimum version 1.12)
  Go version:       go1.10.3
  Git commit:       0ffa825
  Built:            Wed Jul 18 19:13:46 2018
  OS/Arch:          linux/amd64
  Experimental:     true

Volumes に関して

利点

  • Bind mounts よりバックアップやマイグレーションが楽
  • Docker API や Docker CLI 経由での管理が可能
  • Linux コンテナと Windows コンテナの両方で機能する
  • 複数コンテナで Volume を共有することが可能
  • Volume Driver を利用することでリモートホストやクラウドに Volume を格納することが可能
  • Volume Driver を利用することで Volume の暗号化なども可能

作成方法

以下2つの方法があります。

① docker create volume コマンドを実行

# 名前付き Volume を作成する場合
$ docker volume create [Volume名]

# 匿名 Volume を作成する場合
$ docker volume create

② コンテナかサービスを作成するタイミングで –mount または -v (–volume) オプションを付与して実行

–mount と -v (–volume) の違いは、–mount の方が –volume より明示的で冗長なものになっていることと、swarm mode で稼働させる際には –mount しか利用することが出来ないなどの違いなどが挙げられます。

# 匿名 Volume を作成する場合
-v [Volume をマウントするコンテナ内のパス]
--volume [Volume をマウントするコンテナ内のパス]

# 名前付き Volume を作成する場合
-v [Volume名]:[Volume をマウントするコンテナ内のパス]
--volume [Volume名]:[Volume をマウントするコンテナ内のパス]

--mount 'type=volume,src=[Volume名],dst=[Volume をマウントするコンテナ内のパス]'

作成した Volume をコンテナにマウントすることで Volume の利用が可能になります。上の表で説明したように Volume の実体は Docker の管理下にある特定のファイルシステム(Linux だとデフォルトで /var/lib/docker/volumes)なので、結果的な挙動は、Bind mounts と非常に似ているなあと感じました。

また、Volume には “名前付き” と “匿名” の2種類が存在していて、匿名の場合は Volume 作成時に Docker ホスト内で一意となるランダムな名前が自動で付与されるようです。

実際にコマンドを実行して挙動を確認していく

Volume の作成(① docker create volume コマンド実行するパターン)

# 名前付き Volume の作成
$ docker volume create my-vol

# 匿名 Volume の作成(引数なしだと匿名 Volume が作成される)
$ docker volume create
87252a07e154a3f0b95bae2a64c844c56f6b04ffe451393495ba623ab1d07c9e

# Volume 一覧
$ docker volume ls
DRIVER              VOLUME NAME
local               87252a07e154a3f0b95bae2a64c844c56f6b04ffe451393495ba623ab1d07c9e
local               my-vol

Volume の検査

$ docker volume inspect my-vol
[
    {
        "CreatedAt": "2018-08-11T04:06:34Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/my-vol/_data",
        "Name": "my-vol",
        "Options": {},
        "Scope": "local"
    }
]

$ docker volume inspect 87252a07e154a3f0b95bae2a64c844c56f6b04ffe451393495ba623ab1d07c9e
[
    {
        "CreatedAt": "2018-08-11T04:06:44Z",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/var/lib/docker/volumes/87252a07e154a3f0b95bae2a64c844c56f6b04ffe451393495ba623ab1d07c9e/_data",
        "Name": "87252a07e154a3f0b95bae2a64c844c56f6b04ffe451393495ba623ab1d07c9e",
        "Options": {},
        "Scope": "local"
    }
]

Volume の削除

Volume の削除は自動的には行われないので、コマンドから明示的に行う必要があります。

# Volume を指定して削除
$ docker volume rm my-vol 87252a07e154a3f0b95bae2a64c844c56f6b04ffe451393495ba623ab1d07c9e
my-vol
87252a07e154a3f0b95bae2a64c844c56f6b04ffe451393495ba623ab1d07c9e

$ docker volume ls
DRIVER              VOLUME NAME

# Volume の一括削除
$ docker volume create
54acafc21b31706e605908423a1ded45ecca9ecfe0fcccf49d833d2363e46d3e

$ docker volume create
b1ed197aa7ef5368d128ffff255e4c70a8b2a5d6d6322b9172e929ad493cd95f

$ docker volume ls
DRIVER              VOLUME NAME
local               54acafc21b31706e605908423a1ded45ecca9ecfe0fcccf49d833d2363e46d3e
local               b1ed197aa7ef5368d128ffff255e4c70a8b2a5d6d6322b9172e929ad493cd95f

$ docker volume prune
WARNING! This will remove all local volumes not used by at least one container.
Are you sure you want to continue? [y/N] y
Deleted Volumes:
54acafc21b31706e605908423a1ded45ecca9ecfe0fcccf49d833d2363e46d3e
b1ed197aa7ef5368d128ffff255e4c70a8b2a5d6d6322b9172e929ad493cd95f

Total reclaimed space: 0B

$ docker volume ls
DRIVER              VOLUME NAME

Volume の作成(② –mount または -v (–volume) オプションを付与して実行するパターン)

# --mount を付与するパターンで実行
# -v (--volume) を付与するパターンは後の検証で登場させます
$ docker run -d \
    --name=nginx-test \
    --mount source=nginx-vol,destination=/usr/share/nginx/html \
    nginx:latest
717a6acad80c03016d73e5d0b042815faadfb504450bd4983ab21c93913525d8

$ docker volume ls
DRIVER              VOLUME NAME
local               nginx-vol

# Volume がコンテナ内の /usr/share/nginx/html にマウントされている様子がわかる
$ docker inspect nginx-test | jq '.[].Mounts'
[
  {
    "Type": "volume",
    "Name": "nginx-vol",
    "Source": "/var/lib/docker/volumes/nginx-vol/_data",
    "Destination": "/usr/share/nginx/html",
    "Driver": "local",
    "Mode": "z",
    "RW": true,
    "Propagation": ""
  }
]

# Volume をマウントするコンテナ内のパス直下に格納されているファイルを確認
$ docker exec nginx-test ls -l /usr/share/nginx/html
total 8
-rw-r--r-- 1 root root 537 Jun  5 12:00 50x.html
-rw-r--r-- 1 root root 612 Jun  5 12:00 index.html

# Docker for Mac で稼働している仮想マシンに入って Volume の実体を確認するとデータが同じであることがわかる
$ screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty
linuxkit-025000000001:~# ls -l /var/lib/docker/volumes/nginx-vol/_data
total 8
-rw-r--r--    1 root     root           537 Jun  5 12:00 50x.html
-rw-r--r--    1 root     root           612 Jun  5 12:00 index.html

コンテナが無くなっても Volume は削除されずに残り再利用が可能なことを確認

# -v (--volume) を付与するパターンで実行
$ docker run -d \
   --name=nginx-test \
   -v nginx-vol:/usr/share/nginx/html \
   nginx:latest
bc30617cd6b888c625be0000f39ba4cbe7486f7d2c16a73d64ce6eb50daad3ea

# Volume が作成されたことを確認
$ docker volume ls
DRIVER              VOLUME NAME
local               nginx-vol

# Volume をマウントしたコンテナ内のパス直下にファイルを追加する
$ docker exec -it nginx-test bash

root@bc30617cd6b8:/# ls -l /usr/share/nginx/html
total 8
-rw-r--r-- 1 root root 537 Jun  5 12:00 50x.html
-rw-r--r-- 1 root root 612 Jun  5 12:00 index.html

root@bc30617cd6b8:/# touch /usr/share/nginx/html/test.html

root@bc30617cd6b8:/# ls -l /usr/share/nginx/html
total 8
-rw-r--r-- 1 root root 537 Jun  5 12:00 50x.html
-rw-r--r-- 1 root root 612 Jun  5 12:00 index.html
-rw-r--r-- 1 root root   0 Aug 11 06:35 test.html

root@bc30617cd6b8:/# exit

# コンテナを削除
$ docker rm -f nginx-test
nginx-test

# Volume は削除されていないことを確認
docker volume ls
DRIVER              VOLUME NAME
local               nginx-vol

# Docker for Mac で稼働している仮想マシンに入って Volume の実体を確認すると追加されたファイルも含まれた形でデータが残っていることがわかる
$ screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty
linuxkit-025000000001:~# ls -l /var/lib/docker/volumes/nginx-vol/_data
total 8
-rw-r--r--    1 root     root           537 Jun  5 12:00 50x.html
-rw-r--r--    1 root     root           612 Jun  5 12:00 index.html
-rw-r--r--    1 root     root             0 Aug 11 06:35 test.html

# 削除されずに残っている Volume を指定する形で再度コンテナを起動してみる
$ docker run -d \
   --name=nginx-test \
   -v nginx-vol:/usr/share/nginx/html \
   nginx:latest
889cb7cbbc0fc8809ad51508eee2f7db04aaa6626ea31c63abbe62b6205d0a33

# 既に存在する Volume を指定したので Volume が新規作成されることはない
$ docker volume ls
DRIVER              VOLUME NAME
local               nginx-vol

# Volume をマウントしたコンテナ内のパス直下を確認すると以前のコンテナで利用していたデータと同じものが確認できた
$ docker exec nginx-test ls -l /usr/share/nginx/html
total 8
-rw-r--r-- 1 root root 537 Jun  5 12:00 50x.html
-rw-r--r-- 1 root root 612 Jun  5 12:00 index.html
-rw-r--r-- 1 root root   0 Aug 11 06:35 test.html

Bind mounts に関して

Bind mounts が1番使われるものかなと勝手に思っています。
単なる経験不足かもしれませんが、私自身メインでこれしか使ったことがありません。

制限事項

Bind mounts は Volumes に比べて、以下のようにいくつか機能的な制限があります。

  • ファイルまたはディレクトリは Docker ホストの絶対パスで指定する必要がある
  • 相対パスの指定はサポートされていない(Volumes の作成と区別するためにかなって勝手に思ってる)
  • マウントするパスを指定する際に Docker ホストに存在していないファイルまたはディレクトリを指定することも可能

使用するに注意すべきところ

Bind mounts を使用してコンテナに Docker ホストの特定のファイルシステムをマウントすることで、コンテナ内のプロセスを介して Docker ホストのファイルシステムに変更が加えることが可能になるので、セキュリティ的な観点で注意が必要となります。

実際にコマンドを実行して挙動を確認していく

データが Docker ホスト側のファイルシステム上で永続化されることを確認

# --mount を付与するパターンで実行(--mount の type を bind と指定する必要がある)
$ mkdir app
$ docker run -d \
    --name nginx-test \
    --mount type=bind,source=$(pwd)/app,dst=/app \
    nginx:latest
4a73dea39da50260d6d8d137072cba6f8b001a4c1523fe836c7bc1595b0abff3

# Bind mounts したコンテナ内のパス直下にファイルを作成してみる
$ docker exec nginx-test touch /app/test.txt

# Bind mounts した Docker ホスト側のディレクトリを確認するとデータが永続化されていることがわかる
$ ls -l app
total 0
-rw-r--r--  1 yoshida  staff     0B  8 11 16:01 test.txt

# Bind mounts した Docker ホスト側のディレクトリ直下にファイルを作成してみる
$ touch app/test2.txt

# Bind mounts したコンテナ内のパス直下を確認すると作成したファイルが反映されていることがわかる
$ docker exec nginx-test ls -l /app
total 0
-rw-r--r-- 1 root root 0 Aug 11 07:01 test.txt
-rw-r--r-- 1 root root 0 Aug 11 07:03 test2.txt

tmpfs mounts に関して

tmpfs mounts は Volume と Bind mounts と違い、コンテナが停止したら、そこに書き込まれたファイルは永続化されません。なので、適した使い方が求められそうな感じですね。ちなみに docker engine の swarm mode は tmpfs mounts で機密情報をコンテナにマウントしているみたいです。

制限事項

  • Volume と Bind mounts と違い複数コンテナでデータ共有することは不可能
  • Docker on Linux でのみ利用可能

実際にコマンドを実行して挙動を確認していく

データが Docker ホスト側のメモリ上で永続化されることを確認

# --mount を付与するパターンで実行(--mount の type を tmpfs と指定する必要がある)
$ docker run -d \
    --name nginx-test \
    --mount type=tmpfs,destination=/app \
    nginx:latest
5f2d4098a5dc51c8a69a5c50050236a483095329a2ee052e90bac54141d05ab0

# --tmpfs オプションでも同じ挙動
# $ docker run -d \
#    --name nginx-test \
#    --tmpfs /app \
#    nginx:latest

# コンテナのマウント情報をみると tmpfs mounts が適用されていることがわかる
$ docker inspect nginx-test | jq '.[].Mounts'
[
  {
    "Type": "tmpfs",
    "Source": "",
    "Destination": "/app",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
]

# Mac のメモリ内を確認する術がちょっと謎なのでギブアップ...。
$ some_command

それぞれ推奨されるユースケース

Volumes

基本的には Volumes を使うのがベストとのこと

  • 複数コンテナでデータを共有したい場合
  • Docker ホストが特定のディレクトリまたはファイル構造を持つことが保証されていない場合
  • ローカルではなく、リモートホストまたはクラウドプロバイダにコンテナのデータを保存したい場合
  • ある Docker ホストから別の Docker ホストへのデータのバックアップや復元や移行をしたい場合

Bind mounts

  • /etc/resolve.conf などのファイルを Docker ホストとコンテナで共有したい場合
  • ビルドした成果物だけが必要な場合(Golang のビルドなど)
  • Docker ホストが特定のディレクトリまたはファイル構造を持つことが保証されている場合

tmpfs mounts

  • データを Docker ホストとコンテナに残したくない場合

さいごに

公式ドキュメントをしっかりと読んで、手を動かして挙動を確認したことで、Volume、Bind mounts、tmpfs mounts などの Docker が提供するデータ管理の仕組みを理解することができました。データ管理に関して「Docker ホストのディレクトリをマウントしてコンテナ内とデータ同期が取れる!嬉しい!」ぐらいの知識からは脱せたと思うので、非常に良かったなと感じています。

今回は基本的なところだけをピックアップして、基礎の理解に努めたので、今度は Volume Driver を利用してのリモートホストやクラウドへの Volume 格納であったり、Volume の暗号化などもやっていこうと思います!