В этом уроке разберём, как устроен loop (современный способ), чем он отличается от старого with_items, как перебирать списки словарей, как ловить результаты через register и управлять выводом через loop_control. И обязательно поговорим про идемпотентность и грабли, на которых режутся на экзамене.
ansible loop: базовый перебор списка
Директива loop крепится к таске на том же уровне, что и имя модуля. Внутри таски доступна магическая переменная item - это текущий элемент списка на данной итерации. Простейший ansible loop пример выглядит так:
Код: Выделить всё
- name: Создать несколько каталогов
ansible.builtin.file:
path: "/opt/{{ item }}"
state: directory
mode: "0755"
loop:
- app
- logs
- backup
Код: Выделить всё
- name: Создать пользователей сервиса
ansible.builtin.user:
name: "{{ item }}"
state: present
shell: /sbin/nologin
loop: "{{ service_users }}"
loop против with_items. Раньше тот же перебор писали через with_items, и ты до сих пор встретишь это в чужих ролях и в старых ответах на форумах. Запомни: ansible with_items и loop в простом случае со списком строк делают одно и то же. Разница в том, что with_* - семейство устаревших конструкций (with_dict, with_subelements, with_nested и так далее), а loop - единый современный механизм, который вместе с фильтрами Jinja покрывает все их сценарии. Новый код пиши на loop. Отдельный нюанс: with_items автоматически "разворачивал" вложенные списки на один уровень (flatten), а loop этого не делает - если нужно, добавь фильтр flatten явно: loop: "{{ mylist | flatten(levels=1) }}".

Списки словарей и доступ к item.key
Строки - это разминка. На практике в одной итерации часто нужно несколько полей сразу: имя пользователя, его группа, комментарий. Для этого перебирают список словарей, а к полям обращаются через точку - item.name, item.group:
Код: Выделить всё
- name: Завести пользователей с группами
ansible.builtin.user:
name: "{{ item.name }}"
group: "{{ item.group }}"
comment: "{{ item.comment }}"
state: present
loop:
- { name: deploy, group: wheel, comment: "CI deploy" }
- { name: monitor, group: users, comment: "Zabbix agent" }
Код: Выделить всё
- name: Открыть нужные порты
ansible.posix.firewalld:
port: "{{ item.port }}/{{ item.proto }}"
permanent: true
immediate: true
state: enabled
loop:
- { port: 80, proto: tcp }
- { port: 443, proto: tcp }
- { port: 53, proto: udp }
Код: Выделить всё
- name: Разложить конфиги из словаря
ansible.builtin.copy:
content: "{{ item.value }}"
dest: "/etc/myapp/{{ item.key }}.conf"
mode: "0644"
loop: "{{ app_settings | dict2items }}"
# app_settings:
# web: "listen 8080"
# db: "max_conn 200"
Когда нужно поймать результат каждой итерации, навешиваем register. В отличие от обычной таски, ты получишь не один результат, а ключ results - список со словарём по каждой итерации. Дальше по этому списку можно пройти ещё одним циклом:
Код: Выделить всё
- name: Проверить наличие файлов
ansible.builtin.stat:
path: "/etc/{{ item }}"
loop:
- hosts
- resolv.conf
- fstab
register: stat_out
- name: Показать, чего не хватает
ansible.builtin.debug:
msg: "Нет файла {{ item.item }}"
loop: "{{ stat_out.results }}"
when: not item.stat.exists
loop_control - наводим порядок. У длинных словарей вывод цикла раздувается: Ansible печатает весь item целиком, и лог становится нечитаемым (а если в item лежат пароли - это ещё и утечка в консоль). Лечится через loop_control:
Код: Выделить всё
- name: Создать пользователей тихо
ansible.builtin.user:
name: "{{ item.name }}"
password: "{{ item.pw | password_hash('sha512') }}"
state: present
loop: "{{ users }}"
loop_control:
label: "{{ item.name }}" # в логе будет только имя
index_var: idx # номер итерации с нуля
pause: 2 # пауза 2 сек между итерациями
Вложенные циклы. Декартово произведение двух списков - например, выдать набор прав каждому пользователю - делается фильтром product. Старый аналог - with_nested:
Код: Выделить всё
- name: Дать права на каталоги
ansible.builtin.file:
path: "/srv/{{ item.0 }}/{{ item.1 }}"
state: directory
loop: "{{ ['team_a', 'team_b'] | product(['in', 'out']) | list }}"
# item.0 - команда, item.1 - подкаталог
Код: Выделить всё
- name: Разложить SSH-ключи
ansible.posix.authorized_key:
user: "{{ item.0.name }}"
key: "{{ item.1 }}"
loop: "{{ users | subelements('keys') }}"
# item.0 - сам пользователь, item.1 - один ключ из его списка keys
Грабля номер один - цикл вокруг модуля, который сам умеет списки. Классика и любимый ловильный вопрос: установка пакетов через loop.
Код: Выделить всё
# ПЛОХО: dnf вызывается N раз, медленно, шумно
- ansible.builtin.dnf:
name: "{{ item }}"
state: present
loop: [git, vim, htop]
# ХОРОШО: одна транзакция dnf, один проход
- ansible.builtin.dnf:
name:
- git
- vim
- htop
state: present
Идемпотентность в циклах. Цикл сам по себе её не ломает и не чинит - всё держится на модуле внутри. Если на каждой итерации вызывается идемпотентный модуль (file, copy, user, firewalld), повторный прогон не наделает изменений и отрапортует ok. А вот если внутри loop стоит command или shell, который что-то дописывает в файл, - получишь N изменений при каждом запуске. Тогда либо добавляй creates/removes, либо защищайся через when, либо ищи нормальный модуль.
Ещё две частые ошибки. Первая - забыть, что при register с циклом результат лежит в .results, а не в корне переменной; обращение напрямую даёт undefined. Вторая - перепутать уровень отступа: loop должен быть на одном уровне с модулем (ключом таски), а не внутри параметров модуля, иначе Ansible его просто не увидит и ругнётся на синтаксис.
Связь с экзаменом RHCE (EX294)
Циклы на EX294 нужны постоянно: создать пачку пользователей из переменной, открыть несколько портов, разложить файлы. Пиши на loop - это текущий стандарт, его принимают без вопросов. Где режутся: оборачивают dnf в цикл (формально работает, но это антипаттерн и трата времени на экзамене с таймером), путаются в item.key против item.value при разборе словарей и забывают про .results после register. Потренируй эти три места до автоматизма - и блок с циклами на экзамене не отнимет лишних минут.
Мини-лаба: повтори руками
- Напиши плейбук, который через loop создаёт три каталога в /opt и проверь идемпотентность: второй прогон должен дать changed=0.
- Заведи список из трёх словарей (имя, группа, комментарий) и создай пользователей одной таской с ansible.builtin.user.
- Возьми словарь port: proto и открой порты в firewalld, перебрав его через dict2items.
- Добавь loop_control с label, чтобы в выводе светилось только имя пользователя, а не весь словарь. Сравни логи до и после.
- Сделай stat по списку файлов с register, затем вторым циклом по .results выведи через debug только отсутствующие.
- Намеренно оберни dnf в loop, замерь время, потом переделай на список в name - почувствуй разницу.
- Чем loop отличается от with_items и почему в новом коде предпочитают loop?
- Как из словаря ключ-значение получить список для перебора и как обратиться к ключу и значению?
- Где лежат результаты итераций после register с циклом и как по ним пройти повторно?
- Почему ставить пакеты через loop с dnf - плохая идея, и как сделать правильно?
loop - единый современный механизм перебора: списки строк, списки словарей (item.key), словари через dict2items, вложенность через product и subelements. register на цикле даёт .results, а loop_control с label/index_var/pause наводит порядок в выводе. Главное помни про идемпотентность и не оборачивай в цикл модули, которые сами едят списки. Старые with_* знать полезно для чтения чужого кода, но писать пора на loop.