CUBE SUGAR CONTAINER

技術系のこと書きます。

Docker: データボリュームとデータボリュームコンテナ

Docker を使っているとデータの永続化が問題になる。 例えばデータベースのアプリケーションを動作させるとして、どこにデータを残せばいいだろう。 通常のファイルシステム上に置いてしまうと、コンテナが終了すると使えなくなってしまう。 そんなときに便利なのが今回使うデータボリュームとデータボリュームコンテナのようだ。 これを使うと Docker ホスト上のファイルをコンテナでマウントできるようになる。

データボリューム

まず、データボリュームから使ってみる。 コンテナにデータボリュームを接続するには docker run コマンドに -v オプションをつける。 もちろん Dockerfile で指定することもできて、そのときは VOLUME 命令を使う。

例えばコンテナの /mydata にデータボリュームをマウントしてみよう。 イメージには CentOS7 を使った。

$ docker run --name datavolume1 -i -v /mydata -t centos:7 /bin/bash

コンテナ上で df コマンドを確認すると /mydata に /dev/sda1 がマウントされていることがわかる。

# df
Filesystem     1K-blocks    Used Available Use% Mounted on
none            19049892 2073860  15985308  12% /
tmpfs             510044       0    510044   0% /dev
tmpfs             510044       0    510044   0% /sys/fs/cgroup
/dev/sda1       19049892 2073860  15985308  12% /mydata
shm                65536       0     65536   0% /dev/shm

一旦 Docker ホストの操作に戻ろう。 マウントされたデータボリュームの情報は docker inspect コマンドで確認できる。 ここで得られるフォーマットは JSON なので jq コマンドを使って表示内容を絞ると分かりやすい。

$ docker inspect datavolume1 | jq ".[0].Mounts"
[
  {
    "Name": "da484ac1637f1f18a85648b7c129ca33006a18c73f319f36fea1e257afb66a16",
    "Source": "/mnt/sda1/var/lib/docker/volumes/da484ac1637f1f18a85648b7c129ca33006a18c73f319f36fea1e257afb66a16/_data",
    "Destination": "/mydata",
    "Driver": "local",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
]

上記では Docker ホスト上の /mnt/sda1/var/lib/docker/volumes/... にあるファイルが Docker コンテナの /mydata にマウントされていることが読み取れる。

試しにコンテナ上で /mydata にファイルを作ってみよう。

# cd /mydata/
# touch helloworld.txt

そして Docker ホスト側で先ほど docker inspect で確認したディレクトリを確認してみる。 するとコンテナの中で作ったファイルがそのまま見えることがわかる。

$ sudo ls /mnt/sda1/var/lib/docker/volumes/da484ac1637f1f18a8564
8b7c129ca33006a18c73f319f36fea1e257afb66a16/_data
helloworld.txt

なお、コンテナを終了したり削除してもこのデータボリュームとして使われているディレクトリは削除されない点に注意が必要だ。

このデータボリュームに使われるディレクトリの場所は指定することもできる。 それには -v オプションで \<dir>:\<mount> というようにカンマで区切って左側にそのパスを指定する。

$ docker run --name datavolume2 -i -v /tmp/mydata:/mydata -t centos:7 /bin/bash

上記では Docker ホスト上の /tmp/mydata を Docker コンテナの /mydata としてマウントする、という意味になる。

先ほどと同じように docker inspect でデータボリュームの情報を確認してみよう。

$ docker inspect datavolume2 | jq ".[0].Mounts"
[
  {
    "Source": "/tmp/mydata",
    "Destination": "/mydata",
    "Mode": "",
    "RW": true,
    "Propagation": "rprivate"
  }
]

この内容を見ても Docker ホストの /tmp/mydata が Docker コンテナの /mydata にマウントされたことが読み取れる。

先ほどと同じように Docker コンテナ側でファイルを /mydata においてみよう。

# cd /mydata/
# touch helloworld.txt

Docker ホスト側でマウントされたディレクトリをみるとファイルができていることがわかる。

$ ls /tmp/mydata
helloworld.txt

データボリュームコンテナ

次はデータボリュームコンテナという機能を試してみよう。 先ほどのデータボリュームは Docker ホスト上のファイルを Docker コンテナでマウントする機能だった。 データボリュームコンテナは、データボリュームをマウントした Docker コンテナから、別のコンテナがそのデータボリュームを使えるようにする機能だ。

まずはデータボリュームを使われる側のコンテナを用意する。 先ほどと同じように -v オプションでデータボリュームを追加しておこう。 名前は volumecontainer1 にしておく。 データボリュームは /sharedvolume に追加する。

$ docker run -d --name volumecontainer1 -v /sharedvolume -t centos:7 /bin/bash

docker inspect コマンドでデータボリュームが追加されていることを確認する。

$ docker inspect volumecontainer1 | jq ".[0].Mounts"
[
  {
    "Name": "fab265e09aae405baab9e4e5c36d0c4b0b77e9208b6d93610001562cc81fb457",
    "Source": "/mnt/sda1/var/lib/docker/volumes/fab265e09aae405baab9e4e5c36d0c4b0b77e9208b6d93610001562cc81fb457/_data",
    "Destination": "/sharedvolume",
    "Driver": "local",
    "Mode": "",
    "RW": true,
    "Propagation": ""
  }
]

次にデータボリュームを使う側のコンテナを作る。 このときはオプションとして --volumes-from で先ほどのコンテナの名前を指定しよう。

$ docker run --name vcuser1 --volumes-from volumecontainer1 -i -t centos:7 /bin/bash

コンテナに入ると /sharedvolume ディレクトリができている。

# find / -name sharedvolume
/sharedvolume

df の結果をみても /sharedvolume に /dev/sda1 がマウントされていることがわかる。

# df -a | grep sharedvolume
/dev/sda1       19049892 2294828  15764340  13% /sharedvolume

ディレクトリの中にファイルを作ってみよう。

# cd /sharedvolume/
# touch helloworld.txt

データボリュームコンテナは複数のコンテナから共有できる。 そこで、もう一台「使う側」のコンテナを起動してみよう。

$ docker run --name vcuser2 --volumes-from volumecontainer1 -i -t centos:7 /bin/bash

新しく起動したコンテナで /sharedvolume ディレクトリの中身を見ると、先ほど作ったファイルが見えている。

# ls /sharedvolume/
helloworld.txt

まとめ

今回は Docker のデータボリュームとデータボリュームコンテナという機能について見てきた。 コンテナは基本的にステートレスに作るのがベストプラクティスみたいだけど、どうしてもステートフルに作らなきゃいけないものはある。 例えば自前でデータベースを用意する場合なんかはその代表で、そこではどうしてもデータの永続化について考えなきゃいけない。 そんなときは今回見てきたデータボリュームとデータボリュームコンテナを使うと便利そうだ。

Python: PyPI にないパッケージを依存パッケージにするには

PyPI に無いパッケージを自作パッケージの依存ライブラリにしようとしたら色々と苦労したので、そこで得られた知見を共有しておく。 どうやら現状では setuptools と pip で対応方法が異なっているために、それぞれで微妙に異なるやり方を取る必要があるようだ。

今回使った環境は次の通り。

$ sw_vers 
ProductName:    Mac OS X
ProductVersion: 10.11.3
BuildVersion:   15D21
$ python --version
Python 3.5.1
$ pip list
pip (8.0.2)
setuptools (20.1.1)
wheel (0.24.0)

自作パッケージを用意する

まずは動作確認に使う自作パッケージを用意する。 名前は mypackage にしよう。 パッケージは枠だけ用意して中身は空っぽにする。 今回、パッケージ自体は関心の外にあるため。

$ mkdir mypackage
$ touch mypackage/__init__.py

次にパッケージに対してセットアップスクリプトを書く。 これでパッケージ mypackage がインストール可能になる。

$ cat << 'EOF' > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup


def main():
    setup(
        name='mypackage',
        version='0.0.1',
        zip_safe=False,
        packages=['mypackage'],
    )


if __name__ == '__main__':
    main()
EOF

早速セットアップスクリプトを使って今の環境に mypackage をインストールしよう。

$ python setup.py install

パッケージをインストールすると pip list サブコマンドで mypackage が見えるようになる。

$ pip list | grep mypackage
mypackage (0.0.1)

もちろん Python でインポートして使うこともできる。 まあ、中身は空っぽだけど。

$ python -c "import mypackage"

PyPI にない依存パッケージを追加する

さて、ここまででやっとスタートラインに立てた。 作成した mypackage に PyPI には置かれていない依存パッケージを追加してみよう。 追加するのは自分で作った以下のジョークパッケージにする。

github.com

PyPI にないパッケージを依存パッケージに追加するには次のドキュメントが参考になる。

Specifying Dependencies — Python Packaging Tutorial

通常であれば依存パッケージは setuptools.setup() 関数の install_requires 引数に名前を追加して終わりだ。 しかし、追加したいパッケージが PyPI に無いときは dependency_links の指定も必要になる。 ここにインストールに使うリンクを記述する。

$ cat << 'EOF' > setup.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from setuptools import setup


def main():
   setup(
       name='mypackage',
       version='0.0.1',
       zip_safe=False,
       packages=['mypackage'],
       install_requires=[
           'wehatemtg==0.0.1',
       ],
       dependency_links=[
           'git+https://github.com/momijiame/wehatemtg.git#egg=wehatemtg-0.0.1',
       ]
   )


if __name__ == '__main__':
   main()

EOF

セットアップスクリプトを修正したら、もう一度 mypackage をインストールし直そう。

$ python setup.py install

今度は依存パッケージの wehatemtg が一緒にインストールされるようになった。

$ pip list | grep wehatemtg
wehatemtg (0.0.1)

これで、セットアップスクリプトを使ったインストールで PyPI にないパッケージを依存パッケージに追加できるようになった。

pip で PyPI にない依存パッケージを扱う

めでたしめでたし、と行きたいところだけど残念ながらそうはいかない。 実は問題は pip を使ったときに発生する。

まずは一旦依存パッケージの wehatemtg をアンインストールしておこう。

$ pip uninstall -y wehatemtg

次に mypackage をソース配布物としてビルドする。

$ python setup.py sdist

できあがったソース配布物を pip コマンドでインストールしてみよう。

$ pip install dist/mypackage-0.0.1.tar.gz
Processing ./dist/mypackage-0.0.1.tar.gz
  Requirement already satisfied (use --upgrade to upgrade): mypackage==0.0.1 from file:///Users/amedama/Documents/temporary/package-not-on-pypi in /Users/amedama/.virtualenvs/temporary/lib/python3.5/site-packages
Collecting wehatemtg==0.0.1 (from mypackage==0.0.1)
  Could not find a version that satisfies the requirement wehatemtg==0.0.1 (from mypackage==0.0.1) (from versions: )
No matching distribution found for wehatemtg==0.0.1 (from mypackage==0.0.1)

見事にエラーになった。 wehatemtg をインストールしようとしたけど見つからないらしい。

何故エラーになるのだろうか? pip のソースコードを調べたところ原因が判明した。 次を見てもらいたい。

github.com

ここには次のようなコメントがある。

# We trust every url that the user has given us whether it was given
#   via --index-url or --find-links
# We explicitly do not trust links that came from dependency_links
# We want to filter out any thing which does not have a secure origin.

つまり pip はセットアップスクリプトの dependency_links を信用しないらしい。 PyPI にないパッケージをインストールするときは --index-url か --find-links オプションを明示的に指定する必要があるようだ。

ではそのコメントにしたがってオプションをつけて実行してみよう。

$ pip install dist/mypackage-0.0.1.tar.gz --find-links=git+https://github.com/momijiame/wehatemtg.git#egg=wehatemtg-0.0.1
...(省略)...
Successfully installed wehatemtg-0.0.1

今度は上手くいった。

$ pip list | grep wehatemtg
wehatemtg (0.0.1)

まとめ

今回は PyPI にないパッケージを依存パッケージにする方法について書いた。 基本的には setuptools.setup() 関数の dependency_links 引数にパッケージのインストール元 URL を記述すれば良い。 ただし、どうやら pip はそこに書いてある内容を信用しないのでコマンド実行時にオプションで渡してやる必要がある点に注意が必要だ。