はじめに
簡単簡単と言われて使い始めたAnsible、
- エージェントレス
- YAML記法
- 豊富なモジュール
簡単なことに加え、便利ですよね。
しかし、使い倒していく上でそろそろあることがお困りではないでしょうか。
そう、管理対象ホスト、インベントリの管理です。
インベントリはini形式に毛が生えたようなもので、基本的にはただのテキストファイルです。
グループ化や親子構造を取れたりするので便利ではあるのですが、共通化や汎用化を進めていくとグループが複雑なり、テキスト管理が辛くなってきます。
そうした場合、 Dynamic Inventory という機能を用いて、静的なファイルによる管理から動的なデータベース等による管理を選ぶことも可能です。
しかし、AWSやAzure、OpenStackやCloudStackなど、名だたるクラウド関連サービスや製品に対するDynamic Inventoryはあらかた用意されているのですが、そうしたクラウドのコア機能に触れない場合は中々手が出ないこともあるでしょう。多分。
でも大丈夫、Dynamic Inventoryをつくるのは意外と簡単です。
今回はDynamic Inventoryをいくつかの方法でつくってみたいと思います。
(コードが適当なのはやる気の低下によるものです。お察しください。)
下記の環境で試します。
$ ansible --version
ansible 2.2.0.0
config file = /etc/ansible/ansible.cfg
configured module search path = Default w/o overrides
Dynamic Inventoryとは
まずDynamic Inventoryという機能がいかなるものなのかを学びましょう。
Dynamic Inventory のページに色々書いてあるのですが、技術的に詳しいことは書いていないのでさっさと Developing Dynamic Inventory Sources のページを見ましょう。
When the external node script is called with the single argument --list , the script must output a JSON encoded hash/dictionary of all the groups to be managed to stdout . Each group’s value should be either a hash/dictionary containing a list of each host/IP, potential child groups, and potential group variables, or simply a list of host/IP addresses, like so:
これを見ると、結局Dynamic Inventoryとは、
- --list 引数を取れる
- JSON形式 のデータを標準出力する
スクリプト であればよいことがわかります。
基本構造
Dynamic Inventory が出力すべき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" ]
}
のように、
{
# シンプルにグループホストのみを定義する場合
"<グループ名1>" : [ "<ホスト1>", "<ホスト2>" ],
# varsやchildrenを定義する場合
"<グループ名2>" : {
"hosts" : [ "<ホスト1>", "<ホスト2>" ],
"vars" : {
"<変数名1>" : "<値1>"
}
"children" : [ "<子グループ1>", "<子グループ2>"]
}
}
という構造を取れればよいようです。
Dynamic Inventoryが登場した当初はグループホストのみを定義する書き方しか対応していなかったようですが、 バージョン1.3 以降ではvarsやchildrenも一緒に定義できるようになりました。
応用: hostvars
応用的な書き方として、hostvarsもDynamic Inventoryで表現することができます。
hostvarsは元々、Dynamic Inventory スクリプトに対して、 --host <ホスト名> の引数を渡すことで、指定したホストに関する hostvars が取得する機能を実装することで実現されていました。
$ ./dynamic_inventory --host moocow.example.com
{
"asdf" : 1234
}
$ ./dynamic_inventory --host llama.example.com
{
"asdf" : 5678
}
しかし、この仕様では hostvars の利用に関して、
- Dynamic Inventoryに--listでホストグループを問い合わせる
- Ansibleの実行グループから、実行対象ホストを特定する
- 実行対象の各ホストに対して、それぞれ--host <ホスト名>でDynamic Inventoryを実行してhostvarsを取得する
というステップを踏む必要があり、 実行対象ホストの数に応じて Dynamic Inventory の起動回数が増え、それだけ データストアへの問い合わせ が多くなるという問題がありました。
そのため、グループの指定方法が拡張された1.3で hostvars の指定方法も、下記のように ホストグループ情報に "_meta" を加えて出力させる ことで、問い合わせ回数の最小化が可能になりました。
{
# ホストグループの定義
(省略)
# hostvarsの定義
"_meta" : {
"hostvars" : {
"moocow.example.com" : { "asdf" : 1234 },
"llama.example.com" : { "asdf" : 5678 },
}
}
}
インベントリファイルの場合
Dynamic Inventoryとの対比を見るため、まずは静的ファイルのインベントリを使っておきましょう。
$ cat inventory
[sample-servers]
192.168.100.10 host_var=hoge
192.168.100.20 host_var=fuga
[sample-servers:vars]
group_var=hogefuga
このとき、このような動きをします。
$ ansible -i inventory sample-servers -m debug -a "msg={{ host_var }}"
192.168.100.20 | SUCCESS => {
"msg": "fuga"
}
192.168.100.10 | SUCCESS => {
"msg": "hoge"
}
$ ansible -i inventory sample-servers -m debug -a "msg={{ group_var }}"
192.168.100.20 | SUCCESS => {
"msg": "hogefuga"
}
192.168.100.10 | SUCCESS => {
"msg": "hogefuga"
}
これと同等の動きを自作Dynamic Inventoryにさせてみましょう。
インベントリのJSON化
公式ページで確認したように、Dynamic Inventoryとは、何かのデータストアそのものを指すわけではなく、 JSON形式のホストリストを標準出力するスクリプト でした。
なのでまずは、あのファイルと同等の意味を持つJSONを最終形として見ておきましょう。こちら。
{
"_meta": {
"hostvars": {
"192.168.100.10": {
"host_var": "hoge"
},
"192.168.100.20": {
"host_var": "fuga"
}
}
},
"sample-servers": {
"hosts": [
"192.168.100.10",
"192.168.100.20"
],
"vars": {
"group_var": "hogefuga"
}
}
}
つまり、このJSONを吐き出すようにDynamic Inventoryスクリプトを作ればいいわけです。
インチキDynamic Inventory
まずはDynamic Inventoryの挙動を確かめるべく、インチキをしてDynamic Inventoryを体感してみましょう。
そう、あのJSONをそのまま吐き出すスクリプトを作るのです。
先ほどのゴールとなるJSONをそのままファイルに書き出します。
$ cat ~/sample_json
(さっきのJSONの中身)
そしてそれを cat
するだけのスクリプトを作りましょう。
$ vi fake_dynamic_inventory
#!/bin/bash
cat ~/sample_json
ではさっそくこのインチキDynamic Inventoryで動かしてみましょう。
$ ansible -i ./fake_dynamic_inventory sample-servers -m debug -a "msg={{ host_var }}"
ERROR! The file ./fake_dynamic_inventory looks like it should be an executable inventory script, but is not marked executable. Perhaps you want to correct this with `chmod +x ./fake_dynamic_inventory`?
The file ./fake_dynamic_inventory looks like it should be an executable inventory script, but is not marked executable. Perhaps you want to correct this with `chmod +x ./fake_dynamic_inventory`?
おやおや、実行権がないと怒られてしまいました。
そうです、Dynamic Inventoryスクリプトには 実行権 をつけなければいけないことにご注意ください。
$ chmod +x fake_dynamic_inventory
$ ansible -i ./fake_dynamic_inventory sample-servers -m debug -a "msg={{ host_var }}"
192.168.100.10 | SUCCESS => {
"msg": "hoge"
}
192.168.100.20 | SUCCESS => {
"msg": "fuga"
}
$ ansible -i ./fake_dynamic_inventory sample-servers -m debug -a "msg={{ group_var }}"
192.168.100.20 | SUCCESS => {
"msg": "hogefuga"
}
192.168.100.10 | SUCCESS => {
"msg": "hogefuga"
}
完璧ですね。まずは指定形式のJSONをAnsibleに食わせるときちんと動くことがわかりました。
RDBMS: PostgreSQLの場合
まずはデータストアといえば、というわけでRDBMSからPostgreSQLに登場していただきます。
適当にDockerか何かでPostgreSQLを用意してください。あとテーブル作成用にpsqlも。
こんな感じに。
# PostgreSQLイメージの取得
$ docker pull docker.io/postgres
# postgresコンテナの起動
$ docker run --name postgres -e POSTGRES_PASSWORD=password -d postgres
# psqlのインストール
$ sudo yum install postgresql
# PostgreSQL on postgresコンテナへの接続
$ psql -h $(sudo docker inspect --format "{{ .NetworkSettings.IPAddress }}" postgres) -U postgres
postgres=#
ではテーブルを作ってデータを突っ込みましょう。
下記のテーブルを作ります。
仕様上の穴しかない非常にアホっぽいテーブルですが、コンセプトデータみたいなものなんで勘弁してください。
- groupsテーブル
groupname |
---|
sample-servers |
- groupvarsテーブル
groupname | varname | varvalue |
---|---|---|
sample-servers | group_var | hogefuga |
- hostsテーブル
hostname | groupname |
---|---|
192.168.100.10 | sample-servers |
192.168.100.20 | sample-servers |
- hostvarsテーブル
hostname | varname | varvalue |
---|---|---|
192.168.100.10 | host_var | hoge |
192.168.100.20 | host_var | fuga |
下記のDML文とDDL文をpsqlから流しておいてください。
CREATE TABLE groups (
groupname varchar(16) unique
);
CREATE TABLE groupvars (
groupname varchar(16) REFERENCES groups ( groupname ),
varname varchar(16),
varvalue varchar(16)
);
CREATE TABLE hosts (
hostname varchar(16) unique,
groupname varchar(16) REFERENCES groups ( groupname )
);
CREATE TABLE hostvars (
hostname varchar(16) REFERENCES hosts ( hostname ),
varname varchar(16),
varvalue varchar(16)
);
INSERT INTO groups VALUES ( 'sample-servers' );
INSERT INTO groupvars VALUES ( 'sample-servers', 'group_var', 'hogefuga' );
INSERT INTO hosts VALUES ( '192.168.100.10', 'sample-servers' );
INSERT INTO hosts VALUES ( '192.168.100.20', 'sample-servers' );
INSERT INTO hostvars VALUES ( '192.168.100.10', 'host_var', 'hoge' );
INSERT INTO hostvars VALUES ( '192.168.100.20', 'host_var', 'fuga' );
さて、ここからこのデータベースに対して問い合わせを行い、あのJSON出力を得ることを考えます。
何かしらのプログラミング言語を使うとよいと思うので、今回はPythonにします。
PythonからPostgreSQLに接続することに関しては、 psycopg2 を使います。
$ sudo yum install python-psycopg2
では下記Pythonスクリプトを作成して、動かしてみてください。
PostgresSQLの接続先は docker inspect
で勝手に取るようにしていますが、Docker以外でやっている場合はIPを直接書くなどしてください。
#!/usr/bin/python
# -*- coding:utf-8 -*-
import commands
import psycopg2
import json
output_dict={}
# DBコネクションの定義
conn = psycopg2.connect(
host = commands.getoutput('sudo docker inspect --format "{{ .NetworkSettings.IPAddress }}" postgres'),
port = 5432,
database="postgres",
user="postgres",
password="password")
cur_group = conn.cursor()
cur_group.execute("""SELECT groupname FROM groups""")
# グループごとに辞書を作成して追加する
for row_group in cur_group:
group_dict={}
grouphosts=[]
groupvars={}
cur_hosts = conn.cursor()
cur_hosts.execute("""SELECT hostname FROM hosts WHERE groupname = %s""", (row_group[0],))
for row_host in cur_hosts:
grouphosts.append(row_host[0])
cur_groupvars = conn.cursor()
cur_groupvars.execute("""SELECT varname, varvalue FROM groupvars WHERE groupname = %s""", (row_group[0],))
for row_groupvar in cur_groupvars:
groupvars[row_groupvar[0]]=row_groupvar[1]
group_dict["hosts"]=grouphosts
group_dict["vars"]=groupvars
output_dict[row_group[0]]=group_dict
cur_hosts = conn.cursor()
cur_hosts.execute("""SELECT DISTINCT hostname FROM hosts""")
meta_dict={}
hostvars={}
# ホストごとに辞書を作成して追加する
for row_host in cur_hosts:
hostvar={}
cur_hostvars = conn.cursor()
cur_hostvars.execute("""SELECT varname,varvalue FROM hostvars WHERE hostname = %s""", (row_host[0],))
for row_hostvar in cur_hostvars:
hostvar[row_hostvar[0]]=row_hostvar[1]
hostvars[row_host[0]]=hostvar
meta_dict["hostvars"]=hostvars
output_dict["_meta"]=meta_dict
# 辞書をJSON形式して標準出力する
print json.dumps(output_dict, indent=4)
特に何のひねりもなくfor文をまわしてdictを作ってjson形式で吐き出しています。
もっとうまい方法があるかもしれませんが、普通にやる場合はRDBMSである以上、ネスト構造の部分にfor文をあてていくことになるかと思います。
RDBMSということでテーブルのイメージはつきやすいですが、それをJSONにまとめて出力するのは少し大変そうです。
とはいえ、JSONとして吐き出すというDynamic Inventoryのルールは満たしているため、
$ chmod +x postgresql_dynamic_inventory.py
$ ansible -i ./postgresql_dynamic_inventory.py sample-servers -m debug -a "msg={{ host_var }}"
192.168.100.20 | SUCCESS => {
"msg": "fuga"
}
192.168.100.10 | SUCCESS => {
"msg": "hoge"
}
というように、Dynamic Inventoryとしての機能に問題はなさそうです。
もう少しがんばる場合
今回は実装や設計の工夫はせずあくまでコンセプト的なところを確認するため、かなり面倒を省略しています。
例えば、
- グループの親子関係(children)を定義していない
- groupvarsで単純なkey:valueの変数しか定義していない
- hostvarsで単純なkey:valueの変数しか定義していない
といったところがあります。
親子関係はおそらくgroupsテーブルに親グループの情報を付加すればある程度いけると思いますが、varsでkey:valueだけでなく辞書形式の変数を導入しようとすると、いわゆる第一正規化の問題にあたり、ちょいと面倒になります。
ただ、最近のPostgreSQLやMySQLなどはデータ型として JSON型 などが持てるようになりました。
そのため、これらを活用すると1行1カラムの中でも実質複数のデータを格納することができ、この問題を解決できるかもしれません。
ただ、RDBMSでJSONを使うというのはまだまだ発展途上なところがありますし、他の用途がないのであれば後述するような元々JSONデータが格納できる別のNoSQLータベースを選択するほうがいいのかもしれません。
NoSQL: MongoDBの場合
続いてはMongoDBで同じことをやってみましょう。
MongoDBはNoSQLのうち、ドキュメント型に位置付けられるもので、端的に言えばスキーマレスでJSONのようなデータがそのままの形式で格納できます。
それでは、やってみましょう。
$ sudo docker pull docker.io/tutum/mongodb
$ sudo docker run -d -p 27017:27017 -p 28017:28017 -e MONGODB_USER="mongo" -e MONGODB_DATABASE="mongo" -e MONGODB_PASS="password" tutum/mongodb
MongoDBにアクセスするべくmongoクライアントを導入しましょう。
標準リポジトリにはMongoDB本体と一緒になっているやつしかなくてキモイので別途公式リポジトリを追加しておきます。
[mongodb-org-3.2]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/3.2/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-3.2.asc
あとはmongoクライアントをインストールしてMongoDBにデータを突っ込んでおきましょう。
# mongoクライアントのインストール
$ sudo yum install mongodb-org-shell
# MongoDBへの接続
$ mongo mongo -u mongo -p password
# テストデータの追加
> db.inventory.insert({"_meta":{"hostvars":{"192\uff0E168\uff0E100\uff0E10":{"host_var":"hoge"},"192\uff0E168\uff0E100\uff0E20":{"host_var":"fuga"}}},"sample-servers":{"hosts":["192\uff0E168\uff0E100\uff0E10","192\uff0E168\uff0E100\uff0E20"],"vars":{"group_var":"hogefuga"}}})
ここで微妙なのはMongoDBが仕様上、ピリオドをキー名にできないことです。
そのため、テストデータをよく見てもらうと、IPアドレスにあるピリオドの部分が \uff0E
になっていることがわかると思います。
#!/usr/bin/python
# -*- coding:utf-8 -*-
import pymongo
import json
from bson.json_util import dumps
# DBコネクションの定義
conn = pymongo.MongoClient('127.0.0.1', 27017)
db = conn.mongo
db.authenticate("mongo","password")
inventory = db.inventory
output_dict = {}
# inventoryコレクションの取得
output_dict.update(inventory.find()[0])
# オブジェクトIDの除去
del output_dict["_id"]
# 辞書をJSON形式して標準出力する
print json.dumps(output_dict, indent=4)
当たり前といえば当たり前の話なのですが、MongoDBにはゴールとなるJSONを突っ込んでいるだけなので、プログラム側では特にやることがありません。
しいて言うなら、MongoDBによって暗黙的に付与されるオブジェクトIDを除去していることくらいでしょうか。
実際の動作を見ても、
$ chmod +x mongodb_dynamic_inventory.py
$ ansible -i ./mongodb_dynamic_inventory.py sample-servers -m debug -a "msg={{ host_var }}"
192.168.100.20 | SUCCESS => {
"msg": "fuga"
}
192.168.100.10 | SUCCESS => {
"msg": "hoge"
}
特に問題なさそうです。
ピリオドの扱い
テストデータを用意する際に触れた通り、MongoDBに格納するオブジェクトはキー名にピリオドを持つことができません。
そのため、エンコードをした上でデータを投入しているのですが、実は上のコードでは
$ ./mongodb_dynamic_inventory.py
{
"_meta": {
"hostvars": {
"192\uff0e168\uff0e100\uff0e10": {
"host_var": "hoge"
},
"192\uff0e168\uff0e100\uff0e20": {
"host_var": "fuga"
}
}
},
"sample-servers": {
"hosts": [
"192\uff0e168\uff0e100\uff0e10",
"192\uff0e168\uff0e100\uff0e20"
],
"vars": {
"group_var": "hogefuga"
}
}
}
というように、Dynamic Inventoryとしての出力にもエンコードがそのまま残るようになっています。
全キー走査して置換するとかの方法もあるとは思いますが、このままでも動くこともあり、面倒なのでやっていません。
MongoDBへのデータ投入からPythonによる取り出しの流れでもっといい感じにできればいいのですが...。
ちなみに気付いた人もいるかもしれませんが、この影響でAnsibleが出力するホスト名が少しキモくなっています。
[普通]
192.168.100.10 | SUCCESS => {
"msg": "hoge"
}
[今回]
192.168.100.10 | SUCCESS => {
"msg": "hoge"
}
ピリオドが全角です。キモいけど我慢。
おわりに
いかがだったでしょうか。
Dynamic Inventoryというとなんだか難しそうな仕組みに聞こえますが、フタを開けてみればただのJSONです。
もし静的なファイルで大量のホストを頑張って管理しているのなら、ここはひとつDynamic Inventory化してみるのもいいでしょう。
今回はバックエンドとしてPostgreSQLやMongoDBを使ったりしましたが、モノによってはもっと簡単かつシンプルに動かすことができるかもしれません。
(Ansible 2.4のロードマップに Inventory Overhaul(インベントリ大点検) という文言が見えますが詳細が見えてくるまでそっと見守りましょう。。。)