Один файл со списком серверов — и любая команда выполняется на всех сразу
ansible.cfg--limitcommand и shellbecome, проверять изменения без применения и читать факты узловВ модуле 01 адрес узла, пользователь и ключ писались в каждой команде. Список узлов выносят в файл — инвентарь. Самый распространённый формат — INI:
# Учебные узлы курса. Имя слева — как узел называется в Ansible, ansible_host — куда подключаться.
[web]
web1 ansible_host=172.30.50.11
web2 ansible_host=172.30.50.12
[db]
db1 ansible_host=172.30.50.21
# Группа из групп: все узлы учебного «продакшна»
[prod:children]
web
db
[web:vars]
site_title=Сервис цен
[web] — группа, под ней узлы. Узел может входить в несколько групп.web1 — имя узла в Ansible. Оно попадает в вывод и в переменную inventory_hostname. ansible_host — куда на самом деле подключаться; если имя узла разрешается в DNS, адрес можно не писать.[prod:children] — группа из групп. Так описывают окружения (prod, stage) или площадки.[web:vars] — переменные для всех узлов группы. Переменные — тема модуля 04; здесь одна, для примера.all — все узлы, ungrouped — узлы вне групп.Как Ansible понял инвентарь, показывает ansible-inventory: дерево групп и все переменные конкретного узла:
$ ansible-inventory --graph
@all:
|--@ungrouped:
|--@prod:
| |--@web:
| | |--web1
| | |--web2
| |--@db:
| | |--db1
$ ansible-inventory --host web1
{
"ansible_host": "172.30.50.11",
"site_title": "Сервис цен"
}
То же самое можно записать в YAML — длиннее, зато удобнее, когда у узлов много переменных:
# Тот же инвентарь в формате YAML
all:
children:
web:
hosts:
web1:
ansible_host: 172.30.50.11
web2:
ansible_host: 172.30.50.12
vars:
site_title: Сервис цен
db:
hosts:
db1:
ansible_host: 172.30.50.21
prod:
children:
web:
db:
$ ansible-inventory -i inventory.yml --graph
@all:
|--@ungrouped:
|--@web:
| |--web1
| |--web2
|--@db:
| |--db1
|--@prod:
| |--@web:
| | |--web1
| | |--web2
| |--@db:
| | |--db1
Инвентарь не обязательно файл. Для облаков есть динамические инвентари — плагины, которые при запуске спрашивают у облака список машин и их метки. Принцип тот же: на выходе группы, узлы и переменные.
Пользователь, ключ и путь к инвентарю тоже одинаковы для всех команд — их пишут в файл настроек проекта:
[defaults]
inventory = inventory.ini
remote_user = ansible
private_key_file = ~/.ssh/ansible_ed25519
# Python на узлах искать как раньше, но без предупреждения
interpreter_python = auto_silent
# сколько узлов обрабатывать одновременно (по умолчанию 5)
forks = 10
Ansible ищет настройки в таком порядке и берёт первый найденный файл: переменная окружения ANSIBLE_CONFIG, ansible.cfg в текущем каталоге, ~/.ansible.cfg, /etc/ansible/ansible.cfg. Файлы не объединяются. Поэтому каждый проект держит свой ansible.cfg рядом с плейбуками и запускает Ansible из своего каталога. Какие настройки действуют сейчас и откуда они взялись:
$ ansible-config dump --only-changed | grep -E "^(DEFAULT_FORKS|DEFAULT_REMOTE_USER|INTERPRETER_PYTHON)" | sed "s/(.*)//"
DEFAULT_FORKS = 10
DEFAULT_REMOTE_USER = ansible
INTERPRETER_PYTHON = auto_silent
interpreter_python = auto_silent убирает предупреждение из модуля 01: Ansible по-прежнему сам находит Python на узле, но больше не напоминает об этом. Строже — указать путь явно: переменная ansible_python_interpreter со значением /usr/bin/python3 в инвентаре.
ansible.cfg из него: чужой файл настроек мог бы подменить ключ, пользователя или модули. Держите проект в своём домашнем каталоге или в каталоге с правами только для себя.Теперь хватает имени группы и модуля:
$ ansible all -m ping
web2 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.13"
},
"changed": false,
"ping": "pong"
}
db1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.13"
},
"changed": false,
"ping": "pong"
}
web1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3.13"
},
"changed": false,
"ping": "pong"
}
Первый аргумент ansible — шаблон узлов: имя узла, группы или их сочетание. --list-hosts показывает, на каких узлах выполнится команда, ничего не выполняя:
$ ansible web --list-hosts
hosts (2):
web1
web2
$ ansible "all:!db" --list-hosts
hosts (2):
web1
web2
$ ansible prod --limit web2 --list-hosts
hosts (1):
web2
| Шаблон | Узлы |
|---|---|
all или * | Все |
web | Группа web |
web:db | Объединение групп |
web:&prod | Пересечение: узлы web, которые входят и в prod |
all:!db | Все, кроме группы db (в оболочке — в кавычках, иначе ! обработает bash) |
web* | Имена по маске |
--limit (-l) дополнительно сужает выбор. Особенно он нужен с плейбуками: плейбук написан для группы, а проверить изменение хочется сначала на одном узле.
Разовая команда (ad-hoc) — один модуль на выбранных узлах, без плейбука. Модуль по умолчанию — command:
$ ansible all -m command -a "cat /etc/debian_version"
web2 | CHANGED | rc=0 >>
13.7
db1 | CHANGED | rc=0 >>
13.7
web1 | CHANGED | rc=0 >>
13.7
command запускает программу напрямую, без оболочки. Переменные окружения он подставляет, а конвейеры, перенаправления и * — нет: | и всё после него становятся обычными аргументами echo. Модуль shell выполняет строку через /bin/sh, и конвейер работает:
$ ansible db -m command -a "echo \$HOME | tr a-z A-Z"
db1 | CHANGED | rc=0 >>
/home/ansible | tr a-z A-Z
$ ansible db -m shell -a "echo \$HOME | tr a-z A-Z"
db1 | CHANGED | rc=0 >>
/HOME/ANSIBLE
Без оболочки безопаснее: если в команду попадёт значение переменной с ; или $(…), command не выполнит его как код. Поэтому shell используют, только когда действительно нужны возможности оболочки.
Поставим nginx на веб-узлы. Модуль apt без повышения прав работает от пользователя ansible — и apt ему отказывает:
$ ansible web1 -m apt -a "name=nginx state=present" | grep -E "\| FAILED|\"msg\""
[WARNING]: Updating cache and auto-installing missing dependency: python3-apt
web1 | FAILED! => {
"msg": "E: Could not open lock file /var/lib/apt/lists/lock - open (13: Permission denied)\nE: Unable to lock directory /var/lib/apt/lists/",
-b (--become) выполняет модуль через sudo от root. В плейбуках это ключ become: true — для всего плейбука или отдельной задачи. Права повышают только там, где они нужны: прочитать файл или проверить адрес можно и без root.
$ ansible web -b -m apt -a "name=nginx state=present update_cache=true" | st
web1 | CHANGED => {
web2 | CHANGED => {
$ ansible web -b -m apt -a "name=nginx state=present" | st
web2 | SUCCESS => {
web1 | SUCCESS => {
Первый запуск поставил nginx (CHANGED), второй ничего не менял (SUCCESS). Модуль systemd следит, чтобы служба работала и была в автозапуске. Debian запускает nginx сразу после установки, поэтому здесь ответ SUCCESS — менять нечего; на узле, где служба остановлена, модуль её запустит. uri делает HTTP-запрос с самого узла:
$ ansible web -b -m systemd -a "name=nginx state=started enabled=true" | st
web1 | SUCCESS => {
web2 | SUCCESS => {
$ ansible web -m uri -a "url=http://127.0.0.1/" | grep -E "\| |\"status\""
web2 | SUCCESS => {
"status": 200,
web1 | SUCCESS => {
"status": 200,
Модуль copy кладёт файл на узлы. Содержимое здесь задано прямо в команде, и в нём работают переменные: {{ inventory_hostname }} — имя узла, {{ site_title }} — переменная группы web из инвентаря:
$ ansible web -b -m copy -a "content='{{ site_title }}: {{ inventory_hostname }}\n' dest=/var/www/html/index.html" | st
web2 | CHANGED => {
web1 | CHANGED => {
$ ansible web -m uri -a "url=http://127.0.0.1/ return_content=true" | grep -E "\| |\"content\""
web2 | SUCCESS => {
"content": "Сервис цен: web2\n",
web1 | SUCCESS => {
"content": "Сервис цен: web1\n",
| Задача | Модуль |
|---|---|
| Пакеты | apt (package — для любого дистрибутива) |
| Службы | systemd (service) |
| Файлы и каталоги | copy, file, template (модуль 05) |
| Пользователи и группы | user, group, authorized_key |
| Проверить HTTP | uri |
| Документация модуля | ansible-doc apt, список — ansible-doc -l |
--check — режим проверки: модули сообщают, что изменили бы, но ничего не делают. CHANGED означает «пакета нет, поставил бы», и на узле его действительно по-прежнему нет:
$ ansible web -b -m apt -a "name=htop state=present" --check | st
web2 | CHANGED => {
web1 | CHANGED => {
$ ansible web -m shell -a "command -v htop || echo htop не установлен"
web1 | CHANGED | rc=0 >>
htop не установлен
web2 | CHANGED | rc=0 >>
htop не установлен
Перед изменениями на боевых узлах --check показывает, на каких узлах и что поменяется. Не все модули умеют этот режим: command и shell в нём просто пропускаются, потому что Ansible не знает, что сделает команда.
Модуль setup собирает о узле факты: дистрибутив и версию, адреса, память, диски, процессоры — сотни значений. Плейбуки собирают их автоматически в начале работы и используют в условиях и шаблонах (модуль 04). filter оставляет только нужные:
$ ansible db1 -m setup -a "filter=ansible_distribution*"
db1 | SUCCESS => {
"ansible_facts": {
"ansible_distribution": "Debian",
"ansible_distribution_file_parsed": true,
"ansible_distribution_file_path": "/etc/os-release",
"ansible_distribution_file_variety": "Debian",
"ansible_distribution_major_version": "13",
"ansible_distribution_minor_version": "7",
"ansible_distribution_release": "trixie",
"ansible_distribution_version": "13.7",
"discovered_interpreter_python": "/usr/bin/python3.13"
},
"changed": false
}
$ ansible all -m setup -a "filter=ansible_default_ipv4" | grep -E "\| |\"address\""
db1 | SUCCESS => {
"address": "172.30.50.21",
web1 | SUCCESS => {
"address": "172.30.50.11",
web2 | SUCCESS => {
"address": "172.30.50.12",
Сбор фактов занимает время на каждом узле. Если плейбуку они не нужны, его отключают ключом gather_facts: false.
ansible-inventory --graph и --hostansible.cfg проекта лежит рядом с плейбуками; действует первый найденный файл, а не их объединение--limit выбирают, где выполнять; --list-hosts показывает это заранееcommand — без оболочки и безопаснее; shell — только когда нужны конвейеры и перенаправленияbecome и только там, где нужно; --check показывает изменения, не внося ихsetup; плейбуки собирают их сами