Ansible InventoryPluginのススメ

[さくらインターネット Advent Calendar 2021](https://qiita.com/advent-calendar/2021/sakura) 16日目の記事です。
# 概要

Ansibleのインベントリを動的に生成する方法として、「ダイナミックインベントリ」がよく使われていると思いますが、「インベントリプラグイン」という別の方法もあるよ、というお話です。

https://docs.ansible.com/ansible/latest/user_guide/intro_dynamic_inventory.html

https://docs.ansible.com/ansible/latest/plugins/inventory.html

インベントリプラグインは、ダイナミックインベントリと比べて仕組みが少し複雑ですが、ケースによってはインベントリプラグインを用いたほうが簡潔にプレイブックを構成できる場合があります。



# さっそく試してみる

例として、以下のようなExcelファイル(book1.xlsx)をAnsibleのインベントリとして利用してみます。
左から、ホスト名、IPアドレス、用途といったイメージです。

![book1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36522/08c0cd54-9369-1f70-c615-b2af34108216.png)

## インベントリプラグイン場所を指定しておく
デフォルトのままだと扱いにくいため、インベントリプラグインを参照するディレクトリを設定しておきます。

```ansible.cfg
[defaults]
inventory_plugins=./plugins/inventory
```

デフォルトの値は以下のドキュメントに記載があります。

https://docs.ansible.com/ansible/latest/reference_appendices/config.html#default-inventory-plugin-path

## インベントリファイルを作る

`ansible-playbook` コマンドの `-i` オプションで指定する、インベントリファイルを作成します。
`plugin` にはこれから作成するインベントリプラグイン名を記載し、`files`には読み込むExcelファイルを定義しておきます。

```xlsx.yml
plugin: xlsx
files:
  - "book1.xlsx"
```

## インベントリプラグインをつくる

とても雑ですが、Excelファイルを読み込んでインベントリに追加するスクリプトです。

```plugins/inventory/xlsx.py
import openpyxl
from ansible.plugins.inventory import BaseInventoryPlugin


class InventoryModule(BaseInventoryPlugin):
    NAME = "xlsx"

    def verify_file(self, path):
        valid = False
        if super(InventoryModule, self).verify_file(path):
            if path.endswith(".yml"):
                valid = True
                return valid
        return True

    def _load_xlsx(self, path):
        inventory_data = self.loader.load_from_file(path, cache=False)
        nodes = []
        for f in inventory_data.get("files"):
            wb = openpyxl.load_workbook(f)
            ws = wb["Sheet1"]
            for row in ws.rows:
                nodes.append(
                    {
                        "hostname": row[0].value,
                        "address": row[1].value,
                        "role": row[2].value,
                    }
                )
        return nodes

    def parse(self, inventory, loader, path, cache=True):
        super(InventoryModule, self).parse(inventory, loader, path, cache)

        xlsx_data = self._load_xlsx(path)

        for data in xlsx_data:
            self.inventory.add_host(data["hostname"])
            self.inventory.set_variable(
                data["hostname"], "ansible_host", data["address"]
            )
            self.inventory.add_group(data["role"])
            self._populate_host_vars([data["hostname"]], {}, data["role"])
            self.inventory.add_child("all", data["role"])
```

詳細は割愛します。自作される場合は以下のリポジトリが大変参考になります。

https://github.com/ansible-collections/community.general/tree/main/plugins/inventory

### 実行してみる
無事にExcelファイルの中身をインベントリとして扱うことができました！

```
$ ansible-inventory -i xlsx.yml --list
{
    "_meta": {
        "hostvars": {
            "node1": {
                "ansible_host": "192.0.2.1"
            },
            "node2": {
                "ansible_host": "192.0.2.2"
            },
            "node3": {
                "ansible_host": "192.0.2.3"
            }
        }
    },
    "all": {
        "children": [
            "db",
            "ungrouped",
            "web"
        ]
    },
    "db": {
        "hosts": [
            "node2",
            "node3"
        ]
    },
    "web": {
        "hosts": [
            "node1"
        ]
    }
}
```

### おまけ
たとえばこんなファイル(book2.xlsx)が追加されたとしても
![book2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36522/20134c3a-f71b-194c-73b1-23a0eff86891.png)

インベントリファイルに追加するだけでOKです。

```xlsx.yml
plugin: xlsx
files:
  - "book1.xlsx"
  - "book2.xlsx"
```

```
$ ansible-inventory -i xlsx.yml --list
{
    "_meta": {
        "hostvars": {
            "node1": {
                "ansible_host": "192.0.2.1"
            },
            "node2": {
                "ansible_host": "192.0.2.2"
            },
            "node3": {
                "ansible_host": "192.0.2.3"
            },
            "node4": {
                "ansible_host": "192.0.2.4"
            },
            "node5": {
                "ansible_host": "192.0.2.5"
            },
            "node6": {
                "ansible_host": "192.0.2.6"
            }
        }
    },
    "all": {
        "children": [
            "db",
            "ungrouped",
            "web"
        ]
    },
    "db": {
        "hosts": [
            "node2",
            "node3",
            "node5",
            "node6"
        ]
    },
    "web": {
        "hosts": [
            "node1",
            "node4"
        ]
    }
}
```


なお作成したファイルのディレクトリ構造は以下の通りです。

```
$ tree
.
├── ansible.cfg
├── book1.xlsx
├── book2.xlsx
├── plugins
│   └── inventory
│       └── xlsx.py
└── xlsx.yml

```
# おわりに
ダイナミックインベントリの場合、与えられる引数として環境変数を主に使うことになるかと思いますが、インベントリプラグインの場合はインベントリファイルにいくらでも定義ができるため自由度が高く、より柔軟なインベントリを構成することができます。ダイナミックインベントリで悩んでいる方はぜひお試しください。