让我们创建一个 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 の利用に関して、

    1. 用 Dynamic Inventory 的 –list 查询主机组

 

    1. 从 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テーブル
groupnamesample-servers
    groupvarsテーブル
groupnamevarnamevarvaluesample-serversgroup_varhogefuga
    hostsテーブル
hostnamegroupname192.168.100.10sample-servers192.168.100.20sample-servers
    hostvarsテーブル
hostnamevarnamevarvalue192.168.100.10host_varhoge192.168.100.20host_varfuga

请通过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(清点清单)”这一词,但在了解详细情况之前,让我们静待消息。。。)

广告
将在 10 秒后关闭
bannerAds