Spec-Zone.ru › Ansible

Циклы

Ansible предлагает ключевые слова loop, with_<lookup> и until для многократного выполнения задачи. Примеры часто используемых циклов: изменение владельца нескольких файлов и/или каталогов с помощью модуля file, создание нескольких пользователей с помощью модуля user и повторение шага опроса до получения определённого результата.

Примечание

  • В Ansible 2.5 мы добавили loop как более простой способ работы с циклами, но рекомендуем его для большинства случаев.
  • Мы не объявляли with_<lookup> устаревшим — этот синтаксис останется допустимым в обозримом будущем.
  • loop и with_<lookup> нельзя использовать одновременно. Хотя их можно вложить в until, это влияет на каждую итерацию цикла.
  • Сравнение циклов
  • Использование циклов

    • Итерация по простому списку
    • Итерация по списку хешей
    • Итерация по словарю
    • Регистрация переменных в цикле
    • Повторение задачи до выполнения условия
    • Цикл по инвентарю
  • Обеспечение списка на входе loop: использование query вместо lookup
  • Добавление управляющих параметров к циклам

    • Ограничение вывода цикла с помощью label
    • Пауза внутри цикла
    • Прерывание цикла
    • Отслеживание хода цикла с помощью index_var
    • Расширенные переменные цикла
    • Получение имени переменной loop_var
  • Вложенные циклы

    • Итерация по вложенным спискам
    • Объединение циклов с помощью include_tasks
    • Until и loop
  • Переход с with_X на loop

    • with_list
    • with_items
    • with_indexed_items
    • with_flattened
    • with_together
    • with_dict
    • with_sequence
    • with_subelements
    • with_nested/with_cartesian
    • with_random_choice

Сравнение циклов

  • Обычно 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 управления циклом. Этот параметр предоставляет следующую информацию.

Переменная

Описание

ansible_loop.allitems

Список всех элементов цикла

ansible_loop.index

Текущая итерация цикла (индексация с 1)

ansible_loop.index0

Текущая итерация цикла (индексация с 0)

ansible_loop.revindex

Количество итераций до конца цикла (индексация с 1)

ansible_loop.revindex0

Количество итераций до конца цикла (индексация с 0)

ansible_loop.first

True при первой итерации

ansible_loop.last

True при последней итерации

ansible_loop.length

Количество элементов в цикле

ansible_loop.previtem

Элемент предыдущей итерации цикла. Не определён во время первой итерации.

ansible_loop.nextitem

Элемент следующей итерации цикла. Не определён во время последней итерации.

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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API