Циклы
Ansible предлагает ключевые слова loop, with_<lookup> и until для многократного выполнения задачи. Примеры часто используемых циклов: изменение владельца нескольких файлов и/или каталогов с помощью модуля file, создание нескольких пользователей с помощью модуля user и повторение шага опроса до получения определённого результата.
Примечание
- В Ansible 2.5 мы добавили
loopкак более простой способ работы с циклами, но рекомендуем его для большинства случаев. - Мы не объявляли
with_<lookup>устаревшим — этот синтаксис останется допустимым в обозримом будущем. -
loopиwith_<lookup>нельзя использовать одновременно. Хотя их можно вложить вuntil, это влияет на каждую итерацию цикла.
Сравнение циклов
- Обычно
untilиспользуется для задач, которые могут завершиться с ошибкой, аloopиwith_<lookup>предназначены для повторения задач с небольшими вариациями. - Ключевые слова
loopиwith_<lookup>выполняют задачу один раз для каждого элемента входного списка, тогда какuntilповторяет задачу до выполнения условия. Для программистов первые два — это «циклы for», а последний — «цикл while/until». - Ключевые слова
with_<lookup>используют плагины поиска — дажеitemsявляется поиском. - Ключевое слово
loopэквивалентноwith_listи является лучшим выбором для простых циклов. - Ключевое слово
loopне принимает строку на входе; см. раздел Обеспечение списка на входе для loop: использование query вместо lookup. - Ключевое слово
untilпринимает «условие завершения» (выражение, возвращающееTrueилиFalse), которое «неявно обрабатывается как шаблон» ({{ }}не требуется) и обычно основано на переменной, которую выregisterдля задачи. -
loop_controlвлияет и наloop, и наwith_<lookup>, но не наuntil, для которого предусмотрены собственные связанные ключевые слова:retriesиdelay. - В общем случае любое использование
with_*, рассмотренное в разделе Переход с with_X на loop, можно заменить наloop. - Будьте осторожны при замене
with_itemsнаloop, посколькуwith_itemsнеявно выполняет одноуровневое уплощение. Возможно, для получения точно такого же результата потребуется использовать| flatten(1)сloop. Например, чтобы получить тот же результат, что и:
with_items: - 1 - [2,3] - 4
потребуется
loop: "{{ [1, [2, 3], 4] | flatten(1) }}"
- Не следует заменять на ключевое слово
loopоператорыwith_*, в которых требуется использоватьlookupвнутри цикла. Например, вместо следующего варианта:
loop: "{{ lookup('fileglob', '*.txt', wantlist=True) }}"
лучше оставить
with_fileglob: '*.txt'
Использование циклов
Итерация по простому списку
Повторяющиеся задачи можно записать в виде обычных циклов по простому списку строк. Список можно определить непосредственно в задаче.
- name: Add several users
ansible.builtin.user:
name: "{{ item }}"
state: present
groups: "wheel"
loop:
- testuser1
- testuser2
Список можно определить в файле переменных или в разделе «vars» вашего сценария, а затем указать в задаче имя списка.
loop: "{{ somelist }}"
Оба этих примера эквивалентны следующему:
- name: Add user testuser1
ansible.builtin.user:
name: "testuser1"
state: present
groups: "wheel"
- name: Add user testuser2
ansible.builtin.user:
name: "testuser2"
state: present
groups: "wheel"
Для некоторых плагинов список можно напрямую передать параметру. Такая возможность есть у большинства модулей управления пакетами, например yum и apt. Если такая возможность есть, передавать список параметру предпочтительнее, чем выполнять цикл по задаче. Например:
- name: Optimal yum
ansible.builtin.yum:
name: "{{ list_of_packages }}"
state: present
- name: Non-optimal yum, slower and may cause issues with interdependencies
ansible.builtin.yum:
name: "{{ item }}"
state: present
loop: "{{ list_of_packages }}"
Проверьте документацию модуля, чтобы узнать, можно ли передать список параметру конкретного модуля.
Итерация по списку хешей
Если у вас есть список хешей, в цикле можно обращаться к вложенным ключам. Например:
- name: Add several users
ansible.builtin.user:
name: "{{ item.name }}"
state: present
groups: "{{ item.groups }}"
loop:
- { name: 'testuser1', groups: 'wheel' }
- { name: 'testuser2', groups: 'root' }
При сочетании условных операторов с циклом оператор when: обрабатывается отдельно для каждого элемента. Примеры см. в разделе Основные условные операторы с when.
Итерация по словарю
Для перебора словаря используйте dict2items:
- name: Using dict2items
ansible.builtin.debug:
msg: "{{ item.key }}: {{ item.value.ip_address }} {{ item.value.role }}"
loop: "{{ server_configs | dict2items }}"
vars:
server_configs:
web_01:
ip_address: "10.1.1.50"
role: "frontend"
db_01:
ip_address: "10.1.1.100"
role: "backend_db"
Здесь мы перебираем server_configs и выводим ключ и выбранные вложенные поля.
Если значения в словаре сами являются словарями (например, каждой группе соответствует словарь, содержащий gid), помните, что после применения dict2items у каждого элемента цикла есть два атрибута: item.key и item.value. Для доступа к вложенным полям используйте item.value.<field>.
Регистрация переменных в цикле
Результат цикла можно зарегистрировать в переменной. Например:
- name: Register loop output as a variable
ansible.builtin.shell: "echo {{ item }}"
loop:
- "one"
- "two"
register: echo
При использовании register в цикле структура данных в переменной будет содержать атрибут results — список всех ответов модуля. Это отличается от структуры данных, возвращаемой при использовании register без цикла. Атрибут changed/failed/skipped рядом с results отражает общее состояние. changed/failed будет равно true, если хотя бы одна итерация привела к изменению или ошибке, а skipped будет равно true, только если были пропущены все итерации.
{
"changed": true,
"msg": "All items completed",
"results": [
{
"changed": true,
"cmd": "echo \"one\" ",
"delta": "0:00:00.003110",
"end": "2013-12-19 12:00:05.187153",
"invocation": {
"module_args": "echo \"one\"",
"module_name": "shell"
},
"item": "one",
"rc": 0,
"start": "2013-12-19 12:00:05.184043",
"stderr": "",
"stdout": "one"
},
{
"changed": true,
"cmd": "echo \"two\" ",
"delta": "0:00:00.002920",
"end": "2013-12-19 12:00:05.245502",
"invocation": {
"module_args": "echo \"two\"",
"module_name": "shell"
},
"item": "two",
"rc": 0,
"start": "2013-12-19 12:00:05.242582",
"stderr": "",
"stdout": "two"
}
]
}
Последующие циклы по зарегистрированной переменной для проверки результатов могут выглядеть так:
- name: Fail if return code is not 0
ansible.builtin.fail:
msg: "The command ({{ item.cmd }}) did not have a 0 return code"
when: item.rc != 0
loop: "{{ echo.results }}"
Во время итерации результат текущего элемента будет помещён в переменную.
- name: Place the result of the current item in the variable
ansible.builtin.shell: echo "{{ item }}"
loop:
- one
- two
register: echo
changed_when: echo.stdout != "one"
Повторение задачи до выполнения условия
Добавлено в версии 1.4.
С помощью ключевого слова until можно повторять задачу до выполнения определённого условия. Пример:
- name: Retry a task until a certain condition is met
ansible.builtin.shell: /usr/bin/foo
register: result
until: result.stdout.find("all systems go") != -1
retries: 5
delay: 10
Эта задача выполняется до 5 раз с задержкой 10 секунд между попытками. Если в stdout любой попытки содержится “all systems go”, задача завершается успешно. Значение “retries” по умолчанию — 3, а “delay” — 5.
Чтобы просмотреть результаты отдельных повторных попыток, запустите сценарий с -vv.
Если выполнить задачу с until и зарегистрировать результат в переменной, в зарегистрированной переменной будет ключ “attempts”, содержащий количество повторных попыток задачи.
Если until не указано, задача будет повторяться до успешного выполнения, но не более retries раз (добавлено в версии 2.16).
Ключевое слово until можно сочетать с loop или with_<lookup>. Результат задачи для каждого элемента цикла регистрируется в переменной и может использоваться в условии until. Пример:
- name: Retry combined with a loop
uri:
url: "https://{{ item }}.ansible.com"
method: GET
register: uri_output
with_items:
- "galaxy"
- "docs"
- "forum"
- "www"
retries: 2
delay: 1
until: "uri_output.status == 200"
Примечание
При использовании ключевого слова timeout в цикле оно применяется к каждой попытке выполнения задачи. Подробнее см. в разделе TASK_TIMEOUT.
Цикл по инвентарю
Обычно сам сценарий представляет собой цикл по вашему инвентарю, но иногда задаче нужно выполнить одно и то же для другого набора хостов. Чтобы пройти циклом по всему инвентарю или его части, можно использовать обычный loop с переменными ansible_play_batch или groups.
- name: Show all the hosts in the inventory
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ groups['all'] }}"
- name: Show all the hosts in the current play
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ ansible_play_batch }}"
Также для этого можно использовать специальный плагин поиска inventory_hostnames:
- name: Show all the hosts in the inventory
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ query('inventory_hostnames', 'all') }}"
- name: Show all the hosts matching the pattern, ie all but the group www
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ query('inventory_hostnames', 'all:!www') }}"
Дополнительную информацию о шаблонах см. в разделе Шаблоны: выбор хостов и групп.
Обеспечение списка на входе loop: использование query вместо lookup
Ключевое слово loop требует на входе список, но ключевое слово lookup по умолчанию возвращает строку со значениями, разделёнными запятыми. В Ansible 2.5 появилась новая функция Jinja2 с именем query, которая всегда возвращает список. Благодаря ей плагины поиска имеют более простой интерфейс и выдают более предсказуемый результат при использовании ключевого слова loop.
Чтобы заставить lookup возвращать список для loop, используйте wantlist=True или вместо этого используйте query.
Следующие два примера выполняют одно и то же.
loop: "{{ query('inventory_hostnames', 'all') }}"
loop: "{{ lookup('inventory_hostnames', 'all', wantlist=True) }}"
Добавление управляющих параметров к циклам
Добавлено в версии 2.1.
Ключевое слово loop_control позволяет гибко управлять циклами.
Ограничение вывода цикла с помощью label
Добавлено в версии 2.2.
При переборе сложных структур данных вывод задачи в консоль может быть очень большим. Чтобы ограничить отображаемый вывод, используйте директиву label с loop_control.
- name: Create servers
digital_ocean:
name: "{{ item.name }}"
state: present
loop:
- name: server1
disks: 3gb
ram: 15Gb
network:
nic01: 100Gb
nic02: 10Gb
# ...
loop_control:
label: "{{ item.name }}"
В выводе этой задачи будет показано только поле name для каждого item, а не всё содержимое многострочной переменной {{ item }}.
Примечание
Эта функция предназначена для повышения читаемости вывода в консоли, а не для защиты конфиденциальных данных. Если loop содержит конфиденциальные данные, задайте для задачи no_log: true, чтобы предотвратить их раскрытие.
Пауза внутри цикла
Добавлено в версии 2.2.
Чтобы задать время (в секундах) между выполнением каждого элемента цикла задачи, используйте директиву pause с loop_control.
# main.yml
- name: Create servers, pause 3s before creating next
community.digitalocean.digital_ocean:
name: "{{ item }}"
state: present
loop:
- server1
- server2
loop_control:
pause: 3
Прерывание цикла
Добавлено в версии 2.18.
Используйте директиву break_when с loop_control, чтобы выйти из цикла после любого элемента на основании выражений Jinja2.
# main.yml
- name: Use set_fact in a loop until a condition is met
vars:
special_characters: "!@#$%^&*(),.?:{}|<>"
character_set: "digits,ascii_letters,{{ special_characters }}"
password_policy: '^(?=.*\d)(?=.*[A-Z])(?=.*[{{ special_characters | regex_escape }}]).{12,}$'
block:
- name: Generate a password until it contains a digit, uppercase letter, and special character (10 attempts)
set_fact:
password: "{{ lookup('password', '/dev/null', chars=character_set, length=12) }}"
loop: "{{ range(0, 10) }}"
loop_control:
break_when:
- password is match(password_policy)
- fail:
msg: "Maximum attempts to generate a valid password exceeded"
when: password is not match(password_policy)
Отслеживание хода цикла с помощью index_var
Добавлено в версии 2.5.
Чтобы отслеживать текущую позицию в цикле, используйте директиву index_var с loop_control. Эта директива задаёт имя переменной, в которой будет храниться текущий индекс цикла.
- name: Count our fruit
ansible.builtin.debug:
msg: "{{ item }} with index {{ my_idx }}"
loop:
- apple
- banana
- pear
loop_control:
index_var: my_idx
Примечание
Индексация index_var начинается с 0.
Расширенные переменные цикла
Добавлено в версии 2.8.
Начиная с Ansible 2.8, можно получать расширенную информацию о цикле с помощью параметра extended управления циклом. Этот параметр предоставляет следующую информацию.
Переменная | Описание |
| Список всех элементов цикла |
| Текущая итерация цикла (индексация с 1) |
| Текущая итерация цикла (индексация с 0) |
| Количество итераций до конца цикла (индексация с 1) |
| Количество итераций до конца цикла (индексация с 0) |
|
|
|
|
| Количество элементов в цикле |
| Элемент предыдущей итерации цикла. Не определён во время первой итерации. |
| Элемент следующей итерации цикла. Не определён во время последней итерации. |
loop_control: extended: true
Примечание
При использовании loop_control.extended на управляющем узле будет расходоваться больше памяти. Это связано с тем, что ansible_loop.allitems содержит ссылку на все данные цикла для каждой итерации. При сериализации результатов для отображения плагинами обратного вызова в основном процессе Ansible эти ссылки могут быть разыменованы, что приведёт к увеличению использования памяти.
Добавлено в версии 2.14.
Чтобы отключить элемент ansible_loop.allitems и снизить потребление памяти, задайте loop_control.extended_allitems: false.
loop_control: extended: true extended_allitems: false
Получение имени переменной loop_var
Добавлено в версии 2.8.
Начиная с Ansible 2.8, имя значения, переданного в loop_control.loop_var, можно получить с помощью переменной ansible_loop_var.
Авторы ролей, создавая роли, поддерживающие циклы, могут не задавать обязательное значение loop_var, а получить его следующим образом:
"{{ lookup('vars', ansible_loop_var) }}"
Вложенные циклы
Хотя в этих примерах мы используем loop, то же самое относится и к with_<lookup>.
Итерация по вложенным спискам
Самый простой способ «вложить» циклы — избежать их вложения и просто отформатировать данные, чтобы получить тот же результат. Для итерации по сложным спискам можно использовать выражения Jinja2. Например, цикл может объединять вложенные списки, имитируя вложенный цикл.
- name: Give users access to multiple databases
community.mysql.mysql_user:
name: "{{ item[0] }}"
priv: "{{ item[1] }}.*:ALL"
append_privs: true
password: "foo"
loop: "{{ ['alice', 'bob'] | product(['clientdb', 'employeedb', 'providerdb']) | list }}"
Объединение циклов с помощью include_tasks
Добавлено в версии 2.1.
Две задачи с циклами можно вложить друг в друга с помощью include_tasks. Однако по умолчанию Ansible задаёт для каждого цикла переменную цикла item. Это означает, что внутренний вложенный цикл перезапишет значение item внешнего цикла. Чтобы этого избежать, можно указать имя переменной для каждого цикла с помощью loop_var и loop_control.
# main.yml
- include_tasks: inner.yml
loop:
- 1
- 2
- 3
loop_control:
loop_var: outer_item
# inner.yml
- name: Print outer and inner items
ansible.builtin.debug:
msg: "outer item={{ outer_item }} inner item={{ item }}"
loop:
- a
- b
- c
Примечание
Если Ansible обнаружит, что в текущем цикле используется уже определённая переменная, он вызовет ошибку и завершит задачу с ошибкой.
Until и loop
Условие until применяется отдельно к каждому item из loop:
- debug: msg={{item}}
loop:
- 1
- 2
- 3
retries: 2
until: item > 2
В результате Ansible дважды повторит попытку для первых двух элементов, затем завершит попытки для второго элемента с ошибкой на третьей попытке, после чего успешно выполнит первую попытку для третьего элемента. В итоге задача в целом завершится с ошибкой.
[started TASK: debug on localhost]
FAILED - RETRYING: [localhost]: debug (2 retries left).Result was: {
"attempts": 1,
"changed": false,
"msg": 1,
"retries": 3
}
FAILED - RETRYING: [localhost]: debug (1 retries left).Result was: {
"attempts": 2,
"changed": false,
"msg": 1,
"retries": 3
}
failed: [localhost] (item=1) => {
"msg": 1
}
FAILED - RETRYING: [localhost]: debug (2 retries left).Result was: {
"attempts": 1,
"changed": false,
"msg": 2,
"retries": 3
}
FAILED - RETRYING: [localhost]: debug (1 retries left).Result was: {
"attempts": 2,
"changed": false,
"msg": 2,
"retries": 3
}
failed: [localhost] (item=2) => {
"msg": 2
}
ok: [localhost] => (item=3) => {
"msg": 3
}
fatal: [localhost]: FAILED! => {"msg": "One or more items failed"}
Переход с with_X на loop
В большинстве случаев для циклов лучше использовать ключевое слово loop, а не циклы в стиле with_X. Синтаксис loop обычно лучше выразить с помощью фильтров, а не более сложного использования query или lookup.
В этих примерах показано, как преобразовать многие распространённые циклы в стиле with_ в loop и фильтры.
with_list
with_list напрямую заменяется на loop.
- name: with_list
ansible.builtin.debug:
msg: "{{ item }}"
with_list:
- one
- two
- name: with_list -> loop
ansible.builtin.debug:
msg: "{{ item }}"
loop:
- one
- two
with_items
with_items заменяется на loop и фильтр flatten.
- name: with_items
ansible.builtin.debug:
msg: "{{ item }}"
with_items: "{{ items }}"
- name: with_items -> loop
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ items|flatten(levels=1) }}"
with_indexed_items
with_indexed_items заменяется на loop, фильтр flatten и loop_control.index_var.
- name: with_indexed_items
ansible.builtin.debug:
msg: "{{ item.0 }} - {{ item.1 }}"
with_indexed_items: "{{ items }}"
- name: with_indexed_items -> loop
ansible.builtin.debug:
msg: "{{ index }} - {{ item }}"
loop: "{{ items|flatten(levels=1) }}"
loop_control:
index_var: index
with_flattened
with_flattened заменяется на loop и фильтр flatten.
- name: with_flattened
ansible.builtin.debug:
msg: "{{ item }}"
with_flattened: "{{ items }}"
- name: with_flattened -> loop
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ items|flatten }}"
with_together
with_together заменяется на loop и фильтр zip.
- name: with_together
ansible.builtin.debug:
msg: "{{ item.0 }} - {{ item.1 }}"
with_together:
- "{{ list_one }}"
- "{{ list_two }}"
- name: with_together -> loop
ansible.builtin.debug:
msg: "{{ item.0 }} - {{ item.1 }}"
loop: "{{ list_one|zip(list_two)|list }}"
Ещё один пример со сложными данными
- name: with_together -> loop
ansible.builtin.debug:
msg: "{{ item.0 }} - {{ item.1 }} - {{ item.2 }}"
loop: "{{ data[0]|zip(*data[1:])|list }}"
vars:
data:
- ['a', 'b', 'c']
- ['d', 'e', 'f']
- ['g', 'h', 'i']
with_dict
with_dict можно заменить на loop и один из фильтров: dictsort или dict2items.
- name: with_dict
ansible.builtin.debug:
msg: "{{ item.key }} - {{ item.value }}"
with_dict: "{{ dictionary }}"
- name: with_dict -> loop (option 1)
ansible.builtin.debug:
msg: "{{ item.key }} - {{ item.value }}"
loop: "{{ dictionary|dict2items }}"
- name: with_dict -> loop (option 2)
ansible.builtin.debug:
msg: "{{ item.0 }} - {{ item.1 }}"
loop: "{{ dictionary|dictsort }}"
with_sequence
with_sequence заменяется на loop и функцию range, а также, возможно, фильтр format.
- name: with_sequence
ansible.builtin.debug:
msg: "{{ item }}"
with_sequence: start=0 end=4 stride=2 format=testuser%02x
- name: with_sequence -> loop
ansible.builtin.debug:
msg: "{{ 'testuser%02x' | format(item) }}"
loop: "{{ range(0, 4 + 1, 2)|list }}"
В цикле конечная точка диапазона не включается.
with_subelements
with_subelements заменяется на loop и фильтр subelements.
- name: with_subelements
ansible.builtin.debug:
msg: "{{ item.0.name }} - {{ item.1 }}"
with_subelements:
- "{{ users }}"
- mysql.hosts
- name: with_subelements -> loop
ansible.builtin.debug:
msg: "{{ item.0.name }} - {{ item.1 }}"
loop: "{{ users|subelements('mysql.hosts') }}"
with_nested/with_cartesian
with_nested и with_cartesian заменяются на loop и фильтр product.
- name: with_nested
ansible.builtin.debug:
msg: "{{ item.0 }} - {{ item.1 }}"
with_nested:
- "{{ list_one }}"
- "{{ list_two }}"
- name: with_nested -> loop
ansible.builtin.debug:
msg: "{{ item.0 }} - {{ item.1 }}"
loop: "{{ list_one|product(list_two)|list }}"
with_random_choice
with_random_choice заменяется простым использованием фильтра random без необходимости в loop.
- name: with_random_choice
ansible.builtin.debug:
msg: "{{ item }}"
with_random_choice: "{{ my_list }}"
- name: with_random_choice -> loop (No loop is needed here)
ansible.builtin.debug:
msg: "{{ my_list|random }}"
tags: random
См. также
- Сценарии Ansible
-
Введение в сценарии
- Роли
-
Организация сценариев с помощью ролей
- Общие советы
-
Советы и рекомендации по работе со сценариями
- Условные операторы
-
Условные операторы в сценариях
- Использование переменных
-
Всё о переменных
- Общение
-
Есть вопросы? Нужна помощь? Хотите поделиться идеями? Посетите руководство по общению Ansible
© 2012–2018 Michael DeHaan
© 2018–2025 Red Hat, Inc.
Licensed under the GNU General Public License version 3.
https://docs.ansible.com/projects/ansible/latest/playbook_guide/playbooks_loops.html