Ansible dynamic inventory + Zabbix APIで監視対象を動的に構成管理


## Overview

普段Ansibleを使っているがinventory管理が思いのほか大変で、ホスト追加の度にinventoryファイルを編集とかしてた。
が、 **dynamic inventory** というものがあるのを最近知ったので使ってみる。
簡単にいうと、動的にAnsibleのinventoryホストを生成することが出来る機能で、一々マシンの追加の度にinventoryファイルを編集とかしなくていい。人間に優しい。
あと既にサーバ監視にZabbixを使っていたのでZabbixのホスト一覧からinventoryを生成し、Zabbix管理下のホストを構成管理ということもしてみる。

## Requirements

- Ansible (Tested on 1.8.1)
- Zabbix (Tested on 2.0.8)
- CentOS (Teted on 6.5)


## Installation

dynamic inventoryを使うには`-i`オプションにansibleが認識可能なJSON形式の結果を吐く実行可能なスクリプトを渡してやればいい。
形式さえ合ってればいいのでシェルスクリプトでもrubyでも何でも好きな言語で書ける。

例えばローカルの`/etc/hosts`に存在するホスト名をinventoryとして使いたければ

```inventory.sh
#!/bin/bash
HOSTS=$(cat /etc/hosts | grep -v ^# | awk '{print $2}')
HOSTS=$(echo "$HOSTS" | sed -e 's/^\(.*\)$/"\1"/g')
HOSTS=$(echo $HOSTS | sed -e 's/\s/,/g')
echo "{\"all\": {\"hosts\": [$HOSTS]}}"
```

のようなスクリプトを書いて渡してやるだけでいい。

ansibleで使うには

```bash
$ ansible -i inventory.sh all -m ping
```

のようにする。

さらに`/etc/ansible/hosts`にスクリプトを設置すればいちいち`-i`オプションを使う必要もなくなる。

```bash
$ cp inventory.sh /etc/ansible/hosts && chmod +x /etc/ansible/hosts

$ ansible all -m ping

web1 | success >> {
    "changed": false, 
    "ping": "pong"
}

web2 | success >> {
    "changed": false, 
    "ping": "pong"
}

...(中略)...

```

素晴らしい。

JSONの形式は以下のような形式ならなんでもいい。

```json
{
    "databases"   : {
        "hosts"   : [ "host1.example.com", "host2.example.com" ],
        "vars"    : {
            "a"   : true
        }
    },
    "webservers"  : [ "host2.example.com", "host3.example.com" ],
    "atlanta"     : {
        "hosts"   : [ "host1.example.com", "host4.example.com", "host5.example.com" ],
        "vars"    : {
            "b"   : false
        },
        "children": [ "marietta", "5points" ]
    },
    "marietta"    : [ "host6.example.com" ],
    "5points"     : [ "host7.example.com" ],
    "_meta" : {
       "hostvars" : {
          "host1.example.com"     : { 
             "asdf" : 1234,
             "ansible_ssh_host": "host1.example.com" 
           },
          "host2.example.com"     : { 
             "asdf" : 5678,
             "ansible_ssh_host": "host2.example.com" 
           }
       }
    }
}
```

`_meta`内にはhostvarsを入れられるのでhostごとに異なる情報があればここに入れておく。(`ansible_ssh_port`, `ansible_ssh_user`, `ansible_ssh_host`等)


[公式ドキュメント: Developing Dynamic Inventory Sources — Ansible Documentation](http://docs.ansible.com/developing_inventory.html)

## Zabbix連携

Ansibleが既に公式で配布しているZabbix用の[dynamic inventoryスクリプト](https://github.com/ansible/ansible/tree/devel/plugins/inventory)があるのでこれを使います。


使ってみたがそのままでは動作しなかったのと、デフォルトではホストグループ単位でのグループ化しか出来なかったので少し修正して、
Zabbixのテンプレート単位でのinventory化も出来るようにします。
修正後のファイルは[Gistに公開してあります](https://gist.github.com/shufo/9eea1fd8120aa1ea67c7)。

```diff
@@ -71,19 +71,41 @@ class ZabbixInventory(object):
             'hosts': []
         }
 
+    def templatestub(self):
+        return {
+            'hosts': []
+        }
+
     def get_host(self, api, name):
         data = {}
         return data
 
     def get_list(self, api):
-        hostsData = api.host.get({'output': 'extend', 'selectGroups': 'extend'})
-
+        hostsData = api.host.get(
+          {
+            'output': 'extend', 
+            'selectGroups': 'extend', 
+            'selectInterfaces': 'extend',
+            'selectInventory': 'extend'
+          }
+        )
+        templateData = api.template.get(
+          {
+            'output': 'extend', 
+            'selectGroups': 'extend', 
+            'selectHosts': 'extend',
+          }
+        ) 
         data = {}
         data[self.defaultgroup] = self.hoststub()
+        data['_meta'] = {}
+        data['_meta']['hostvars'] = {}
 
         for host in hostsData:
             hostname = host['name']
             data[self.defaultgroup]['hosts'].append(hostname)
+            data['_meta']['hostvars'][hostname] = host
+            data['_meta']['hostvars'][hostname]['ansible_ssh_host'] = hostname
 
             for group in host['groups']:
                 groupname = group['name']
@@ -93,6 +115,18 @@ class ZabbixInventory(object):
 
                 data[groupname]['hosts'].append(hostname)
 
+        for template in templateData:
+            templatename = template['name']
+            
+            for host in template['hosts']:
+                hostname = host['name']
+
+                if not templatename in data:
+                    data[templatename] = self.templatestub()
+

```

- 修正したものをansibleのconfディレクトリにコピー

```bash
cp zabbix.py /etc/ansible/hosts
chmod +x /etc/ansible/hosts
cp zabbix.ini /etc/ansible/
```

- zabbixのpythonクライアントをインストール

```
pip install zabbix-api
```

- 確認

```bash
# ホストグループベースのansible実行
$ ansible webservers -m ping

web1 | success >> {
    "changed": false, 
    "ping": "pong"
}

web2 | success >> {
    "changed": false, 
    "ping": "pong"
}

...(中略)...

# テンプレートベースのansible実行
$ ansible "Template OS Linux" -m ping

web1 | success >> {
    "changed": false, 
    "ping": "pong"
}

web2 | success >> {
    "changed": false, 
    "ping": "pong"
}

...(中略)...
```

これでZabbix上のホストグループベースでのinventoryの生成と、テンプレートベースでのinventoryの生成が出来ました。
テンプレートベースでの構成管理が出来るので、テンプレートと密接に関わるUserParameterやLow Level Discoveryの構成管理は楽になると思います。

またスクリプトのZabbix APIのhost.getを叩いてる箇所に

```
'selectInventory': 'extend'
```

をパラメータに追加してホストインベントリも取得しているので
Zabbixでラック管理やシリアル管理、タグ管理等の資産管理をしている場合は
hostvarsを見て特定のラックのみ更新、特定のタグのマシンのみ更新とか出来ます。
他にも[Zabbix API](https://www.zabbix.com/documentation/2.0/manual/appendix/api/api)次第でinventoryの生成方法を色々工夫出来ると思います。


あとはZabbixのディスカバリチェックで監視対象のホストをホストグループに追加したり
テンプレートにリンクさせたりすれば動的にinventoryを増やしたり減らしたり出来ます。

dynamic inventory用のスクリプト自体は公式が配布しているものを少し修正すれば
十分実用的になるので各々の環境に合わせたdynamic inventoryプラグインを作るのがよいかと思います。