ksaitoの日記

日々試したことの覚え書き

dockerのデータの永続化

dockerでデータを永続化するために--volumes-fromを使ってみました。

やりたいこと

dockerのオフシャルコンテナでmysqlやjenkinsを手軽に立ち上げることができます。

データについては、-vオプションでホストOSのディレクトリにマッピングしていたのですが、ファイルオーナやパーミッションの違いでアプリケーションが起動しなかったりするので--volumes-fromを使ってデータもコンテナ化してホストOSの依存をなくしたい。

単純なパターン

オフシャルドキュメントのManage data in containersを参考にします。

データコンテナの作成

データを保管するコンテナをdocker createコマンドで作成します。イメージは、busyboxを使って-vオプションでデータを保管するパスを指定します。

$ docker create -v /var/jenkins_home --name jenkins_test busybox /bin/true
a028f489ae189002030b8ecb7e96b371cf66ea005fcfcc694575f8ab6bb2832e
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
a028f489ae18        busybox             "/bin/true"         3 minutes ago       Created                                 jenkins_test
$ 

データの作成

次に一連のコマンドでコンテナを起動してファイルを作成し、コンテナを削除します。

$ docker run -it --rm --volumes-from jenkins_test ubuntu /bin/bash
root@12c4596d222f:/# cd /var/jenkins_home/
root@12c4596d222f:/var/jenkins_home# ls
root@12c4596d222f:/var/jenkins_home# touch data-saved
root@12c4596d222f:/var/jenkins_home# ls -l
total 0
-rw-r--r-- 1 root root 0 Apr  9 09:50 data-saved
root@12c4596d222f:/var/jenkins_home# exit
$ 

通常は、/var/jenkins_homeに作成したファイルは、--rmオプションを付けているのでコンテナ終了とともに削除されます。

先ほど作成したデータコンテナを--volumes-fromで指定しているのでファイルは、データコンテナに作成されます。

再度、同じコマンドで新しいコンテナを起動してファイルがあることを確認します。

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
a028f489ae18        busybox             "/bin/true"         11 minutes ago      Created                                 jenkins_test
$ docker run -it --rm --volumes-from jenkins_test ubuntu /bin/bash
root@560e5c11911d:/# ls /var/jenkins_home/
data-saved
root@560e5c11911d:/# 

データのバックアップ

オフシャルドキュメントには、コンテナを使ってデータをホストOSにtar.gzする方法が記載されています。

docker commitでimageを作る方法は、volumeのデータは対象外となるようです。

$ docker run -it --rm --volumes-from test ubuntu ls /var/jenkins_home
data-saved
$ docker commit test jenkins_test:3.0
c9ab350ba0dd011a6db95dd725edc43a7a0ceebaccc2c95ffbd6afbe1654bda5
$ docker create --name test3 jenkins_test:3.0
86e771c33d849d51d062ce5a63ab3e39765523f31cda6b4a705c70a6bc54b176
$ docker run -it --rm --volumes-from test3 ubuntu ls /var/jenkins_home
$ 

ドキュメント通りバックアップします。

$ docker run --rm --volumes-from test -v $(pwd):/backup busybox tar cfz /backup/backup.tar.gz /var/jenkins_home
tar: removing leading '/' from member names
$ tar tfz backup.tar.gz
var/jenkins_home/
var/jenkins_home/data-saved
$ 

そして、リストアしてみます。

$ docker run --rm --volumes-from test3 -v $(pwd):/backup ubuntu bash -c "cd /var/jenkins_home && tar xfz /backup/backup.tar.gz --strip 2"
$ docker run -it --rm --volumes-from test3 ubuntu ls /var/jenkins_home/jenkins_home
data-saved
$

おまけ

docker inspectでボリュームのデータを確認することができます。

$ docker inspect test | grep Source
            "Source": "/var/lib/docker/volumes/ef00bd2a22796753979728951c2e802fc2446befbad2f64f3a15be2353775774/_data",
$ sudo ls /var/lib/docker/volumes/ef00bd2a22796753979728951c2e802fc2446befbad2f64f3a15be2353775774/_data
data-saved
$ 

リストア後なので、同じデータがありますがチェックサムが異なるので別データとなっていることがわかります。

$ docker inspect test3 | grep Source
            "Source": "/var/lib/docker/volumes/dc93f38d20643b7aaaf02c554cfeb9786b3feee44ce4ac64430cbfaedbd1b140/_data",
$ sudo ls             "Source": "/var/lib/docker/volumes/d7c133c44da4b03dce4ef8cec8736e4ac4893d4c82ea9c7ddd16fcabe3c17801/_data",
$ sudo ls /var/lib/docker/volumes/d7c133c44da4b03dce4ef8cec8736e4ac4893d4c82ea9c7ddd16fcabe3c17801/_data
$ 

Makefileでできること

Makefileでよく使うパターンを検証しました。

検証用のファイルは、下記の通りです。

ソースがtest1ディレクトリの*.txtでtest2ディレクトリの*.objがターゲットファイルという想定です。

できるだけ、Makefileがシンプルになるように工夫してみました。

$ tree ./test?
./test1
├── a.txt
└── b.txt
./test2
└── a.obj

0 directories, 3 files
$ 

ワイルドカード

ワイルドカードで指定したファイルに対して操作をする場合、$(wildcard)を使います。

$(wildcard 正規表現)でソースファイルのリストを作ることができます。そのリストに対して$(SRC:.txt=.obj)でソースファイルの拡張子を変更したリストを作ることができます。

$ cat Makefile
SRC=$(wildcard test1/*.txt)
OBJ=$(SRC:.txt=.obj)

all:
        @echo src=$(SRC)
        @echo obj=$(OBJ)
$ make
src=test1/a.txt test1/b.txt
obj=test1/a.obj test1/b.obj
$ 

ルールの追加

*.txtから*.objを作るためのルールを追加します。

$ cat Makefile
SRC=$(wildcard test1/*.txt)
OBJ=$(SRC:.txt=.obj)

all: $(OBJ)

%.obj: %.txt
        @echo compile src=$< obj=$@
$ make
compile src=test1/a.txt obj=test1/a.obj
compile src=test1/b.txt obj=test1/b.obj
$ 

ターゲットファイルのディレクトリを変更

ソースがtest1ディレクトリ、ターゲットがtest2ディレクトリで拡張子のルールを使う場合、下記のようにターゲットをtest1ディレクトリに作成してtest2にコピーしました。

コピー先のファイル名を$(addprefix ./test2/, $(notdir $@))でディレクトリ部分のみ変更しています。

$ cat Makefile
SRC=$(wildcard test1/*.txt)
OBJ=$(SRC:.txt=.obj)

all: $(OBJ)

%.obj: %.txt
        @echo compile src=$< obj=$@
        @echo cp -p $@ $(addprefix ./test2/, $(notdir $@))
$ make
compile src=test1/a.txt obj=test1/a.obj
cp -p test1/a.obj ./test2/a.obj
compile src=test1/b.txt obj=test1/b.obj
cp -p test1/b.obj ./test2/b.obj
$ 

仕上げ

echoを実際のコマンドに置き換えて動作をチェックします。 cleanターゲットも追加して動作を確認します。

$ cat Makefile
SRC=$(wildcard test1/*.txt)
OBJ=$(SRC:.txt=.obj)

all: $(OBJ)

%.obj: %.txt
        @echo compile src=$< obj=$@
        touch $@
        cp -p $@ $(addprefix ./test2/, $(notdir $@))

clean:
        -@rm -rf $(OBJ)
$ make -n
echo compile src=test1/a.txt obj=test1/a.obj
touch test1/a.obj
cp -p test1/a.obj ./test2/a.obj
echo compile src=test1/b.txt obj=test1/b.obj
touch test1/b.obj
cp -p test1/b.obj ./test2/b.obj
$ make 
compile src=test1/a.txt obj=test1/a.obj
touch test1/a.obj
cp -p test1/a.obj ./test2/a.obj
compile src=test1/b.txt obj=test1/b.obj
touch test1/b.obj
cp -p test1/b.obj ./test2/b.obj
$ make
make: Nothing to be done for 'all'.
$ make clean
$ make
compile src=test1/a.txt obj=test1/a.obj
touch test1/a.obj
cp -p test1/a.obj ./test2/a.obj
compile src=test1/b.txt obj=test1/b.obj
touch test1/b.obj
cp -p test1/b.obj ./test2/b.obj
$ 

本当は、ターゲットのtest2/*.objtest1/*.txtのタイムスタンプを比較して、test1/*.txtから直接test2/*.objを作りたです。

packerを使う

公開されているboxを使うのは、ちょっと抵抗があるのでpackerを使ってvagrant boxを作りました。

何ができるのか

packerでいろいろなことができるようですが、使っているのは下記です。

  • OSインストール用のISOファイルのダウンロード
  • VirtualBoxVM作成
  • OSのインストール
  • デフォルトユーザの作成
  • vagrant boxの作成

packerにすることで半年に一度のubuntuアップグレードがバージョン番号を変更とISOイメージのチェックサムを変更+packer実行で完了します。

packerのソース

githububuntu 15.10のboxを作るソースをアップしています。

buildersでISOファイルの場所やインストールの操作を書きます。

post-processorsのtypeで作成するタイプを複数指定できます。

使っているのはvagrant boxとatlasの2つですが、AWSのAMIも作れるようです。

atlasとの連携

HashiCorpにatlasのアカウントを作るとローカルPCにVirtualBoxがなくてもOSイメージを作成することができます。

おおよそ下記の手順です。以前に見た時よりメニューはだいぶ変わっていますが...

  • GitHubにアカウントを作りpacker用のリポジトリを作る
  • atlasのvagrant enterpriseにboxを作る
  • altasのpackerにconfigurationを作る

configurationにソースとなるGitHubリポジトリ、ブランチ、ディレクトリ、ファイル名を指定、出力先となるvagrant enterpriseのboxを選択しておくと、GitHubのコミットをトリガにboxが作成されます。

packerで前提となるパッケージのインストールと設定まで行えばvagrantAWSなどメジャーな環境のVMイメージを一気に作ることができるようです。

packerのインストール

MacBookにpackerをインストールしました。

ダウンロード

packerのダウンロードページからOS X 64bit用のバイナリをダウンロードします。

インストール

zip形式なので/usr/local/binに展開したらインストール完了です。

$ packer --version
0.10.0
$ 

ansibleのdocker connection plugin

ansibleのdocker connection plugを使ってみました。

環境

ansibleとdockerのバージョンは、下記の通りです。

$ ansible --version
ansible 2.0.1.0
  config file = /home/vagrant/.ansible.cfg
  configured module search path = Default w/o overrides
(0)21:07:12 vagrant@vivid64$ 
(0)21:07:12 vagrant@vivid64$ docker version
Client:
 Version:      1.9.1
 API version:  1.21
 Go version:   go1.4.2
 Git commit:   a34a1d5
 Built:        Fri Nov 20 13:16:54 UTC 2015
 OS/Arch:      linux/amd64

Server:
 Version:      1.9.1
 API version:  1.21
 Go version:   go1.4.2
 Git commit:   a34a1d5
 Built:        Fri Nov 20 13:16:54 UTC 2015
 OS/Arch:      linux/amd64
$ 

最初は、手動でコンテナを起動します。 コンテナ作成時に--nameオプションで名前を指定します。ansibleは、この名前でコンテナを識別します。

$ docker run --name target -td ubuntu:14.04 bash
909d3137c6d28f02ed261bba2974dc2ed1d50a7313a2843a6da2cec09ab8f61b
(0)20:54:49 vagrant@vivid64$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
909d3137c6d2        ubuntu:14.04        "bash"              3 seconds ago       Up 3 seconds                            target
$

docker connection pluginは、python必須なのでコンテナにpythonをインストールします。

$ docker exec target "apt-get install -y python"

疎通確認

ansibleコマンドで疎通を確認します。

-c dockerを指定することで通常sshで接続するところをdocker execでコマンドを実行します。

$ ansible -i "target," target -c docker -a hostname
target | SUCCESS | rc=0 >>
909d3137c6d2

$ 

playbookの準備

下記のようなplaybookを準備します。

$ cat sample.yml 
---
- name: start up docker container
  hosts: localhost
  tasks:
    - add_host: name=target

- name: configure container
  hosts: target
  connection: docker
  tasks:
    - command: id
      register: id_result
    - debug: var=id_result.stdout
      when: id_result | success
$ 

実行

$ ansible-playbook -i "localhost," sample.yml 

PLAY [start up docker container] ***********************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [add_host] ****************************************************************
changed: [localhost]

PLAY [configure container] *****************************************************

TASK [setup] *******************************************************************
ok: [target]

TASK [command] *****************************************************************
changed: [target]

TASK [debug] *******************************************************************
ok: [target] => {
    "id_result.stdout": "uid=0(root) gid=0(root) groups=0(root)"
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0   
target                     : ok=3    changed=1    unreachable=0    failed=0   

$ 

ansibleで一対多の設定をする

設定する対象がデータベースやアプリケーションサーバの場合、「一つのインスタンス複数のアプリケーションをデプロイ」したり、「一つのアプリケーションに複数のデータソースを追加」といった一対多の設定はよくあります。

ansibleで設定を行う場合に、

  • 設定ファイルを肥大化を防止
  • 必要最小限の値を設定
  • 設定と構造の関係を把握することが容易

となるようなパターンを検討してみました。

より良い方法があるかもしれませんが、ひとまずこれで動いています。

設定:構造を定義

設定は、``./host_vars/[対象サーバ名]を起点にします。こうすることでansibleがインベントリから対象サーバを特定した際に自動的に設定が読み込まれます。

$ cat host_vars/appserv1
serverName: app-server1

tomcat:
  - appName: inst1
    db:
      - dba
  - appName: inst2
    db:
      - db1
      - db2
$ 

この例では、tomcatという変数にinst1inst2という2つのアプリケーションがあり、inst1は、dbaというデータベース接続を、inst2は、db1db2の2つのデータベース接続を設定します。

設定:各オブジェクトのパラメータ定義

各オブジェクトのパラメータ定義は、./varsにオブジェクト毎に設定します。

アプリケーションのパラメータは、./vars/inst1.yml./vars/inst2.ymlに設定します。 各アプリケーションに関連するデータベースの設定は、./vars/[アプリ名][db名].ymlに設定します。

今回の例ですと、./vars/inst1dba.yml./vars/inst2db1.yml./vars/inst2db2.ymlに設定することになります。

$ ls vars/inst[12]*.yml | cat
vars/inst1.yml
vars/inst1dba.yml
vars/inst2.yml
vars/inst2db1.yml
vars/inst2db2.yml
$ 

アプリケーションの設定は、下記のようにappNameを定義します。

$ for i in `ls ./vars/inst[12].yml`; do echo $i; cat $i; done
./vars/inst1.yml
appName: inst1
./vars/inst2.yml
appName: inst2
$ 

データベースの設定は、下記とします。

$ for i in `ls ./vars/inst[12]db*.yml`; do echo $i; cat $i; done
./vars/inst1dba.yml
schemaName: inst1dba
./vars/inst2db1.yml
schemaName: inst2db1
./vars/inst2db2.yml
schemaName: inst2db2
$ 

roleの定義

パラメータを読み込んで設定するroleを作成するために下記のようなファイル一式を作ります。host_varsvarsは、上記で設定したファイルです。

$ tree
.
├── host_vars
│   ├── appserv1
│   └── appserv2
├── hosts
├── roles
│   └── app
│       └── tasks
│           ├── db.yml
│           ├── main.yml
│           └── tomcat.yml
├── site.yml
└── vars
    ├── inst1.yml
    ├── inst1dba.yml
    ├── inst2.yml
    ├── inst2db1.yml
    └── inst2db2.yml
$ 

roles/app/tasks/main.ymlでアプリケーションの設定と、それに紐づくデータベースの設定をするためのタスクを定義します。

$ cat roles/app/tasks/main.yml
---
- include: tomcat.yml
  with_items: "{{tomcat}}"
- include: db.yml
  with_subelements:
    - tomcat
    - db
$ 

一つ目のタスクは、アプリケーション設定をするtomcat.ymlというファイルをインクルードします。 ./host_avrs/[サーバ名]で定義したtomcatという変数に定義したアプリケーションの回数繰り返します。

二つ目のタスクは、アプリケーションに紐付いたデータベースの設定をするdb.ymlというファイルをインクルードします。 本当は、インスタンスの設定に入れ子したかったのですが、with_itemsで使われるitemという変数が入れ子の中では設定できず諦めました。(誰かうまい方法教えて)

いろいろ調べた結果、with_subelementstomcat変数の配列の一つを取り出し、appName+dbの組み合わせをdb.ymlに渡しています。

それぞれの処理

サンプルなので設定されたパラメータを読み込み表示するだけの処理です。

アプリケーションの設定は、以下の通りです。

$ cat roles/app/tasks/tomcat.yml 
---
- include_vars: "{{item.appName}}.yml"
- debug: msg="Tomcat application Name={{appName}}"
$ 

DBの設定は、以下の通りです。

$ cat roles/app/tasks/db.yml
---
- include_vars: "{{item.0.appName}}{{item.1}}.yml"
- debug: msg="{{item.0.appName}} application dbName={{schemaName}}"
$ 

どちらも、最初にinclude_vars./varsディレクトリ下の所定の定義ファイルのパラメータを読み込み、表示します。

実行

インベントリにとりあえず1台サーバを追加します。 appserv2も追加して、./host_vars/appserv1./host_vars/appserv2とにコピーすると、全くおなじ設定のサーバを作ることができます。 もちろん、./vars/inst1*.yml一式をコピーして個別の設定にすることも可能です。

[appservers]
appserv1
#appserv2
$ 

実行すると、こんな感じです。

アプリケーションの名前やDB接続先など、設定が必要な固有の値をtemplateで埋め込んだりtaskの中で参照するといった使い方になります。

$ ansible-playbook -i hosts site.yml
  
PLAY ***************************************************************************
  
TASK [setup] *******************************************************************
ok: [appserv1]

TASK [app : include] ***********************************************************
included: /home/vagrant/git/sample/roles/app/tasks/tomcat.yml for appserv1
included: /home/vagrant/git/sample/roles/app/tasks/tomcat.yml for appserv1

TASK [app : include_vars] ******************************************************
ok: [appserv1]

TASK [app : debug] *************************************************************
ok: [appserv1] => {
    "msg": "Tomcat application Name=inst1"
}

TASK [app : include_vars] ******************************************************
ok: [appserv1]

TASK [app : debug] *************************************************************
ok: [appserv1] => {
    "msg": "Tomcat application Name=inst2"
}

TASK [app : include] ***********************************************************
included: /home/vagrant/git/sample/roles/app/tasks/db.yml for appserv1
included: /home/vagrant/git/sample/roles/app/tasks/db.yml for appserv1
included: /home/vagrant/git/sample/roles/app/tasks/db.yml for appserv1

TASK [app : include_vars] ******************************************************
ok: [appserv1]

TASK [app : debug] *************************************************************
ok: [appserv1] => {
    "msg": "inst1 application dbName=inst1dba"
}


TASK [app : include_vars] ******************************************************
ok: [appserv1]

TASK [app : debug] *************************************************************
ok: [appserv1] => {
    "msg": "inst2 application dbName=inst2db1"
}

TASK [app : include_vars] ******************************************************
ok: [appserv1]

TASK [app : debug] *************************************************************
ok: [appserv1] => {
    "msg": "inst2 application dbName=inst2db2"
}
  
PLAY RECAP *********************************************************************
appserv1                   : ok=16   changed=0    unreachable=0    failed=0
  
$ 

ansibleのコアモジュール

Developing Modulesを参考にansibleのcoreモジュールを弄ってみました。

環境準備

オフシャルドキュメントのチュートリアルにテスト方法とかんたんなモジュールの作り方が書かれています。

下記でテスト環境を準備します。

$ git clone git://github.com/ansible/ansible.git --recursive
$ source ansible/hacking/env-setup
$ chmod +x ansible/hacking/test-module

pingモジュール

pingモジュールは、lib/ansible/modules/core/system/ping.pyにあります。

普通に使うとこんな感じ

$ ansible -m ping localhost
localhost | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
$ 

戻り値のpongpong!!に修正します。 gitのsubmoduleになっているようです。

$ git diff | cat
diff --git a/lib/ansible/modules/core b/lib/ansible/modules/core
--- a/lib/ansible/modules/core
+++ b/lib/ansible/modules/core
@@ -1 +1 @@
-Subproject commit 45367c3d090ccf4d649b103b50b6eec939b6ee14
+Subproject commit 45367c3d090ccf4d649b103b50b6eec939b6ee14-dirty
$ cd lib/ansible/modules/core/system
$ git --no-pager diff
diff --git a/system/ping.py b/system/ping.py
index ed93f7d..9f8ddf6 100644
--- a/system/ping.py
+++ b/system/ping.py
@@ -49,7 +49,7 @@ def main():
         ),
         supports_check_mode = True
     )
-    result = dict(ping='pong')
+    result = dict(ping='pong!!')
     if module.params['data']:
         if module.params['data'] == 'crash':
             raise exceptions.Exception("boom")
$ 

修正後に実行するとこんな感じ

$ ansible -m ping localhost
localhost | SUCCESS => {
    "changed": false, 
    "ping": "pong!!"
}
$ 

command/shellモジュール

command/shellモジュールは、lib/ansible/modules/core/commandsにあります。

実行するとこんな感じ

$ ansible localhost -m shell -a "/bin/ls"
localhost | SUCCESS | rc=0 >>
__init__.py
command.py
raw.py
script.py
shell.py

$ ansible localhost -m command -a "/bin/ls"
localhost | SUCCESS | rc=0 >>
__init__.py
command.py
raw.py
script.py
shell.py

$ 

test-moduleで実行するには、こんな感じで実行します。

$ $ANSIBLE_HOME/hacking/test-module -m command.py -a "/bin/ls"
* including generated source, if any, saving to: /home/vagrant/.ansible_module_generated
* this may offset any line numbers in tracebacks/debuggers!
***********************************
RAW OUTPUT
{"changed": true, "end": "2016-03-03 18:31:12.148437", "stdout": "__init__.py\ncommand.py\nraw.py\nscript.py\nshell.py", "cmd": ["/bin/ls"], "rc": 0, "start": "2016-03-03 18:31:12.145253", "stderr": "", "delta": "0:00:00.003184", "invocation": {"module_args": {"warn": true, "executable": null, "chdir": null, "_raw_params": "/bin/ls", "removes": null, "creates": null, "_uses_shell": false}}, "warnings": []}


***********************************
PARSED OUTPUT
{
    "changed": true, 
    "cmd": [
        "/bin/ls"
    ], 
    "delta": "0:00:00.003184", 
    "end": "2016-03-03 18:31:12.148437", 
    "invocation": {
        "module_args": {
            "_raw_params": "/bin/ls", 
            "_uses_shell": false, 
            "chdir": null, 
            "creates": null, 
            "executable": null, 
            "removes": null, 
            "warn": true
        }
    }, 
    "rc": 0, 
    "start": "2016-03-03 18:31:12.145253", 
    "stderr": "", 
    "stdout": "__init__.py\ncommand.py\nraw.py\nscript.py\nshell.py", 
    "warnings": []
}
$ 

デバッガを使うには、下記のようにします。

デバッガは、Python付属のpdbで開発用にクローンしてtest-moduleで生成・実行されたソースをデバッグすることができます。

$ pdb $ANSIBLE_HOME/hacking/test-module -m command.py -a "/bin/ls" --debugger /usr/bin/pdb
> /home/vagrant/git/module-dev/ansible/hacking/test-module(32)<module>()
-> import sys
(Pdb) q
$ $ANSIBLE_HOME/hacking/test-module -m command.py -a "/bin/ls" --debugger /usr/bin/pdb
* including generated source, if any, saving to: /home/vagrant/.ansible_module_generated
* this may offset any line numbers in tracebacks/debuggers!
> /home/vagrant/.ansible_module_generated(22)<module>()
-> import copy
(Pdb) list
 17     # GNU General Public License for more details.
 18     #
 19     # You should have received a copy of the GNU General Public License
 20     # along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
 21
 22  -> import copy
 23     import sys
 24     import datetime
 25     import glob
 26     import traceback
 27     import re
(Pdb)