让我们创建一个 Ansible 动态清单!
首先
Ansible是以”非常简单”为口号而开始使用的。
-
- エージェントレス
-
- YAML記法
- 豊富なモジュール
除了简单的事情之外,这也非常方便。
不过,在充分使用之后,您可能已经遇到了一些问题,请问是这样吗?
是的,管理目标主机和清单管理。
盘点是一种类似于 ini 格式的东西,基本上只是一个普通的文本文件。
尽管可以进行分组和父子结构,这是方便的,但是在推进常规化和通用化的过程中,分组变得复杂,文本管理变得困难。
在这种情况下,您可以使用名为Dynamic Inventory的功能,选择从静态文件管理到动态数据库等管理方式。然而,对于像AWS、Azure、OpenStack和CloudStack等著名的云相关服务和产品,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这个功能是什么。
在动态库存页面上写着许多内容,但并没有详细说明技术细节,所以我们赶紧去看看”开发动态库存来源”的页面吧。
当外部节点脚本以单个参数–list被调用时,脚本必须将所有待管理的组的JSON编码散列/字典输出到stdout。每个组的值应该是一个包含每个主机/IP、潜在子组和潜在组变量列表的散列/字典,或者仅仅是一个主机/IP地址的列表,如下所示:
看到这个,最终Dynamic Inventory是指
–list 引数を取れる
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" ]
}
就像这样,
{
# シンプルにグループホストのみを定義する場合
"<グループ名1>" : [ "<ホスト1>", "<ホスト2>" ],
# varsやchildrenを定義する場合
"<グループ名2>" : {
"hosts" : [ "<ホスト1>", "<ホスト2>" ],
"vars" : {
"<変数名1>" : "<値1>"
}
"children" : [ "<子グループ1>", "<子グループ2>"]
}
}
只需要一个选项:
似乎只需要采取这种结构。
起初,动态清单似乎只支持定义组主机,但从1.3版本开始,也可以定义vars和children。
应用:hostvars
作为一个实际应用的写法,也可以使用hostvars在动态清单中表示。hostvars最初是通过向动态清单脚本传递 –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"
}
让我们在自己创建的动态库存中实现与此相等的动作。
将库存转化为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。
是的,我们要创建一个直接输出那个JSON的脚本。
将刚刚的JSON作为文件原样写入。
$ cat ~/sample_json
(さっきのJSONの中身)
那么我们来制作一个仅适用于猫的脚本吧。
$ 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テーブル
- groupvarsテーブル
- hostsテーブル
- hostvarsテーブル
请通过psql运行下面的DML语句和DDL语句。
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客户端来访问MongoDB数据库。因为标准仓库中没有MongoDB客户端,只有与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(清点清单)”这一词,但在了解详细情况之前,让我们静待消息。。。)