Memo Log

PANTO MAIMU 's Personal Blog

Ansible Dynamic Inventoryスクリプトのサンプルを作ってみた

Ansibleの話題というと、大体がplaybookの話になってしまっていて、構成管理情報(インベントリファイル)についてはあまり突っ込んだ話がされることがないなぁ...と思うことがあります。

※データではなくロジックばかり語られている感じ...

大規模なシステムの場合、Ansibleが標準で提供しているインベントリファイルの仕様では、一つのファイルにまとめるにしても、複数のファイルに分けるにしても、可読性が悪くなるため構成管理情報のメンテナンスが大変になり、人為的ミスが発生しやすくなる問題があります。

f:id:PANTOMAIMU:20180506165941p:plain
Ansibleの標準的な構成管理

この問題に対する解決策としてよくあるのが、構成管理情報DB(RDBExcel上)で構成管理情報をメンテナンスし、この構成管理情報DBからインベントリファイルに変換して運用する方法です。

f:id:PANTOMAIMU:20180506165935p:plain
構成管理情報DBからインベントリファイルに変換して運用
既存の構成管理情報DBの流用もでき、RDBExcelの機能を使ってバリデーションを行うこともできるため、規模の大きなシステムではこの方法を使った事例が多いと思います。

ただこの方法だと、構成管理情報DBとインベントリファイルの2か所に構成管理情報が存在するため、データが二重管理される状態になる問題があります。
運用手順を守っていれば大きな問題にならないのですが、緊急対応が必要になった場合、大抵手順は守られません...orz

f:id:PANTOMAIMU:20180506165931p:plain
構成管理情報の二重管理問題
このため、構成管理情報DBとインベントリファイルの間で設定値が不一致状態となり、構成管理情報DBからインベントリファイルを変換した際に、緊急対応で設定した値が古い値で上書きされるという問題が起きます。
まぁ、データを二重管理した際によく起きる問題です....orz

この構成管理情報を二重管理しなくて済むようにするため、Ansibleから構成管理情報DBに直接アクセスできれば、二重管理問題を解決することができます。
これに、AnsibleのDynamic Inventory機能が使えればいいなぁ...というのがあって、今回Dynamic Inventoryスクリプトのサンプルを作ってみました。

f:id:PANTOMAIMU:20180506165926p:plain
Dynamic Inventoryスクリプト

Dynamic Inventoryというのは、Ansibleが解釈できるJSON形式の構成情報を返すスクリプトを利用する方法です。
上の図では、Dynamic Inventoryスクリプトが構成管理情報DBを参照して、JSON形式の構成情報をAnsibleに返しています。

Dynamic Inventoryですが、EC2やOpenStack用のものがすでに用意されています。
要は、自分たちのシステム用のDynamic Inventoryスクリプトを作るためのサンプルを作ってみました...という感じですヾ(:3ノシヾ)ノシ

Excelファイルを読み込むDynamic Inventoryスクリプトの説明

それで、今回作成したDynamic Inventoryスクリプトのサンプルは、Excelファイルを読み込んでJSON形式の構成情報を返すものです。
Excelファイルを構成管理情報DBと見立てたものだと思って下さい。

スクリプトExcelファイルは、以下のGitHubに保存しています。
pullするなりダウンロードするなりして使ってみてください。
github.com

まずは構成管理情報DBに当たるExcelファイルについて説明します。

Excelファイルの説明(inventory.xlsx)

このExcelファイルには、Ansibleでアクセスする先のホスト情報を定義するhostsシートと、保守対象のサービスごとのシートがあります。

hostsシート

このhostsシートでは、AnsibleでsshでアクセスするIPアドレスssh接続ユーザ名称を設定します。

f:id:PANTOMAIMU:20180506174733p:plain
hostsシート
groupには、ホストの属するgroupを定義します。このgroupは、Ansible実行時に指定するgroupに使うことができます。
最初にこのhostsシートに、運用対象hostを追加してきます。
ansible_userには、ssh接続ユーザ名称を設定します。未定義の場合は、ansible.cfgで設定されたユーザ名称が使用されます。

サービスごとのシート

次にサービスごとのシートの説明です。
このシートは、保守対象のサービスごとに作成してください。hostsという名称のシートでなければ問題ありません。
サンプルでは、WebサービスとHA Proxyを想定したものを設定しています。

f:id:PANTOMAIMU:20180506174727p:plain
Webサービス用シート
f:id:PANTOMAIMU:20180506174722p:plain
HA Proxy用シート

このサービスごとのシートのA、B列にあるgroupとhost_nameの定義は必須になります。
C列以降は、サービスごとに拡張してください。
1行目にあるタイトル行に記述したタイトル名称が、Ansible側でインベントリ情報を参照する際の項目名称となります。

host_nameには、hostsシートに記述しているホスト名称(host_name)を指定して紐づけます。
複数のサービスシートから、一つのホストを指定しても問題ありません。
例えば、1つのホストにWenサービスとRDBサービスを混在させることはよくあるので、それを想定しています。

groupには、そのサービス内でグループを分けたい場合に使用します。
ただこのgroupは必須なので、すべてを同一グループにしたい場合は、同じグループ名称を設定してください。
なお、このシート名称をグループ名としたグループも、自動的に生成されます。

JSON形式の構成情報出力例

まずは、Dynamic Inventoryスクリプトを実行して、JSON形式の構成情報を出力してみます。
この際、Dynamic Inventoryスクリプトに実行権限を付与してください。
また、このスクリプトが利用しているyamlパッケージと、Excelファイルにアクセスするためのopenpyxlパッケージを事前にインストールしておいてください。

$ sudo pip3 install openpyxl
$ sudo pip3 install pyyaml

それから、excel_inventory.pyの先頭に記述されているPythonコマンドへのパスの記述は、各自の環境に合わせてください。
※このスクリプトはPython3向けに書いているので、各自のPython環境に合わせてください。

それでは、以下のようにDynamic Inventoryスクリプトを実行してみてください。

$ ./excel_inventory.py  | python -m json.tool

出力として、以下のようなJSON形式の構成情報が出力されます。
このデータをみれば、大体何ができるのかが見えてくると思います。

{
    "_meta": {
        "hostvars": {
            "haproxy001": {
                "ansible_host": "192.168.0.5",
                "frontend_ip": "10.100.0.1",
                "frontend_port": 80
            },
            "haproxy011": {
                "ansible_host": "192.168.0.14",
                "frontend_ip": "10.100.1.1",
                "frontend_port": 80
            },
            "webserver001": {
                "ansible_host": "192.168.0.1",
                "is_backup": false,
                "web_ip": "10.0.0.1",
                "web_port": 8080,
                "weight": 1
            },
            "webserver002": {
                "ansible_host": "192.168.0.2",
                "is_backup": false,
                "web_ip": "10.0.0.2",
                "weight": 2
            },
            "webserver003": {
                "ansible_host": "192.168.0.3",
                "is_backup": false,
                "web_ip": "10.0.0.3",
                "weight": 3
            },
            "webserver004": {
                "ansible_host": "192.168.0.4",
                "is_backup": true,
                "web_ip": "10.0.0.4"
            },
            "webserver011": {
                "ansible_host": "192.168.0.11",
                "is_backup": false,
                "web_ip": "10.0.1.1",
                "weight": 1
            },
            "webserver012": {
                "ansible_host": "192.168.0.12",
                "is_backup": false,
                "web_ip": "10.0.1.2",
                "weight": 3
            },
            "webserver013": {
                "ansible_host": "192.168.0.13",
                "is_backup": false,
                "web_ip": "10.0.1.3",
                "weight": 5
            }
        }
    },
    "all": {
        "vars": {
            "all_test1": 123234,
            "all_test2": 2.13,
            "all_test3": true,
            "all_test4": "test_data"
        }
    },
    "cluster001": {
        "hosts": [
            "webserver001",
            "webserver002",
            "webserver003",
            "webserver004",
            "haproxy001"
        ]
    },
    "cluster002": {
        "hosts": [
            "webserver011",
            "webserver012",
            "webserver013",
            "haproxy011"
        ]
    },
    "haproxy": {
        "hosts": [
            "haproxy001",
            "haproxy011"
        ],
        "vars": {
            "frontend_port": 80,
            "ha_proxy_conf_path": "/etc/haproxy/haproxy.cfg"
        }
    },
    "web001": {
        "hosts": [
            "webserver001",
            "webserver002",
            "webserver003",
            "webserver004",
            "haproxy001"
        ]
    },
    "web002": {
        "hosts": [
            "webserver011",
            "webserver012",
            "webserver013",
            "haproxy011"
        ]
    },
    "webservers": {
        "hosts": [
            "webserver001",
            "webserver002",
            "webserver003",
            "webserver004",
            "webserver011",
            "webserver012",
            "webserver013"
        ],
        "vars": {
            "web_conf_path": "/etc/httpd/conf/httpd.conf",
            "web_port": 80
        }
    }
}

この構成情報が、Ansibleで利用できることを確認してみます。

$ ansible all -m debug -a "msg={{ ansible_host }}"
haproxy011 | SUCCESS => {
    "changed": false,
    "msg": "192.168.0.14"
}
haproxy001 | SUCCESS => {
    "changed": false,
    "msg": "192.168.0.5"
}
webserver001 | SUCCESS => {
    "changed": false,
    "msg": "192.168.0.1"
}
webserver002 | SUCCESS => {
    "changed": false,
    "msg": "192.168.0.2"
}
webserver003 | SUCCESS => {
    "changed": false,
    "msg": "192.168.0.3"
}
webserver004 | SUCCESS => {
    "changed": false,
    "msg": "192.168.0.4"
}
webserver012 | SUCCESS => {
    "changed": false,
    "msg": "192.168.0.12"
}
webserver011 | SUCCESS => {
    "changed": false,
    "msg": "192.168.0.11"
}
webserver013 | SUCCESS => {
    "changed": false,
    "msg": "192.168.0.13"
}

groupごとに属するホストが定義されているので、allの部分をExcelシートに指定したgroup名にすれば、そのグループのhostのみになります。
playbookのhostsに、サービスごとのシート名称を指定すれば、誤って異なるサービスのノードに対して処理が実行されることもなくなります。

次に、共通情報定義ファイルについて説明します。

共通情報定義ファイル(common_val.yml)

この共通情報定義ファイルでは、参照するExcelファイル名称と、groupごとの共通定義を指定しています。
通常のインベントリファイルのgroupvars相当のものです。
specific_varsというのは、今回のスクリプトではまだ使用していませんが、スクリプト内でAnsibleに渡す値を生成するためのカスタマイズ処理関数に引き渡す値を指定します。
このあたりについては、実際にexcel_inventory.pyを読んでみてください。

# inventory file list
inventory_file: inventory.xlsx

# all group vars
all_vars:
  all_test1: 123234
  all_test2: 2.13
  all_test3: True
  all_test4: test_data

# group vars
group_vars:
  haproxy:
    ha_proxy_conf_path: /etc/haproxy/haproxy.cfg
    frontend_port: 80

  webservers:
    web_conf_path: /etc/httpd/conf/httpd.conf
    web_port: 80

# specific values
specific_vars:
  specific_data: specific_val

ansible.cfg

Ansibleではお約束のものです。
inventoryに、今回のスクリプトを設定します。

[defaults]
remote_user = root
inventory = excel_inventory.py
host_key_checking = False

ここで指定したスクリプトには、実行権限が必要なので、chmodで実行権限をつけておいてください。

ざっくりとですが、AnsibleのDynamic Inventoryスクリプトのサンプルについて説明しました。
このサンプルでは、Excelファイルを構成管理情報DBとしていますが、Excelの代わりに構成管理情報が管理されているRDBを参照するようにするといった機能拡張をすることも可能です。
AnsibleのEC2やOpenStack用のDynamic Inventoryスクリプトでは、AWSやOpenStackのAPIを呼び出して、似たようなことをしています。

また、今回のサンプルではExcelファイルの値をそのまま構成情報の項目として設定していますが、このスクリプト内で項目値を生成して登録することもできます。
実装はしてませんが、HA Proxyの項目にバランシング先のホストや重みづけを設定したリストを生成して渡すことが可能です。
ノードの増減によって設定値が変わるようなロードバランサへの設定を、転送先ノードの情報から生成できるようにすれば、冗長な設定も不要になると考えています。

それでは~