Инвентаризация в серверной
Инвентаризация в серверной

Лень — двигатель прогресса. Именно по этой причине Ansible — лучший друг любого админа, который не хочет руками применять настройки к 1000 серверов. Я использую его на регулярной основе, но при этом именно тема инвентарных файлов каждый раз умудряется меня удивить. Поэтому в этой статье решил собрать всю общую информацию, начиная с inventory.ini и заканчивая плагинами для динамических инвентарей.

Инвентарь — это не просто список серверов. Это карта вашей инфраструктуры, в которой отражено, где что находится, как ко всему подключаться и какие настройки применять. Правильно организованный инвентарь может сэкономить кучу времени, а неправильный — превратить деплой в бесконечную отладку.

На чём держится инвентарь?

Давайте разберёмся с базовыми концепциями. Ansible использует push-модель: control node (машина, на которой мы запускаем Playbook) подключается к managed nodes (целевые хосты) и выполняет на них задачи. Инвентарь — это дорожная карта, которая отражает куда, как и когда можно идти.
По умолчанию Ansible ищет инвентарь в /etc/ansible/hosts, но мы можем указать альтернативный источник через флаг -i:

# Использовать конкретный файл инвентаря
ansible-playbook -i inventory.yml site.yml

# Использовать директорию с несколькими инвентарями
ansible-playbook -i inventory/ site.yml

# Комбинировать несколько источников
ansible-playbook -i static-hosts.yml -i aws_ec2.yml site.yml

Инвентарь решает три ключевые задачи:

  1. Определяет цели — какие хосты существуют и доступны для автоматизации.

  2. Организует структуру — как хосты логически группируются.

  3. Хранит конфигурацию — переменные для подключения и настройки каждого хоста.

Статические инвентари: INI vs YAML

Статические инвентари — это обычные файлы, содержащие списки целевых хостов. Ansible поддерживает два основных формата:

  • INI,

  • YAML.

INI формат

INI-формат прост и понятен для небольших инвентарей:

# Хосты без группы (ungrouped)
noname.example.com ansible_host=192.0.2.50 ansible_port=5555

# Группа веб-серверов
[webservers]
web1.example.com
web2.example.com ansible_host=10.0.1.11
web[3:5].example.com  # Диапазон: web3, web4, web5

# Группа баз данных с переменными
[dbservers]
db1.example.com http_port=80 maxconn=808
db2.example.com http_port=8080 maxconn=909

# Переменные для всей группы
[webservers:vars]
ansible_user=deploy
nginx_worker_processes=4

# Группа групп (children)
[production:children]
webservers
dbservers

[production:vars]
env=production

Важная особенность INI: значения, объявленные рядом с хостом (например, http_port=80), интерпретируются как переменные в Python. То есть http_port=80 станет числом 80, а debug=True — булевым True. Но при этом в секции :vars всё интерпретируется как строки, поэтому debug=True станет строкой "True".

YAML формат

YAML знакомый нам всем имеет древовидную структуру, которая более читаемая и гибкая.

---
all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
          ansible_host: 10.0.1.11
        web3.example.com:
        web4.example.com:
        web5.example.com:
      vars:
        ansible_user: deploy
        nginx_worker_processes: 4
        
    dbservers:
      hosts:
        db1.example.com:
          http_port: 80
          maxconn: 808
        db2.example.com:
          http_port: 8080
          maxconn: 909
          
    production:
      children:
        webservers:
        dbservers:
      vars:
        env: production

Преимущество YAML: Типы данных обрабатываются предсказуемо, нет путаницы между обычными и :vars значениями. Настоятельно рекомендую использовать YAML для всех новых проектов.

Диапазоны и паттерны

Ansible поддерживает удобные паттерны для определения множества хостов:

all:
  children:
    webservers:
      hosts:
        # Числовые диапазоны
        web[01:10].example.com:  # web01, web02, ..., web10
        
        # Числовые диапазоны с шагом
        web[11:20:3].example.com:  # web11, web14, web17, web20
        
        # Алфавитные диапазоны
        web[a:f].example.com:  # weba, webb, webc, webd, webe, webf
        
        # IPv4 диапазоны
        192.168.1.[1:5]:

Проверим, что получилось:

# Показать все хосты в инвентаре
ansible-inventory -i inventory.yml --list
{
    "_meta": {
        "hostvars": {},
        "profile": "inventory_legacy"
    },
    "all": {
        "children": [
            "ungrouped",
            "webservers"
        ]
    },
    "webservers": {
        "hosts": [
            "web01.example.com",
            "web02.example.com",
            "web03.example.com",
            "web04.example.com",
            "web05.example.com",
            "web06.example.com",
            "web07.example.com",
            "web08.example.com",
            "web09.example.com",
            "web10.example.com",
            "web11.example.com",
            "web14.example.com",
            "web17.example.com",
            "web20.example.com",
            "weba.example.com",
            "webb.example.com",
            "webc.example.com",
            "webd.example.com",
            "webe.example.com",
            "webf.example.com",
            "192.168.1.1",
            "192.168.1.2",
            "192.168.1.3",
            "192.168.1.4",
            "192.168.1.5"
        ]
    }
}

# Графическое представление групп
ansible-inventory -i inventory.yml --graph
@all:
  |--@ungrouped:
  |--@webservers:
  |  |--web01.example.com
  |  |--web02.example.com
  |  |--web03.example.com
  |  |--web04.example.com
  |  |--web05.example.com
  |  |--web06.example.com
  |  |--web07.example.com
  |  |--web08.example.com
  |  |--web09.example.com
  |  |--web10.example.com
  |  |--web11.example.com
  |  |--web14.example.com
  |  |--web17.example.com
  |  |--web20.example.com
  |  |--weba.example.com
  |  |--webb.example.com
  |  |--webc.example.com
  |  |--webd.example.com
  |  |--webe.example.com
  |  |--webf.example.com
  |  |--192.168.1.1
  |  |--192.168.1.2
  |  |--192.168.1.3
  |  |--192.168.1.4
  |  |--192.168.1.5

Уже выглядит неплохо, но если мы хотим решать серьёзные задачи, то нужны и механизмы посерьёзнее.

Группы и иерархия

Ansible даёт нам возможность организации хостов через группы и подгруппы.

Специальные группы

Ansible имеет две встроенные группы:

  • all — включает абсолютно все хосты в инвентаре,

  • ungrouped — хосты, которые не принадлежат ни к одной группе (кроме all).

Вложенные группы (children)

all:
  children:
    # Географические группы
    usa:
      children:
        southeast:
          children:
            atlanta:
              hosts:
                host1.example.com:
                host2.example.com:
            raleigh:
              hosts:
                host3.example.com:
          vars:
            timezone: "America/New_York"
            regional_server: foo.southeast.example.com
        
        west:
          children:
            california:
              hosts:
                host4.example.com:
          vars:
            timezone: "America/Los_Angeles"

Правило наследования: переменные дочерних групп имеют более высокий приоритет и переопределяют переменные родительских групп.

Паттерны селекторов хостов

Когда мы запускаем playbook, можно использовать хосты очень гибко:

# Одна группа
ansible-playbook -i inventory.yml site.yml --limit webservers

# Несколько групп (ИЛИ)
ansible-playbook site.yml --limit "webservers,dbservers"

# Пересечение групп (И)
ansible-playbook site.yml --limit "webservers:&production"

# Исключение (НЕ)
ansible-playbook site.yml --limit "production:!dbservers"

# Комбинированные паттерны
ansible-playbook site.yml --limit "webservers:&production:!web1"

# Диапазоны внутри группы
ansible-playbook site.yml --limit "webservers[0:5]"

# По регулярным выражениям
ansible-playbook site.yml --limit "~web[0-9]+"

Наша дорожная карта уже включает в себя множество объектов, но им явно не хватает описания, которое отлично реализуется через переменные.

Переменные: иерархия и приоритет

Переменные в Ansible — это очень мощный инструмент, но требующий внимательности. Потому что очень тяжело с первого раза догадаться, откуда именно мы получили такое значение, и на каком уровне она была переопределена в последний раз.

Определение переменных в инвентаре

all:
  children:
    webservers:
      hosts:
        web1:
          # Переменные на уровне хоста
          ansible_host: 192.168.1.10
          http_port: 8080
          max_connections: 1000
        web2:
          ansible_host: 192.168.1.11
          http_port: 8081
      vars:
        # Переменные на уровне группы
        ansible_user: deploy
        ansible_connection: ssh

Ключевое правило: Host variables переопределяют group variables. В целом это правило легко запомнить, так как везде общие переменные имеют более низкий приоритет.

group_vars и host_vars

Для больших проектов хранить переменные прямо в инвентаре неудобно. Ansible поддерживает внешние файлы переменных:

ansible_project/
├── inventory/
│   └── production
├── group_vars/
│   ├── all.yml              # Переменные для всех хостов
│   ├── webservers.yml       # Переменные для группы webservers
│   └── dbservers.yml        # Переменные для группы dbservers
└── host_vars/
    ├── web1.yml             # Переменные для конкретного хоста web1
    └── db1.yml              # Переменные для хоста db1

Ansible ищет group_vars/ и host_vars/ в двух местах:

  1. Относительно inventory файла.

  2. Относительно playbook.

Важно: если переменные определены в обоих местах, то выше приоритет у тех, что определены на уровне playbook.

Давайте посмотрим на пример:

# group_vars/webservers.yml
---
nginx_port: 80
ssl_enabled: true

application_config:
  database_host: "db.example.com"
  database_port: 5432

Продвинутая организация с поддиректориями

Для сложной архитектуры можно разбить переменные по файлам:

group_vars/
├── all/
│   ├── 00_main.yml          # Загружается первым
│   ├── 10_network.yml
│   ├── 20_security.yml
│   └── 99_vault.yml         # Загружается последним
└── webservers/
    ├── nginx.yml
    ├── app.yml
    └── vault.yml

Ansible загружает файлы в лексикографическом порядке. Если в файлах есть одинаковые переменные, последний загруженный файл выигрывает, поэтому префиксы 00_, 10_, 20_ полезны для контроля порядка загрузки.

Приоритет переменных: полная картина

Теперь самое интересное. Ansible имеет 22 уровня приоритета переменных. От низшего к высшему:

Приоритет

Источник

Область применения

1

command line values (например, -u my_user)

CLI опции (НЕ переменные)

2

role defaults

role/defaults/main.yml

3

inventory file group vars

Переменные групп в инвентаре

4

inventory group_vars/all

group_vars/all относительно inventory

5

playbook group_vars/all

group_vars/all относительно playbook

6

inventory group_vars/*

group_vars групп относительно inventory

7

playbook group_vars/*

group_vars групп относительно playbook

8

inventory file host vars

Переменные хостов в инвентаре

9

inventory host_vars/*

host_vars относительно inventory

10

playbook host_vars/*

host_vars относительно playbook

11

host facts / cached set_facts

Собранные факты о системе

12

play vars

Переменные в play

13

play vars_prompt

Интерактивный ввод

14

play vars_files

Файлы переменных

15

role vars

role/vars/main.yml

16

block vars

Переменные блока

17

task vars

Переменные задачи

18

include_vars

Динамически загруженные

19

set_facts / registered vars

Установленные в runtime

20

role (and include_role) params

Параметры роли

21

include params

Параметры include

22

extra vars (-e "var=value")

Абсолютный чемпион

Очень важно помнить, что -e — это последняя точка правды, эта информация очень сильно может Вас выручить при отладке роли.

Приоритет групп одного уровня

Если хост принадлежит нескольким группам одного уровня, Ansible загружает их в алфавитном порядке. Последняя загруженная группа побеждает:

a_group:
  vars:
    test_var: a
b_group:
  vars:
    test_var: b

Результат: testvar == b

Но также можно управлять приоритетом через ansible_group_priority:

a_group:
  vars:
    testvar: a
    ansible_group_priority: 10
b_group:
  vars:
    testvar: b

Теперь testvar == a

Важно: ansible_group_priority можно устанавливать только в inventory source, в group_vars/ политика не работает.

Отладка переменных

Ряд полезных для дебага команд:

# Показать все переменные для конкретного хоста
ansible-inventory -i inventory.yml --host web1

# Вывод в YAML формате (читабельнее)
ansible-inventory -i inventory.yml --host web1 --yaml

# Показать переменную через ad-hoc команду
ansible -i inventory.yml web1 -m debug -a "var=http_port"

# Показать все переменные хоста
ansible -i inventory.yml web1 -m debug -a "var=hostvars[inventory_hostname]"

Или в playbook:

- hosts: webservers
  tasks:
    - name: Debug конкретной переменной
      debug:
        msg: "HTTP port is {{ http_port | default('not set') }}"
    
    - name: Debug всех переменных хоста
      debug:
        var: hostvars[inventory_hostname]
      verbosity: 2

Динамические инвентари — выход для самых ленивых

Если Вам уже и инвентарь самому писать лень, то можно сложить эту обязанность на плагин. Ну а если быть серьёзным, то в реалиях постоянно меняющейся в облаке инфраструктуры это просто лучший вариант достучаться до всех интересующих Вас узлов.

Inventory Plugins

Inventory plugins — это современный и рекомендуемый способ работы с динамическими инвентарями. Преимущества:

  • Стандартизированный формат конфигурации (YAML).

  • Встроенное кэширование.

  • Фильтрация и группировка из коробки.

  • Поддержка от облачных провайдеров.

  • Не нужно писать код.

AWS EC2 Plugin

Самый популярный плагин для работы с AWS. Сначала установим коллекцию:

ansible-galaxy collection install amazon.aws
pip3 install boto3 botocore

Создаём файл конфигурации aws_ec2.yml (имя ОБЯЗАТЕЛЬНО должно заканчиваться на aws_ec2.yml или aws_ec2.yaml):

---
plugin: amazon.aws.aws_ec2

# Регионы для сканирования
regions:
  - us-east-1
  - eu-west-1

# Фильтрация инстансов
filters:
  # Только running инстансы
  instance-state-name: running
  
  # По тегам
  tag:Environment:
    - production
    - staging

# Группировка по тегам
keyed_groups:
  # Группа по тегу Name: tag_Name_webserver
  - key: tags.Name
    prefix: tag_Name
    separator: "_"
  
  # Группа по тегу Role: role_frontend
  - key: tags.Role
    prefix: role
  
  # Группа по типу инстанса: instance_type_t2_micro
  - prefix: instance_type
    key: instance_type
  
  # Группа по региону: aws_region_us_east_1
  - key: placement.region
    prefix: aws_region

# Группировка через условия
groups:
  # Группа development для хостов с тегом env=devel
  development: "'devel' in (tags.Environment|lower)"
  
  # Группа webservers для хостов с тегом Role=web
  webservers: "tags.Role == 'web'"

# Настройка hostname
hostnames:
  - dns-name          # Предпочитаем DNS имя
  - private-ip-address  # Fallback на private IP

# Создание переменных
compose:
  # ansible_host будет private IP
  ansible_host: private_ip_address
  
  # Кастомные переменные
  ec2_region: placement.region
  ec2_az: placement.availability_zone

Если у вас Ansible control node внутри AWS с IAM ролью, плагин автоматически использует роль. Если снаружи — используем креды:

---
plugin: amazon.aws.aws_ec2

# Явное указание credentials
aws_access_key: ...
aws_secret_key: ...

# Или использовать профиль
boto_profile: production

# Или assume role
iam_role_arn: arn:aws:iam::123456789:role/ansible-role

GCP Compute Plugin

Для Google Cloud похожий подход:

ansible-galaxy collection install google.cloud
pip3 install requests google-auth

Создаём service account в GCP и скачиваем JSON ключ. Затем конфигурация:

---
plugin: google.cloud.gcp_compute

# GCP проект
projects:
  - my-gcp-project-id

# Аутентификация через service account
auth_kind: serviceaccount
service_account_file: /path/to/service-account.json

# Фильтрация
filters:
  - status = RUNNING
  - labels.environment = production

# Группировка по лейблам
keyed_groups:
  - key: labels.role
    prefix: role
  - key: labels.environment
    prefix: env
  - key: zone
    prefix: zone

# Hostname
hostnames:
  - name

compose:
  ansible_host: networkInterfaces[0].accessConfigs[0].natIP

Azure Plugin

ansible-galaxy collection install azure.azcollection
pip3 install azure-cli

Аутентификация через Azure CLI:

az login

Конфигурация azure_rm.yml:

---
plugin: azure.azcollection.azure_rm

# Включить все подписки
include_vm_resource_groups:
  - my-resource-group

# Фильтрация
exclude_host_filters:
  - powerstate != 'running'

# Группировка
keyed_groups:
  - prefix: tag
    key: tags
  - prefix: azure_loc
    key: location
  - prefix: azure_os
    key: os_profile.system

# Hostname
hostnames:
  - name
  - public_ip_name

compose:
  ansible_host: public_ipv4_addresses[0]

NetBox Plugin

NetBox — это отличный open source вариант для ssot Вашей инфраструктуры, поэтому грех не воспользоваться им как источником информации для инвентаря.

ansible-galaxy collection install netbox.netbox
pip3 install pynetbox

Создаём файл конфигурации netbox_inventory.yml:

---
plugin: netbox.netbox.nb_inventory

# NetBox API endpoint и токен
api_endpoint: https://netbox.example.com
token: your-netbox-api-token

# Или через переменные окружения
# export NETBOX_API=https://netbox.example.com
# export NETBOX_TOKEN=your-token

# SSL проверка
validate_certs: true

# Фильтрация устройств
device_query_filters:
  # По статусу: active, offline, planned, staged, failed
  - status: active
  
  # По ролям устройств
  - role:
    - server
    - network
  
  # По сайтам/локациям
  - site:
    - datacenter-1
    - datacenter-2
  
  # По тегам
  - tag:
    - production
    - ansible-managed

# Фильтрация виртуальных машин
virtual_machine_query_filters:
  - status: active
  - cluster: production-cluster

# Группировка по атрибутам NetBox
group_by:
  - site
  - device_role
  - platform
  - manufacturer
  - tags
  - tenant
  - cluster
  - device_type

# Группировка через keyed_groups
keyed_groups:
  # По custom fields: cf_environment_production
  - key: custom_fields.environment
    prefix: cf_environment
    separator: "_"
  
  # По региону из custom field: region_eu_west
  - key: custom_fields.region
    prefix: region
  
  # Комбинация site + role: dc1_webservers
  - key: 'site.slug ~ "_" ~ device_role.slug'
    prefix: ""
  
  # По rack: rack_a01
  - key: rack.name
    prefix: rack
    separator: "_"

# Группировка через условия
groups:
  # Production серверы
  production_servers: >-
    'production' in tags and device_role.slug == 'server'
  
  # Критические системы
  critical: >-
    custom_fields.criticality == 'high' or 'critical' in tags
  
  # Физические серверы
  physical: "device_role is defined"
  
  # Виртуальные машины  
  virtual: "cluster is defined"
  
  # Требуют обновления
  needs_update: >-
    custom_fields.os_version is defined and
    custom_fields.os_version < '22.04'

# Настройка hostname
compose:
  # Primary IP как ansible_host (убираем маску подсети)
  ansible_host: primary_ip4.address | regex_replace('/.*', '')
  
  # Альтернатива через интерфейсы
  # ansible_host: interfaces[0].ip_addresses[0].address | regex_replace('/.*', '')
  
  # Custom fields как переменные
  ansible_user: custom_fields.ansible_user | default('ansible')
  ansible_port: custom_fields.ssh_port | default(22)
  
  # Дополнительные переменные
  environment: custom_fields.environment | default('development')
  os_version: custom_fields.os_version
  backup_enabled: custom_fields.backup_enabled | default(true)
  monitoring_enabled: custom_fields.monitoring | default(true)
  
  # Site-specific настройки
  ntp_server: '"ntp." ~ site.slug ~ ".company.com"'
  
  # NetBox ID для обратной интеграции
  netbox_device_id: id

Композиция источников

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

# Комбинировать несколько источников
ansible-playbook -i static-hosts.yml -i aws_ec2.yml -i azure_rm.yml deploy.yml

Или создать директорию инвентаря:

inventory/
├── 01-static.yml
├── 02-aws_ec2.yml
├── 03-azure_rm.yml
└── 04-gcp.yml

Как уже говорилось ранее, префиксы 01-, 02- помогают контролировать порядок, последний загруженный источник может переопределять переменные предыдущих.

ansible-playbook -i inventory/ deploy.yml

Кэширование динамических инвентарей

Динамические инвентари делают API вызовы, которые не всегда могут похвастаться скоростью и стабильностью. Для решения этой проблемы мы можем включить кэширование:

ansible.cfg:

[defaults]
inventory = ./inventory/

[inventory]
enable_plugins = amazon.aws.aws_ec2, azure.azcollection.azure_rm, google.cloud.gcp_compute
cache = True
cache_plugin = jsonfile
cache_connection = /tmp/ansible_inventory_cache
cache_timeout = 3600

Или в конфигурации плагина:

---
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1

# Кэширование
cache: true
cache_timeout: 3600
cache_connection: /tmp/aws_inventory_cache

Отладка и troubleshooting

Даже с правильно организованным инвентарём иногда что-то идёт не так. Вот мой золотой набор команд для отладки:

# Показать весь инвентарь в JSON
ansible-inventory -i inventory.yml --list

# Показать граф групп
ansible-inventory -i inventory.yml --graph

# Показать граф с переменными
ansible-inventory -i inventory.yml --graph --vars

# Показать переменные конкретного хоста
ansible-inventory -i inventory.yml --host web1

# Вывод в YAML (читабельнее)
ansible-inventory -i inventory.yml --list --yaml

# Проверка connectivity
ansible -i inventory.yml all -m ping

# Проверка конкретной группы
ansible -i inventory.yml webservers -m ping

# Ad-hoc команда для проверки переменной
ansible -i inventory.yml web1 -m debug -a "var=http_port"

# Собрать facts о хосте
ansible -i inventory.yml web1 -m setup

Некоторые из них уже встречались ранее, но это скорее как общая шпаргалка

Оптимизация производительности

Для больших инвентарей производительность становится важным аспектом, особенно, когда роль запускается по расписанию. Все настройки, указанные здесь, применяются в ansible.cfg

SSH оптимизация

[ssh_connection]
# ControlMaster для переиспользования SSH соединений
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
control_path = /tmp/ansible-ssh-%%h-%%p-%%r

# SSH pipelining (меньше SSH соединений)
pipelining = True

Параллелизм

[defaults]
# Количество параллельных процессов (default: 5)
forks = 50

Умный сбор фактов

[defaults]
# Собирать факты, только когда нужно
gathering = smart

# Кэширование фактов
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400

В playbook можно отключить или ограничить факты:

- hosts: all
  gather_facts: false  # Не собирать совсем
  tasks:
    - setup:
        gather_subset:
          - '!all'
          - '!any'
          - min  # Только минимальный набор

Стратегии выполнения

- hosts: webservers
  # Linear: выполнить task на всех хостах, перейти к следующей
  strategy: linear
  
- hosts: appservers
  # Free: хосты выполняют задачи независимо
  strategy: free
  
- hosts: dbservers
  # Батчами: по 5 хостов одновременно
  serial: 5
  
- hosts: production
  # Батчами: 20% хостов за раз
  serial: "20%"
  
  # Прервать, если >25% хостов упали
  max_fail_percentage: 25

Best Practices: чек-лист

✅ Организация

  • Используйте YAML формат.

  • Структурируйте по принципу What-Where-When (webservers/atlanta/production).

  • Разделяйте окружения в отдельные директории для крупных проектов (inventories/dev/, inventories/prod/).

  • Используйте group_vars/ и host_vars/ вместо переменных в инвентаре.

  • Документируйте структуру.

✅ Переменные

  • Host vars переопределяют group vars (общего всегда менее важно).

  • Playbook vars переопределяют inventory vars.

  • Extra vars (-e) переопределяют всё.

  • Используйте {{ var | default('value') }} для optional переменных.

  • Документируйте переменные в defaults/main.yml ролей.

✅ Динамические инвентари

  • Включите кэширование для ускорения.

  • Префиксуйте файлы цифрами для контроля порядка загрузки.

  • Комбинируйте статические и динамические источники.

❌ Чего избегать

  • Не смешивайте переменные разных окружений.

  • Не храните всё в одном огромном файле.

  • Не используйте одинаковые имена для разных концепций.

  • Не игнорируйте warnings от ansible-inventory.

© 2025 ООО «МТ ФИНАНС»

Комментарии (0)