循环
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_X 迁移到 loop 中涵盖的任何
with_*
用法都可以更新为使用loop
。将
with_items
更改为loop
时要小心,因为with_items
执行隐式单层扁平化。您可能需要在loop
中使用| flatten(1)
来匹配确切的结果。例如,要获得与以下相同的输出
with_items:
- 1
- [2,3]
- 4
您将需要
loop: "{{ [1, [2, 3], 4] | flatten(1) }}"
任何需要在循环中使用
lookup
的with_*
语句都不应转换为使用loop
关键字。例如,不要执行以下操作
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
您可以在变量文件中或 playbook 的“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 }}"
loop: "{{ tag_data | dict2items }}"
vars:
tag_data:
Environment: dev
Application: payment
在这里,我们正在遍历 tag_data 并打印其中的键和值。
使用循环注册变量
您可以将循环的输出注册为变量。例如
- 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 秒。如果任何尝试的结果在其标准输出中包含“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 引入了一个名为query的新 Jinja2 函数,该函数始终返回一个列表,在使用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 }}"
此任务的输出将仅显示每个item
的name
字段,而不是多行{{ 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
访问循环变量的名称
版本 2.8 中的新增功能。
从 Ansible 2.8 开始,您可以使用ansible_loop_var
变量获取提供给loop_control.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_control
中的loop_var
为每个循环指定变量名称。
# 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
条件将应用于loop
的每个item
。
- 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