Динамическая инвентаризация#

Динамическая инвентаризация предназначена для автоматического формирования описания инвентаря на основе внешних источников данных (файлы, API, базы данных, облачные сервисы, CMDB (Configuration management database) и другие). Это позволяет управлять инфраструктурой, которая часто изменяется, без необходимости вручную обновлять статические файлы инвентаризации.

Способы динамической инвентаризации#

Как отмечалось ранее, при любом способе Ansible формирует динамически в начале выполнения задания внутреннее описание инвентаря. Однако, к динамической инвентаризации следует относить процессы, в которых пользователь активно влияет на формирование описания с помощью следующих средств:

  • собственные утилиты, формирующие описание в формате JSON, соответствующем требованиям Ansible;

  • дополнительные расширения Ansible для инвентаризации, разработанные собственными средствами или предоставляемые другими разработчиками, например расширения для популярных облачных платформ Amazon EC2, Google Cloud, OpenStack и других;

  • директивы внутри сценариев автоматизации, изменяющие описание инвентаря, например с помощью модулей ansible.builtin.add_host и ansible.builtin.group_by.

Описание инвентаря в формате JSON должно иметь структуру, как в следующем примере:

{
  "group1": {
    "hosts": ["host1", "host2"],
    "vars": {
      "group_var1": "value1"
    },
    "children": ["child_group1"]
  },
  "group2": {
    "hosts": ["host3"]
  },
  "_meta": {
    "hostvars": {
      "host1": {
        "host_var1": "valueA"
      },
      "host2": {},
      "host3": {
        "host_var2": "valueB"
      }
    }
  }
}

Секции, описывающие группы, узлы и переменные, имеют то же значение, что и в файлах со статическим описанием. Секция _meta с метаданными описана далее.

Этапы инвентаризации#

Основные этапы динамической инвентаризации представлены следующими шагами:

  1. Ansible получает источники описания инвентаря, указанные аргументами -i.

  2. Ansible обрабатывает каждый источник с помощью соответствующего расширения Ansible или путем прямого запуска исполняемого файла. Результатом обработки каждого источника является описание инвентаря в формате JSON.

  3. Ansible анализирует полученные данные и формирует группы, переменные и метаданные для каждого сценария.

  4. Ansible использует полученное описание инвентаря для выполнения сценариев автоматизации.

Метаданные#

Метаданные в динамической инвентаризации Ansible предназначены для хранения информации об узлах (hostvars), которая необходима для выполнения сценариев. Метаданные позволяют избежать дополнительных запросов к источнику данных при обращении к переменным узлов во время выполнения сценариев.

Метаданные выполняют следующие функции:

  • упрощают доступ к переменным узлов;

  • повышают производительность путем предварительной загрузки всех переменных;

  • обеспечивают целостность и актуальность информации о каждом узле.

Структура метаданных#

В описании инвентаря в формате JSON, возвращаемом динамическим источником, метаданные размещаются в специальном разделе _meta. Этот раздел содержит вложенный элемент hostvars, который представляет собой словарь, где ключ — название узла, а значение — словарь переменных для этого узла.

Пример структуры метаданных:

{
  "_meta": {
    "hostvars": {
      "web01.example.com": {
        "ansible_host": "192.168.56.11",
        "ansible_user": "admin"
      },
      "db01.example.com": {
        "ansible_host": "192.168.56.21",
        "ansible_user": "root"
      }
    }
  }
}

Элементы структуры метаданных#

Формально структура имеет следующий общий вид:

{
  "_meta": {
    "hostvars": {
      "<host_name>": {
        "<variable>": "<value>"
      }
    }
  }
}

Ключи словарей указывают на следующие данные:

  • hostvars – словарь переменных для всех узлов инвентаря;

  • <host_name> – название конкретного узла, например web01.example.com;

  • <variable> – переменная, определяющая параметр подключения или поведения для данного узла, например ansible_host или ansible_user;

  • <value> – значение переменной.

Примечание

Словарь _meta и вложенный словарь hostvars позволяют Ansible получать все необходимые переменные для каждого узла единовременно, что ускоряет выполнение сценариев и снижает нагрузку на внешний источник данных.

Разработка собственных способов#

Собственные способы должны формировать описание в формате JSON, представленном ранее. К ним относятся следующие:

  • разработка собственного расширения Ansible на языке Python – расширение инвентаризации (Ansible inventory plugin);

  • разработка собственного (также называемого «внешнего») исполняемого скрипта или бинарного файла динамической инвентаризации, который можно использовать как и другие файлы описания инвентаря.

Первый из представленных способов является предпочтительным.

Разработка расширения Ansible для динамической инвентаризации#

Все расширения Ansible, включая рассматриваемый тип, разрабатываются на языке Python.

Рекомендации#

Следующие рекомендации являются основными при разработке расширения:

  • Реализуйте базовый класс BaseInventoryPlugin и определите требуемые методы verify_file и parse.

  • При необходимости реализуйте классы Cacheable для кеширования данных и Constructable для динамического формирования групп узлов.

  • Документируйте все параметры расширения и поддерживаемые форматы источников.

  • Используйте стандартные структуры данных Ansible для описания групп, узлов и переменных.

Структура расширения#

Основные функции инвентаризации реализуются через класс BaseInventoryPlugin. Основные методы класса:

  • verify_file(path) проверяет, может ли расширение обработать указанный источник.

  • parse(inventory, loader, path, cache=True) обрабатывает источники данных и формирует структуру инвентаря.

Внутри метода parse необходимо реализовать логику получения данных, создания групп, добавления узлов и переменных. В нем же, при необходимости, можно реализовать кеширование и динамическое формирование групп через шаблоны Jinja2.

Пример базовой структуры расширения без подробностей:

from ansible.plugins.inventory import BaseInventoryPlugin

class InventoryModule(BaseInventoryPlugin):
    NAME = 'myplugin'

    def verify_file(self, path):
        # Validate source files
        return path.endswith('.myplugin.yml')

    def parse(self, inventory, loader, path, cache=True):
        # Main parsing and inventory creation
        config = self._read_config_data(path)
        # Adding groups, nodes, and variables
        # self.inventory.add_host(...)
        # self.inventory.set_variable(...)

Разработка утилиты динамической инвентаризации#

В качестве альтернативы расширениям можно использовать внешнюю утилиту (исполняемый скрипт или бинарный файл), которая возвращает описание инвентаря в формате JSON. Утилита должна принимать два аргумента:

  • --list– для вывода полной структуры инвентаря;

  • --host <host_name> – для вывода переменных по конкретному узлу.

Следующая команда выполняет простую проверку скрипта:

ansible-inventory -i ./my_inventory.py --list

Примечание

Для повышения производительности рекомендуется, чтобы утилита возвращала секцию _meta с переменными всех узлов (hostvars) при вызове с параметром --list. В противном случае Ansible будет вызывать утилиту для каждого узла отдельно, используя аргумент --host.

Для Ansible ссылку на исполняемый файл (утилиту) следует задавать как и на файлы со статической инвентаризацией, то есть используя аргумент -i. Если скрипт обрабатывает исходное статическое описание инвентаря, заданного в других файлах, то в командной строке его необходимо указывать после указания на эти файлы.

Дополнительные требования:

  • Файл должен быть обязательно отмечен как исполняемый, то есть иметь атрибут x, назначаемый командой вида:

    chmod +x inventory.py
    
  • В файле следует указать строку shebang, например для Python это будет #!/usr/bin/env python3 или #!/usr/bin/python3.

Примеры#

Следующие примеры демонстрируют создание и применение динамической инвентаризации.

Использование расширения constructed#

Ansible использует расширение ansible.builtin.constructed для динамического описания инвентаря встроенными средствами.

Для примера рассмотрим два файла с исходным статическим описанием инвентаря:

# inventory-staging.ym
---
all:
  hosts:
    web01.staging.example.com:
      environment: staging
      public_ip_address: 192.168.56.11
      role: web
    db01.staging.example.com:
      environment: staging
      private_ip_address: 10.0.0.21
      role: db
# inventory-production.yml
---
all:
  hosts:
    web01.prod.example.com:
      environment: production
      public_ip_address: 192.168.57.11
      role: web
    db01.prod.example.com:
      environment: production
      private_ip_address: 10.0.1.21
      role: db

В файле inventory-constructed.yml используется расширение constructed для объединения узлов в группы по заданным признакам:

# inventory-constructed.yml
---
plugin: ansible.builtin.constructed
compose:
  ansible_host: public_ip_address | default(private_ip_address)
groups:
  staging: environment == 'staging'
  production: environment == 'production'
  webservers: role == 'web'
  dbservers: role == 'db'

Следующая команда проверяет результат динамической инвентаризации:

ansible-inventory -i inventory-staging.yml -i inventory-production.yml -i inventory-constructed.yml --list

Примечание

Файл, в котором используется расширение constructed, должен быть последним в списке файлов инвентаря.

Полученное описание инвентаря имеет следующий вид:

Динамическое описание
{
  "_meta": {
    "hostvars": {
      "db01.prod.example.com": {
        "ansible_host": "10.0.1.21",
        "environment": "production",
        "private_ip_address": "10.0.1.21",
        "role": "db"
      },
      "db01.staging.example.com": {
        "ansible_host": "10.0.0.21",
        "environment": "staging",
        "private_ip_address": "10.0.0.21",
        "role": "db"
      },
      "web01.prod.example.com": {
        "ansible_host": "192.168.57.11",
        "environment": "production",
        "public_ip_address": "192.168.57.11",
        "role": "web"
      },
      "web01.staging.example.com": {
        "ansible_host": "192.168.56.11",
        "environment": "staging",
        "public_ip_address": "192.168.56.11",
        "role": "web"
      }
    }
  },
  "all": {
    "children": [
      "ungrouped",
      "staging",
      "webservers",
      "dbservers",
      "production"
    ]
  },
  "dbservers": {
    "hosts": ["db01.staging.example.com", "db01.prod.example.com"]
  },
  "production": {
    "hosts": ["web01.prod.example.com", "db01.prod.example.com"]
  },
  "staging": {
    "hosts": ["web01.staging.example.com", "db01.staging.example.com"]
  },
  "webservers": {
    "hosts": ["web01.staging.example.com", "web01.prod.example.com"]
  }
}

Внешний скрипт для объединения описаний инвентаря#

Приведенный далее скрипт формирует описание инвентаря в формате JSON в соответствии с требованиями Ansible. В скрипте определены методы для требуемых параметров:

  • --list – вывод полного инвентаря;

  • --host <host_name> – вывод переменных для конкретного узла.

Скрипт Python для динамического описания
#!/usr/bin/env python3

import argparse
import json

# Пример хранилища узлов
store = {
    "web01.example.com": {"ansible_host": "192.168.56.11", "ansible_user": "admin"},
    "db01.example.com": {"ansible_host": "192.168.56.21", "ansible_user": "root"},
}


def get_host_vars(host):
    return store.get(host, {})


def get_inventory():
    return {
        "_meta": {"hostvars": {host: vars for host, vars in store.items()}},
        "all": {"hosts": list(store.keys())},
    }


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--list", action="store_true", help="Показать весь инвентарь")
    parser.add_argument("--host", help="Показать переменные для указанного узла")
    args = parser.parse_args()

    if args.list:
        print(json.dumps(get_inventory(), indent=2))
    elif args.host:
        print(json.dumps(get_host_vars(args.host), indent=2))
    else:
        parser.print_help()

Подготовка и проверка скрипта:

  1. Сделайте файл исполняемым:

    chmod +x inventory-script.py
    
  2. Проверьте формирование списка узлов:

    ansible-inventory -i ./inventory-script.py --list
    
  3. Проверьте получение переменных конкретного узла:

    ./inventory-script.py --host web01.example.com
    

Внешний скрипт для выборки описания инвентаря из базы данных#

Следующий пример показывает как извлечь исходные данные об инвентаре из базы данных MySQL и сформировать описание в формате JSON:

Скрипт Python для динамического описания инвентаря, используя базу данных
#!/usr/bin/env python3

import argparse
import json
import mysql.connector
from mysql.connector import Error


class MySQLInventory:
    def __init__(self):
        self.connection = None
        self.inventory = {"_meta": {"hostvars": {}}}

    def connect_to_database(self):
        """Подключение к MySQL"""
        try:
            self.connection = mysql.connector.connect(
                host="localhost",
                database="inventory",
                user="ansible_user",
                password="your_password",
            )
        except Error as e:
            print(f"Ошибка подключения к MySQL: {e}")
            return False
        return True

    def get_inventory_from_db(self):
        """Получить инвентарь из базы данных"""
        if not self.connection:
            return self.inventory

        try:
            cursor = self.connection.cursor(dictionary=True)

            # Получаем хосты
            cursor.execute(
                """
                SELECT hostname, ip_address, ansible_user,
                       environment, role, ssh_port
                FROM servers
                WHERE active = 1
            """
            )

            servers = cursor.fetchall()

            # Создаем группы
            groups = {}

            for server in servers:
                hostname = server["hostname"]

                # Добавляем переменные хоста
                self.inventory["_meta"]["hostvars"][hostname] = {
                    "ansible_host": server["ip_address"],
                    "ansible_user": server["ansible_user"],
                    "ansible_port": server["ssh_port"] or 22,
                    "environment": server["environment"],
                    "role": server["role"],
                }

                # Группируем по окружениям
                env_group = f"env_{server['environment']}"
                if env_group not in groups:
                    groups[env_group] = {"hosts": [], "vars": {}}
                groups[env_group]["hosts"].append(hostname)

                # Группируем по ролям
                role_group = f"role_{server['role']}"
                if role_group not in groups:
                    groups[role_group] = {"hosts": [], "vars": {}}
                groups[role_group]["hosts"].append(hostname)

            # Добавляем группы в инвентарь
            self.inventory.update(groups)

        except Error as e:
            print(f"Ошибка выполнения запроса: {e}")
        finally:
            if self.connection.is_connected():
                cursor.close()
                self.connection.close()

        return self.inventory

    def get_host_vars(self, hostname):
        """Получить переменные для конкретного хоста"""
        inventory = self.get_inventory_from_db()
        return inventory["_meta"]["hostvars"].get(hostname, {})


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--list", action="store_true")
    parser.add_argument("--host", action="store")

    args = parser.parse_args()

    mysql_inventory = MySQLInventory()

    if args.list:
        if mysql_inventory.connect_to_database():
            inventory = mysql_inventory.get_inventory_from_db()
            print(json.dumps(inventory, indent=2))
        else:
            print(json.dumps({"_meta": {"hostvars": {}}}))
    elif args.host:
        if mysql_inventory.connect_to_database():
            host_vars = mysql_inventory.get_host_vars(args.host)
            print(json.dumps(host_vars, indent=2))
        else:
            print(json.dumps({}))


if __name__ == "__main__":
    main()

Использование расширения ansible.builtin.script#

Еще одним способом интеграции исполняемого файла динамической инвентаризации является использование расширения ansible.builtin.script. Для этого необходимо создать промежуточный файл в формате YAML (inventory-plugin.yml в примере) и в нем с помощью этого расширения указать путь к исполняемому файлу.

inventory-plugin.yml#
---
plugin: ansible.builtin.script
path: ./inventory-script.py

Проверка динамической инвентаризации:

ansible-inventory -i inventory-script.py -i inventory-plugin.yml --list

Динамическая инвентаризация в сценариях#

С помощью определенных модулей, например ansible.builtin.add_host и ansible.builtin.group_by, можно изменять описание инвентаря в процессе выполнения сценариев, как в приведенном здесь примере.

Первый сценарий в playbook-dyn-inv.yml вносит изменения в описание инвентаря путем добавления узла и формирования новых групп:

---
- name: Add and group hosts dynamically
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Add a new host to the inventory
      ansible.builtin.add_host:
        name: newhost.example.com
        groups: dynamic_group
        ansible_host: "192.168.56.100"
        os_family: Debian

    - name: Group hosts by OS family
      ansible.builtin.group_by:
        key: "os_family_{{ hostvars['inventory_hostname'].os_family | default('unknown') }}"

- name: Operate on dynamically grouped hosts
  hosts: os_family_Debian
  gather_facts: false
  tasks:
    - name: Show message
      debug:
        msg: This host is in the Debian OS family group.

Изменения, внесенные в одном сценарии (play) сохраняются в описании инвентаря, хранящемся в оперативной памяти (in-memory inventory). Таким образом, они доступны для всех загруженных сценариев, исполняемых после упомянутого.