Ansibleの対象サーバーをMackerelからDynamic Inventoryで取得する

# Mackerel?

Mackerel とは、はてながβテストを開始したサーバー管理ツールです。印象としてはZabbix（メトリックをユーザー自身が自由に定義できる）とNew Relic（データをカジュアルに表示してくれる）のいいとこ取りです。

【ベータテスター募集中】Mackerel: 新しいアプリケーションパフォーマンスマネジメント
https://mackerel.io/

![mackerel.png](https://qiita-image-store.s3.amazonaws.com/0/43082/33367c83-ff09-c999-eaf3-a5c1ad52714d.png)

特徴的なのは、サーバーを「Service」と「Role」というツリー状の役割として定義し、それを Capistrano や Chef、Ansible などの対象サーバーにできるということが挙げられます。例えばAnsibleのサーバー一覧を定義いている Inventory ファイルを git などで管理していると、サーバーの増減が発生したときにファイル修正が必要になりますが、Mackerel に登録しているサーバー一覧をAPI経由で取り出せばサーバー一覧の管理を Mackerel に任せられるというメリットが得られます。

公式サイトのヘルプには Capistrano との連携の仕方は書いてありましたが、Ansible については特に書かれていなかったので Ansible のプレイブックの対象を Mackerel から動的に取得する Dynamic Inventory スクリプトを書いてみました。

# Ansible: Dynamic Inventory

Ansibleにはグループという概念がありますので、Mackerelのロール=Ansibleのグループ、ということにしたいと思います。

Dynamic Inventory のスクリプトは好きな言語で書けますが、Ansible が前提としている Python を利用して書くほうがいいでしょう。

Dynamic Inventori スクリプトの要件は Ansible ドキュメントの Developing Dynamic Inventory Sources のページ通りです。

Developing Dynamic Inventory Sources — Ansible Documentation http://docs.ansible.com/developing_inventory.html

ポイントとしては

* `--list` が引数に渡されたら、ホスト一覧をJSONで表示する
* `--host <hostname>` が引数に渡されたら、指定されたホストのvariablesをJSONで表示する

です。

module などは決まったディレクトリに入れる必要がありますが、Dynamic Inventory のスクリプトはプレイブックを起動する際に絶対パスまたは相対パスで指定することになっているようなので、数が多くなければプレイブックと同じディレクトリに入れてしまって良いと思います。

# Ansible Dynamic Inventory for Mackerel 

同じものを gist にも挙げていますが、以下に作ったスクリプトを貼ります。

Ansible Dynamic Inventory for Mackerel https://gist.github.com/yujiod/1587128075bc74009d32

```py:mackerel.py
#!/usr/bin/python
import urllib
import urllib2
import sys
try:
    import json
except ImportError:
    import simplejson as json
 
apiKey = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
service = 'My Service'
networkInterface = 'eth0'
 
def getGroupList():
    hosts = getHosts()
    print json.dumps(hosts)
 
def getHostInfo(hostname):
    hosts = getHosts(hostname)
    print json.dumps(hosts['_meta']['hostvars'][hostname])
 
def getHosts(hostname=None):
    params = { 'service': service }
 
    if hostname != None:
        params['name'] = hostname
 
    url = 'https://mackerel.io/api/v0/hosts.json?' + urllib.urlencode(params)
    headers = { 'X-Api-Key': apiKey }
    request = urllib2.Request(url, None, headers)
    response = urllib2.urlopen(request)
    hosts = json.loads(response.read())
 
    inventories = {
        'production': { 'children': [] },
        '_meta': { 'hostvars': {} },
    }
 
    for host in hosts['hosts']:
        ipAddress = None
        for interface in host['interfaces']:
            if interface['name'] == networkInterface:
                ipAddress = interface['ipAddress']
                break
 
        if ipAddress == None:
            continue
 
        for serviceName, roles in host['roles'].iteritems():
            for role in roles:
                if role not in inventories:
                    inventories[role] = { 'hosts': [] }
 
                inventories[role]['hosts'].append(host['name'])
                inventories['_meta']['hostvars'][host['name']] = {
                    'ansible_ssh_host': ipAddress,
                    'ansible_ssh_port': '22',
                }
 
                if role not in inventories['production']['children']:
                    inventories['production']['children'].append(role)
 
    return inventories
 
if len(sys.argv) == 2 and (sys.argv[1] == '--list'):
    getGroupList()
elif len(sys.argv) == 3 and (sys.argv[1] == '--host'):
    getHostInfo(sys.argv[2])
else:
    print "Usage: %s --list or --host <hostname>" % sys.argv[0]
    sys.exit(1)
```

* API Key はOrganizationの詳細にあるAPIキーを貼り付けます。
* Serviceは現状固定、取得したいホスト一覧が定義されているサービス名を指定します。
* ネットワークインターフェースが複数ある場合のために、冒頭にインターフェース名を定義しています。SSHで接続可能なIPアドレスを持つインターフェース名に修正してご利用ください。
    * 管理対象のサーバーの事業者が複数ある場合にはインターフェース名が変わってくる可能性がありますので、その場合はもう少し修正が必要そうです。
* 55行目付近に `hostvars` を定義している箇所がありますが、このスクリプトではSSH接続先IPアドレスとSSHポート番号のみを定義しています。適宜修正してご利用ください。
* Mackerel に定義したロール以外に全サーバーを production というグループに入れています。違うグループに入れたい場合は定義修正してご利用ください。

APIキーの場所
![mackerel_apikey.png](https://qiita-image-store.s3.amazonaws.com/0/43082/c9b0f2ab-ef8f-44e8-08f5-9287c49f8f1c.png)

# 動作確認

以下は web ロールに2台、db ロールに2台を Mackerel に定義した際の例です。

```
$ ./mackerel.py --list
{"web": {"hosts": ["web001", "web002"]}, "db": {"hosts": ["db001", "db002"]}, "production": {"hosts": [], "children": ["web", "db"]}, "_meta": {"hostvars": {"web001": {"ansible_ssh_host": "xxx.xxx.xxx.xxx", "ansible_ssh_port": "22"}, "web002": {"ansible_ssh_host": "xxx.xxx.xxx.xxx", "ansible_ssh_port": "22"}, {"db001": {"ansible_ssh_host": "xxx.xxx.xxx.xxx", "ansible_ssh_port": "22"}, {"db001": {"ansible_ssh_host": "xxx.xxx.xxx.xxx", "ansible_ssh_port": "22"}}}}

$ ./mackerel.py --host web001
{"ansible_ssh_host": "xxx.xxx.xxx.xxx", "ansible_ssh_port": "22"}
```

ping モジュールを実行してみます。

```
$ ansible -i mackerel.py all -m ping -u foobar
web001 | success >> {
    "changed": false,
    "ping": "pong"
}
web002 | success >> {
    "changed": false,
    "ping": "pong"
}
db001 | success >> {
    "changed": false,
    "ping": "pong"
}
db002 | success >> {
    "changed": false,
    "ping": "pong"
}

$ ansible -i mackerel.py web -m ping -u foobar
web001 | success >> {
    "changed": false,
    "ping": "pong"
}
web002 | success >> {
    "changed": false,
    "ping": "pong"
}
```

ちゃんとロールに対応するグループに絞り込んで ping が実行できていますね。

# 終わりに

Mackerel は5月上旬にβサービスが始まったばかりで、Organizationの名前変更・削除やメトリックの削除などの基本機能が2014年6月1日時点では実装されていないようです。（Organizationの登録名をスペルミスで誤り、恥ずかしいまま残っています。。）

正式版ではアラートなどの機能が付くようですので、そのあたりの機能が充実してきたら本格的に利用したいですね。