ksaitoの日記

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

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)

zabbix3.0のインストール

2/16にzabbix3.0がリリースされました。 Ubuntu 14.04 LTSにインストールしました。

インストール

オフシャルドキュメントの手順でaptパッケージをインストール後にブラウザでアクセスします。

設定変更は、下記のphpのtimezoneの一箇所だけです。

root@trusty64:~# diff -u zabbix.conf /etc/apache2/conf-enabled/zabbix.conf
--- zabbix.conf 2016-02-28 13:34:28.192211335 +0900
+++ /etc/apache2/conf-enabled/zabbix.conf   2016-02-28 13:39:27.037559358 +0900
@@ -16,7 +16,7 @@
         php_value upload_max_filesize 2M
         php_value max_input_time 300
         php_value always_populate_raw_post_data -1
-        # php_value date.timezone Europe/Riga
+        php_value date.timezone Asia/Tokyo
     </IfModule>
 </Directory>
 
root@trusty64:~# 

ブラウザでアクセスするとインストール画面が表示されます。 画面に従って進めるとインストール完了です。

f:id:ksaito11:20160228135705p:plain

日本語設定

初期画面は、英語設定です。

f:id:ksaito11:20160228140101p:plain

右上の人の形のアイコンを選択して、LanguageでJapaneseを選択します。日本語は、デフォルトでインストールされているので選択するだけです。

f:id:ksaito11:20160228140137p:plain

ダッシュボードは、こんな感じになります。 だいぶイメージが変わって今風になってます。

f:id:ksaito11:20160228140347p:plain

フォント設定

グラフ凡例の日本語は、相変わらず文字化けします。

f:id:ksaito11:20160228140620p:plain

IPAフォントをインストールします。

root@trusty64:~# sudo apt-get install fonts-ipaexfont-gothic

フォントを使えるようにします。

root@trusty64:~# update-alternatives --install "/usr/share/zabbix/fonts/graphfont.ttf" "zabbix-frontend-font" "/usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf" 100
root@trusty64:~# update-alternatives --config zabbix-frontend-font
alternative zabbix-frontend-font (/usr/share/zabbix/fonts/graphfont.ttf を提供) には 2 個の選択肢があります。

  選択肢    パス                                                 優先度  状態
------------------------------------------------------------
* 0            /usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf   100       自動モード
  1            /usr/share/fonts/opentype/ipaexfont-gothic/ipaexg.ttf   100       手動モード
  2            /usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf     10        手動モード

現在の選択 [*] を保持するには Enter、さもなければ選択肢の番号のキーを押してください: 
root@trusty64:~# 

これで凡例も日本語になりました。

f:id:ksaito11:20160228142748p:plain

ansibleモジュールの引数と戻り値の処理

ansibleモジュールは、pythonで書くと引数と戻り値に便利なヘルパがあり便利です。

AnsibleModuleargument_specで引数を定義します。引数が必須かオプションかを定義できるようです。

戻り値は、exit_jsonで返します。最後の2行は、python初心者なのでおまじないということで...

$ cat library/mymodule.py
#! /usr/bin/python

def main():
    module = AnsibleModule(
        argument_spec = dict(
            src  = dict(required=True),
            dest = dict(required=False),
        ),
    )
    src  = module.params['src']
    dest = module.params['dest']
    res_args = dict(
        src = src, dest = dest, changed = True
    )
    module.exit_json(**res_args)

# import module snippets
from ansible.module_utils.basic import *
main()
$ 

実行

テスト実行します。前に書いたサンプルより一貫性があって分かりやすい実装です。

$ $ANSIBLE_HOME/hacking/test-module -m library/mymodule.py -a "src=bbb"
* including generated source, if any, saving to: /home/vagrant/.ansible_module_generated
* this may offset any line numbers in tracebacks/debuggers!
***********************************
RAW OUTPUT
{"dest": null, "src": "bbb", "changed": true, "invocation": {"module_args": {"dest": null, "src": "bbb"}}}


***********************************
PARSED OUTPUT
{
    "changed": true,
    "dest": null,
    "invocation": {
        "module_args": {
            "dest": null,
            "src": "bbb"
        }
    },
    "src": "bbb"
}
$ 

必須オプションが指定されないと、failedとエラーメッセージが戻り値に設定されます。これは便利です。

$ $ANSIBLE_HOME/hacking/test-module -m library/mymodule.py -a "dest=bbb"
* including generated source, if any, saving to: /home/vagrant/.ansible_module_generated
* this may offset any line numbers in tracebacks/debuggers!
***********************************
RAW OUTPUT
{"msg": "missing required arguments: src", "failed": true, "invocation": {"module_args": {"dest": "bbb"}}}


***********************************
PARSED OUTPUT
{
    "failed": true,
    "invocation": {
        "module_args": {
            "dest": "bbb"
        }
    },
    "msg": "missing required arguments: src"
}
$ 

ansible Moduleの作り方2

オフシャルドキュメントを参考にテスト方法と引数の取り方を試しました。

テスト

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

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

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

これでansibleの最新ソース一式をクローンしてローカルのソースでモジュールの開発とテストをする環境が準備できます。

結果を返すだけのシンプルなモジュールをpythonで書きます。

$ cat mymodule.py 
#! /usr/bin/python

import json

print json.dumps({
    "rc": 0,
    "changed": True,
})
$ 

テストします。

$ $ANSIBLE_HOME/hacking/test-module -m mymodule.py
* 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, "rc": 0}


***********************************
PARSED OUTPUT
{
    "changed": true, 
    "rc": 0
}
$ 

引数

ansibleの引数がどのように処理されている確認してみます。

先ほどのソースに、チュートリアルにある引数の処理を追加・実行して確認してみます。

 cat mymodule.py 
#! /usr/bin/python

import json
import sys

args_file = sys.argv[1]

print json.dumps({
    "rc": 0,
    "changed": True,
    "args": args_file
})
$ $ANSIBLE_HOME/hacking/test-module -m mymodule.py -a "arg1=aaa arg2=bbb"
* 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, "args": "/home/vagrant/.ansible_test_module_arguments", "rc": 0}


***********************************
PARSED OUTPUT
{
    "args": "/home/vagrant/.ansible_test_module_arguments", 
    "changed": true, 
    "rc": 0
}
$ 

引数は、-aオプションで"key1=値 key2=値..."で指定します。

sys.argv[1]に引数が書かれたファイルパスがあり、このファイルから引数を読むようです。

$ cat /home/vagrant/.ansible_test_module_arguments 
arg1=aaa arg2=bbb$

ファイルに書かれた引数は、文字列処理として取り込みます。

shlexというのをインポートして引数ごとに分解して=splitしてキーと値を取り出す。 引数を処理するヘルパーのようなものがあったほうが便利そうですが、処理としてはわかりやすい。

$ cat mymodule.py
#! /usr/bin/python

import json
import sys
import shlex

args_file = sys.argv[1]
args_data = file(args_file).read()
args = shlex.split(args_data)
for arg in args:
    if "=" in arg:
        (key, value) = arg.split("=")

print json.dumps({
    "rc": 0,
    "changed": True,
    "args": args_file,
    "key": key,
    "value": value,
})
$ 

実行するとこんな感じ(エラー処理なし、引数複数あっても最後のものしか取りません。サンプルなので)

$ $ANSIBLE_HOME/hacking/test-module -m mymodule.py -a "arg1=aaa arg2=bbb"
* including generated source, if any, saving to: /home/vagrant/.ansible_module_generated
* this may offset any line numbers in tracebacks/debuggers!
***********************************
RAW OUTPUT
{"value": "bbb", "changed": true, "key": "arg2", "args": "/home/vagrant/.ansible_test_module_arguments", "rc": 0}


***********************************
PARSED OUTPUT
{
    "args": "/home/vagrant/.ansible_test_module_arguments",
    "changed": true,
    "key": "arg2",
    "rc": 0,
    "value": "bbb"
}
$ 

ansible_facts

モジュールからansible_factsに値を渡すことができると書いてあるので試してみます。

チュートリアルによると、モジュールの戻り値のjsonansible_factsを返すと、そのあとのモジュールからansible_factsとして参照できるようになります。

モジュール

こんな感じでtestvalueという変数を定義(=json戻り値に含める)します。

$ cat library/mymodule.py
#! /usr/bin/python

import json

print json.dumps({
    "rc": 0,
    "changed": True,
    "ansible_facts" : {
        "testvalue" : "mymodule.py value",
    }
})
$ python library/mymodule.py
{"changed": true, "ansible_facts": {"testvalue": "mymodule.py value"}, "rc": 0}
$ 

モジュールとして実行

モジュールを呼び出した後続の処理では、"{{testvalue}}``で設定した変数を参照することができます。

$ cat site.yml
---
- hosts: localhost
  tasks:
    - mymodule:
    - debug: msg="{{ testvalue }}"
$ ansible-playbook -i "localhost," site.yml

PLAY [localhost] ***************************************************************

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

TASK [mymodule] ****************************************************************
changed: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "mymodule.py value"
}

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

$