В небольшом Ansible-проекте можно долго жить с одним файлом hosts, парой переменных в playbook и договорённостями в голове. Но как только появляются отдельные окружения, например staging и production, всё быстро усложняется. Одни серверы должны подключаться к тестовой базе, другие — к боевой. Где-то включён debug, где-то жёсткие лимиты. У одного хоста нестандартный SSH-порт, у другого другой PHP-FPM socket, а у третьего вообще отдельный upstream.
Если в этот момент не привести проект к нормальной структуре, через пару месяцев вы получите набор YAML-файлов, где переменные дублируются, значения конфликтуют, а любое изменение в production страшно запускать. Хорошая новость в том, что Ansible давно даёт для этого штатные механизмы: inventory, group_vars, host_vars и понятную иерархию окружений.
Ниже разберём практичную схему: как организовать inventory для нескольких сред, где хранить общие и специфичные переменные, как не запутаться в group_vars и host_vars, и почему понимание приоритета переменных важнее, чем кажется на старте.
Сразу практический тезис: если у вас есть staging и production, не пытайтесь различать их только одной переменной внутри общего inventory, если инфраструктура уже заметно расходится. В большинстве случаев удобнее явно оформить среды как отдельные группы или даже как отдельные inventory-источники.
И ещё один тезис: структура проекта должна отвечать на три вопроса без поиска по репозиторию. Какие есть хосты? Какие есть среды? Откуда берётся конкретное значение переменной на конкретной машине? Если структура на это не отвечает, её стоит упростить.
Что именно ломается без нормальной структуры
Проблемы обычно повторяются от проекта к проекту. Вроде бы всё работает, но любое изменение становится рискованным. Типичный сценарий выглядит так: сначала в inventory лежит список серверов, потом рядом появляются переменные группы, потом кто-то добавляет исключения на уровне playbook, а через время уже непонятно, почему на staging nginx слушает один порт, а на production другой.
Главная причина — переменные распределены случайно. Часть живёт в inventory, часть в group_vars/all.yml, часть в ролях, часть в host_vars, а критичные значения вообще передаются через -e в CI. Технически Ansible это переварит, но сопровождать такую схему комфортно не получится.
Хорошая Ansible-структура — это не «красивый репозиторий», а предсказуемость. Вы должны заранее понимать, что применится к хосту ещё до запуска playbook.
Отсюда и простое правило: общие параметры кладём в общее место, отличия сред — на уровень среды, уникальные исключения — только в host_vars и только когда они действительно нужны.
Базовая структура Ansible-проекта для нескольких окружений
Для большинства команд удобна структура, где inventory разделён по средам, а переменные лежат рядом с ними. Это делает проект понятнее и уменьшает вероятность случайно применить staging-настройки к production.
ansible/
├── ansible.cfg
├── inventories/
│ ├── staging/
│ │ ├── hosts.yml
│ │ ├── group_vars/
│ │ │ ├── all.yml
│ │ │ ├── web.yml
│ │ │ ├── db.yml
│ │ │ └── app.yml
│ │ └── host_vars/
│ │ └── stage-web-01.yml
│ └── production/
│ ├── hosts.yml
│ ├── group_vars/
│ │ ├── all.yml
│ │ ├── web.yml
│ │ ├── db.yml
│ │ └── app.yml
│ └── host_vars/
│ └── prod-web-01.yml
├── playbooks/
│ ├── site.yml
│ ├── web.yml
│ └── db.yml
└── roles/
├── common/
├── nginx/
├── php/
└── postgresql/
Это не единственно верный вариант, но он хорошо масштабируется. У staging и production свои inventory-файлы, свои group_vars и свои host_vars. Общие playbook и роли при этом переиспользуются.
Плюс такого подхода в том, что окружения изолированы логически. Минус — если у вас много одинаковых значений, придётся решить, где хранить действительно глобальные переменные, чтобы не копировать их между средами. Обычно для этого используют defaults ролей и аккуратно организованные переменные уровня all.
Если staging и production разворачиваются на отдельных серверах, для такой схемы чаще всего удобнее использовать VDS: проще повторять топологию, держать изоляцию окружений и не упираться в ограничения общего сервера.
Пример inventory в YAML
Если вы используете YAML inventory, структура читается заметно лучше, чем старый INI-формат, особенно когда есть вложенные группы.
all:
children:
web:
hosts:
stage-web-01:
ansible_host: 10.10.10.11
stage-web-02:
ansible_host: 10.10.10.12
app:
hosts:
stage-app-01:
ansible_host: 10.10.20.11
db:
hosts:
stage-db-01:
ansible_host: 10.10.30.11
Для production будет такой же файл, но со своими адресами, составом узлов и, возможно, иной топологией. Например, на staging может быть один узел БД, а на production — primary и replica. Это нормально: inventory должен отражать реальность, а не пытаться сделать среды искусственно одинаковыми.

Когда базовая структура уже определена, можно отдельно подумать о стандартах именования, хранении секретов и правилах ревью. Но сначала важно добиться того, чтобы любая среда читалась как самостоятельная и понятная часть проекта.
Как распределять group_vars и host_vars без дублирования
Частый вопрос — что класть в group_vars, а что в host_vars. Ответ простой: всё, что относится к роли группы серверов, храните в group_vars. Всё, что уникально для конкретного узла, — в host_vars.
Например, если все web-серверы в production должны иметь одинаковые параметры nginx, имя системного пользователя приложения, директорию релиза и настройки healthcheck — это кандидат в group_vars/web.yml. Если только один хост слушает нестандартный порт SSH или имеет дополнительный диск, это уже host_vars.
app_name: myapp
app_user: myapp
app_group: myapp
deploy_path: /var/www/myapp
nginx_vhost_template: app.conf.j2
healthcheck_path: /healthz
Пример для production group_vars/all.yml:
env_name: production
app_env: prod
debug_enabled: false
backup_enabled: true
monitoring_enabled: true
Пример для staging group_vars/all.yml:
env_name: staging
app_env: stage
debug_enabled: true
backup_enabled: false
monitoring_enabled: true
Идея здесь в том, что у вас есть одна и та же логика ролей, но среда меняет поведение через переменные. Роль nginx не должна знать, staging это или production. Она должна читать уже подготовленные значения.
Когда host_vars действительно нужны
У многих проектов host_vars со временем превращаются в свалку. Это тревожный сигнал. Если у вас половина бизнес-логики конфигурации лежит на уровне отдельных хостов, значит, вы недоопределили группы.
Например, если два web-сервера отличаются от третьего не случайно, а постоянно, возможно, нужна дополнительная группа: web_canary, web_legacy, queue, cron. Тогда отличия будут выражены структурно, а не через набор частных исключений.
Используйте
host_varsдля адресов, индивидуальных override и редких особенностей узла.Используйте
group_varsдля роли сервера в инфраструктуре.Если host-specific переменных слишком много, пересмотрите модель групп.
Staging и production: одна схема, разные цели
В теме разделения сред часто допускают две крайности. Первая — делают среды абсолютно идентичными, хотя staging для этого слишком дорогой. Вторая — staging настолько отличается от production, что перестаёт быть полезным для проверки изменений. Истина обычно посередине.
Staging не обязан повторять production по мощности, но должен повторять его по архитектурным принципам. Если в production приложение работает за reverse proxy, с отдельным app-слоем и внешней БД, staging желательно держать по той же схеме, даже если там меньше узлов и скромнее ресурсы.
С точки зрения Ansible это значит, что структура ролей и логика deploy должны быть общими, а отличаться должны только переменные среды. Например, число воркеров, размеры пулов, флаги debug, доменные имена, backup policy, адреса мониторинга и параметры интеграций.
php_fpm_pm_max_children: 20
nginx_worker_connections: 1024
app_domain: stage.example.internal
Для production:
php_fpm_pm_max_children: 80
nginx_worker_connections: 4096
app_domain: example.com
Если разница между средами выражена только значениями, а не переписанными ролями, вы движетесь в правильную сторону.
Если у вас в staging и production используются разные домены и поддомены, полезно заранее продумать не только inventory, но и схему переездов, редиректов и сертификатов. По этой теме пригодится материал про миграцию домена, 301, HSTS и SSL.
Приоритет переменных: почему одно значение «побеждает» другое
Тема приоритета переменных пугает новичков не зря. Большая часть странных эффектов в Ansible возникает не из-за багов, а из-за того, что одно и то же имя переменной объявлено в нескольких местах. Например, в defaults роли, в group_vars/all, в group_vars/web, в host_vars и ещё передано через -e в extra vars.
Полную официальную таблицу помнить наизусть не обязательно, но базовую практическую иерархию понимать нужно. В обычной жизни мыслить можно так: defaults роли — самое слабое значение; групповые и хостовые переменные — сильнее; переменные, переданные напрямую через -e, почти всегда самые приоритетные.
Из-за этого есть простое эксплуатационное правило: не используйте одни и те же имена переменных на всех уровнях без необходимости. Если переменная действительно задаётся пользователем роли, оставьте её в defaults/main.yml. Если это внутренняя служебная переменная роли, лучше дать ей специфичное имя и не переопределять снаружи без крайней нужды.
Если вам приходится открывать пять файлов, чтобы понять итоговое значение переменной, проблема уже не в Ansible, а в дизайне проекта.
Полезный приём — разделять «внешние» и «внутренние» переменные роли. Например, снаружи задаётся app_domain, а внутри роли из неё вычисляется путь к конфигу, имя upstream и прочие производные параметры. Это уменьшает хаос и количество случайных конфликтов.
Практика именования переменных
Одна из лучших привычек — использовать префиксы по контексту. Не просто user или port, а nginx_listen_port, app_user, postgresql_max_connections. Тогда шанс коллизии сильно ниже, особенно в больших проектах с несколькими ролями.
Это особенно важно, когда над инфраструктурой работает не один человек. Через полгода никто не вспомнит, к чему относился абстрактный timeout, а вот nginx_proxy_read_timeout и php_fpm_request_terminate_timeout уже не перепутать.
Секреты здесь тоже лучше отделять от обычных переменных. Если вы храните чувствительные данные в репозитории, делайте это в зашифрованном виде. Для командной работы может быть полезен и отдельный разбор про SOPS, age и хранение секретов в GitOps-пайплайнах.
Если инфраструктура публикует сервисы по HTTPS, не откладывайте управление сертификатами на потом: для production это такая же часть предсказуемой конфигурации, как inventory и переменные. В этом контексте могут пригодиться SSL-сертификаты.
Рабочая модель project structure для команды
Если нужен не просто учебный пример, а схема, с которой удобно жить в команде, я бы рекомендовал следующее разделение ответственности.
inventories/stagingиinventories/productionотвечают за состав окружений и их переменные.rolesсодержат только переиспользуемую логику и разумные defaults.playbooksописывают сценарии применения ролей к группам.Секреты не смешиваются с обычными vars и хранятся отдельно в зашифрованном виде.
Пример site.yml:
- name: Configure web servers
hosts: web
become: true
roles:
- common
- nginx
- php
- name: Configure app servers
hosts: app
become: true
roles:
- common
- name: Configure database servers
hosts: db
become: true
roles:
- common
- postgresql
Запуск тогда становится предсказуемым:
ansible-playbook -i inventories/staging/hosts.yml playbooks/site.yml
ansible-playbook -i inventories/production/hosts.yml playbooks/site.yml
На уровне CI это тоже удобно: одна и та же команда, только с разным inventory. Минимум магии, максимум повторяемости.

Частые ошибки в inventory и переменных
Самая частая ошибка — пытаться хранить слишком много логики в inventory. Inventory должен описывать инфраструктуру: хосты, группы, адреса, connection details и средовые отличия. Но как только туда начинают складывать пол-логики приложения, сопровождать это становится больно.
Вторая ошибка — копировать целиком group_vars между staging и production, а потом годами поддерживать два почти одинаковых дерева. В результате любое изменение надо не забыть внести в оба места. Лучше выносить общие значения в defaults ролей или в общую переменную уровня проекта, а в средах держать только отличия.
Третья ошибка — не проверять итоговую картину inventory перед прогоном. В Ansible есть штатные способы посмотреть, как он видит группы и хосты. Это полезно делать после любого заметного рефакторинга.
ansible-inventory -i inventories/staging/hosts.yml --graph
ansible-inventory -i inventories/staging/hosts.yml --list
ansible-inventory -i inventories/production/hosts.yml --graph
Четвёртая ошибка — использовать host_vars как средство срочного «быстрого фикса». Один раз это спасает, но потом там накапливаются исключения, которые никто не ревизует. Если override стал постоянным, его нужно либо перенести в группу, либо переосмыслить модель ролей.
Как выбрать между одним inventory и несколькими
Технически можно держать staging и production в одном inventory, оформив их как дочерние группы. Для маленьких проектов это бывает удобно. Но когда среды начинают заметно различаться по составу серверов, ролям узлов и перечню переменных, отдельные inventory обычно проще поддерживать.
Один inventory удобен, если:
среды очень похожи по топологии;
команда небольшая;
важно быстро видеть всё дерево хостов в одном месте.
Раздельные inventory удобнее, если:
production существенно сложнее staging;
есть разные источники хостов или разные правила доступа;
вы хотите минимизировать риск случайного запуска не по той среде.
На практике я чаще советую отдельные inventory-каталоги по средам. Это чуть более многословно, зато сильно понятнее в эксплуатации.
Минимальный набор best practices, который реально окупается
Есть много красивых рекомендаций, но если оставить только то, что даёт максимальную пользу в админской реальности, получится довольно компактный набор.
Используйте YAML inventory и человекочитаемые имена групп.
Разделяйте окружения явно:
staging,production, при необходимостиdev.Храните общую логику в ролях, а различия сред — в
group_vars.Используйте
host_varsтолько для настоящих исключений.Не злоупотребляйте
-eдля постоянных значений.Давайте переменным контекстные имена.
Проверяйте inventory командами
ansible-inventoryпосле изменений.Документируйте, где должен жить каждый класс переменных.
Последний пункт особенно недооценивают. Даже короткий README в корне проекта с правилами наподобие «средовые переменные — только в inventories/*/group_vars/all.yml» экономит массу времени и споров.
Итоговая схема, которую не стыдно развивать дальше
Если свести всё к одному рабочему шаблону, то для большинства команд удобен такой подход: отдельные inventory для staging и production, общие playbook, переиспользуемые роли, group_vars для групп и сред, host_vars только для исключений, плюс аккуратные имена переменных и понимание их приоритета.
Именно такая структура лучше всего отвечает на вопросы эксплуатации: что изменится при запуске, откуда берётся значение, как безопасно расширить проект и куда положить новую настройку без создания очередного слоя хаоса.
Если вы только приводите в порядок существующий репозиторий, не пытайтесь переписать всё сразу. Начните с разделения inventory по средам, затем вынесите повторяющиеся значения в group_vars, после этого ревизуйте host_vars и уже потом полируйте роли. Пошаговая миграция почти всегда безопаснее большого «идеального» рефакторинга.
У Ansible хватает нюансов, но именно структура проекта определяет, станет он надёжным рабочим инструментом или источником постоянных сюрпризов. Если inventory и переменные устроены понятно, staging и production перестают мешать друг другу, а изменения становятся предсказуемыми. Для админа это и есть главный признак хорошей автоматизации.


