Динамическая инвентаризация#
Динамическая инвентаризация предназначена для автоматического формирования описания инвентаря на основе внешних источников данных (файлы, 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
с метаданными описана далее.
Этапы инвентаризации#
Основные этапы динамической инвентаризации представлены следующими шагами:
Ansible получает источники описания инвентаря, указанные аргументами
-i
.Ansible обрабатывает каждый источник с помощью соответствующего расширения Ansible или путем прямого запуска исполняемого файла. Результатом обработки каждого источника является описание инвентаря в формате JSON.
Ansible анализирует полученные данные и формирует группы, переменные и метаданные для каждого сценария.
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()
Подготовка и проверка скрипта:
Сделайте файл исполняемым:
chmod +x inventory-script.py
Проверьте формирование списка узлов:
ansible-inventory -i ./inventory-script.py --list
Проверьте получение переменных конкретного узла:
./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
в примере) и в нем с помощью этого расширения указать путь к исполняемому файлу.
---
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). Таким образом, они доступны для всех загруженных сценариев, исполняемых после упомянутого.