7.Ansible.实施任务控制
实验环境
准备 Ansible 工作环境:
# 创建工作目录web并进入
[bq@controller ~]$ mkdir web && cd web
# 创建Ansible配置文件ansible.cfg,定义默认参数
[bq@controller web]$ cat > ansible.cfg <<'EOF'
[defaults]
remote_user = bq # 默认远程登录用户为bq
inventory = ./inventory # 指定inventory文件路径为当前目录的inventory
[privilege_escalation]
become = True # 允许提权(切换到root)
become_user = root # 提权目标用户为root
become_method = sudo # 提权方式为sudo
become_ask_pass = False # 提权时不询问密码(需提前配置sudo免密)
EOF
# 创建inventory文件,定义受管节点列表
[bq@controller web]$ cat > inventory <<'EOF'
controller # 控制节点自身(可作为受管节点)
node1 # 受管节点1
node2 # 受管节点2
node3 # 受管节点3
node4 # 受管节点4
EOF
编写循环任务
循环的核心作用是减少重复代码:当需要对多个对象执行相同操作时(比如创建多个用户、安装多个软件),无需写多个重复任务,只需用循环迭代一个列表即可。
Ansible 通过loop
关键字实现循环,迭代过程中用item
变量表示当前元素。
简单循环(列表迭代)
场景:创建用户 jane 和 joe,均加入 wheel 组。
如果不使用循环,需要写两个任务:
---
- name: 不使用循环创建用户
hosts: node1
gather_facts: no # 不收集受管节点信息(加快执行速度)
tasks:
- name: 创建用户jane
user:
name: "jane" # 用户名
groups: "wheel" # 所属组
state: present # 确保用户存在
- name: 创建用户joe
user:
name: "joe"
groups: "wheel"
state: present
用 loop 改写后(更简洁):
- name: 使用loop循环创建用户
hosts: node1
gather_facts: no
tasks:
- name: 批量创建用户
user:
name: "{{ item }}" # item代表循环中的当前用户(jane或joe)
groups: "wheel"
state: present
loop: # 循环的用户列表
- jane
- joe
也可以将列表定义为变量,再通过loop
引用(更易维护):
- name: 用变量定义循环列表
hosts: node1
gather_facts: no
tasks:
- name: 批量创建用户
user:
name: "{{ item }}"
groups: "wheel"
state: present
loop: "{{ users }}" # 引用变量users
vars: # 定义变量users为用户列表
users:
- jane
- joe
循环散列 / 字典列表
当需要迭代的元素包含多个属性时(比如用户不仅有名称,还有所属组、家目录等),可以用字典列表(每个元素是一个键值对集合),通过item.键名
获取属性。
场景:创建用户 jane(属于 wheel 组)和 joe(属于 root 组)。
yaml
- name: 循环字典列表创建用户
hosts: node1
gather_facts: no
vars: # 定义用户信息列表(每个元素是一个字典)
users:
- name: jane # 用户名
groups: wheel # 所属组
- name: joe
groups: root
tasks:
- name: 批量创建带属性的用户
user:
name: "{{ item.name }}" # 取当前字典的name属性
groups: "{{ item.groups }}" # 取当前字典的groups属性
state: present
loop: "{{ users }}" # 循环用户信息列表
较早样式的循环关键字(了解即可)
Ansible 2.5 之前常用with_
开头的关键字(如with_items
)实现循环,现在推荐用loop
。以下是常见旧语法对比:
旧语法 | 说明 | 对应新语法(loop) | ||
---|---|---|---|---|
with_items | 迭代列表,会自动展开嵌套列表 | loop (但loop 不会展开嵌套列表,需注意区别) | ||
with_list | 迭代列表,不展开嵌套列表 | loop (行为一致) | ||
with_together | 按索引合并多个列表(如 [1,2] 和 [a,b] 合并为 (1,a),(2,b)) | `loop: "{{ list1 | zip(list2) | list }}"` |
with_indexed_items | 迭代列表时同时返回索引(如 a 的索引 0,b 的索引 1) | `loop: "{{ items | enumerate }}"` | |
with_sequence | 生成数字序列(如 1-5) | loop: "{{ range(1,6) }}" |
示例:with_items 与 loop 的区别
with_items
会展开嵌套列表,而loop
不会:
# with_items会将嵌套列表[ [jane, joe] ]展开为[jane, joe],创建两个用户
- name: with_items示例(旧语法)
hosts: node1
tasks:
- name: 创建用户
user: name="{{ item }}" state=present
with_items:
- [jane, joe] # 嵌套列表会被展开
# loop不会展开嵌套列表,会把[jane, joe]当作一个元素,导致错误(用户名不能是列表)
- name: loop错误示例
hosts: node1
tasks:
- name: 创建用户(会失败)
user: name="{{ item }}" state=present
loop:
- [jane, joe] # 嵌套列表不会被展开,item是整个列表
Do-Until 循环(重试直到条件满足)
用于重复执行任务直到满足条件(如等待服务启动、节点上线)。
场景:每隔 1 秒检测 node2 是否可 ping 通,最多重试 20 次。
- name: 检测node2是否可达
hosts: node1 # 在node1上执行ping命令
gather_facts: no
tasks:
- shell: ping -c1 -w 2 node2 # ping node2(1个包,超时2秒)
register: result # 保存命令结果到变量result
until: result.rc == 0 # 条件:返回码为0(成功)时停止重试
retries: 20 # 最大重试次数
delay: 1 # 重试间隔(秒)
循环与注册变量(register)
循环任务的结果会被整合到注册变量的results
属性中,可通过二次循环获取每个迭代的结果。
场景:循环输出字符串,然后打印每个输出的内容。
---
- name: 循环与注册变量示例
hosts: node1
gather_facts: no
tasks:
- name: 循环输出字符串
shell: "echo 这是我的元素:{{ item }}" # 输出当前元素
loop:
- one
- two
register: result # 注册结果(包含所有迭代的信息)
- name: 打印完整结果
debug:
var: result # 查看result的结构(包含results列表)
- name: 打印每个迭代的输出
debug:
msg: "上一步的输出:{{ item.stdout }}" # 取每个迭代的stdout
loop: "{{ result.results }}" # 循环result中的results列表
编写条件任务
条件任务用于根据特定条件决定是否执行任务(比如根据主机系统版本安装不同软件、根据磁盘空间决定是否部署服务)。
核心关键字是when
,后面跟判断条件(如变量值、命令结果、系统信息等)。
常见判断条件
条件类型 | 示例 | 说明 |
---|---|---|
等于(字符串) | ansible_machine == "x86_64" | 系统架构是否为 x86_64 |
等于(数字) | max_memory == 512 | 内存是否为 512MB |
比较大小 | min_memory > 256 | 内存是否大于 256MB |
变量是否定义 | min_memory is defined | 变量 min_memory 是否存在 |
变量是否为空 | min_memory is none | 变量 min_memory 已定义但值为空 |
布尔值判断 | run_task | 变量 run_task 为 true 时成立 |
取反 | not run_task | 变量 run_task 为 false 时成立 |
包含关系 | ansible_distribution in supported_os | 系统发行版是否在支持列表中 |
文件属性 | /etc/hosts is file | 路径是否为普通文件 |
实用示例
1. 布尔变量判断
当变量run_my_task
为 true 时执行任务:
---
- name: 布尔条件示例
hosts: node1
gather_facts: no
vars:
run_my_task: true # 定义布尔变量
tasks:
- name: 条件执行的任务
debug:
msg: "任务执行了!"
when: run_my_task # 条件:变量为true
2. 变量是否定义
判断卷组research
是否存在,存在则创建逻辑卷,否则提示错误:
---
- name: 检查卷组并创建逻辑卷
hosts: node1
tasks:
- name: 创建4000MB的逻辑卷
lvol:
vg: research # 卷组名称
lv: data # 逻辑卷名称
size: 4000 # 大小(MB)
when: ansible_lvm.vgs.research is defined # 条件:卷组research存在
- name: 提示卷组不存在
debug:
msg: "卷组research不存在"
when: ansible_lvm.vgs.research is not defined # 条件:卷组不存在
3. 命令结果判断
根据df
命令结果判断根目录可用空间,足够则安装软件:
---
- name: 根据磁盘空间安装软件
hosts: node1
gather_facts: no
tasks:
- name: 获取根目录可用空间(KB)
shell: df / | awk 'NR==2 {print $4}' # 取df结果第二行第四列(可用空间)
register: fs_size # 保存结果到变量
- name: 安装mariadb-server(空间足够时)
dnf:
name: mariadb-server
state: present
when: fs_size.stdout | int >= 300000 # 条件:可用空间≥300000KB
4. 循环与条件结合
对多个挂载点循环,仅处理根目录(/)且可用空间足够时安装软件:
---
- name: 循环+条件示例
hosts: node1
tasks:
- name: 根目录空间足够时安装mariadb
yum:
name: mariadb-server
state: latest
loop: "{{ ansible_mounts }}" # 循环所有挂载点(从facts获取)
when:
- item.mount == "/" # 仅处理根目录
- item.size_available > 300000000 # 可用空间>300MB(单位:字节)
Ansible Handlers(处理程序)
Handlers 是被其他任务 “通知” 后才执行的任务,通常用于 “配置变更后需要重启服务” 等场景(如修改 nginx 配置后重启 nginx)。
特点:
- 只有通知它的任务执行成功且状态为
changed
时,Handlers 才会运行; - 即使被多个任务通知,Handlers 也只会执行一次;
- 所有任务执行完毕后,Handlers 才会统一运行(除非用
meta
模块强制提前执行)。
基础用法
场景:安装 httpd 后,若配置有变更则重启服务。
---
- name: 部署web服务器
hosts: node1
tasks:
- name: 安装httpd
yum:
name: httpd
state: present
notify: # 若此任务状态为changed,通知Handlers中的"重启apache"
- 重启apache
- name: 安装httpd手册
yum:
name: httpd-manual
state: present
notify: # 再次通知同一个Handler
- 重启apache
- debug:
msg: 所有任务执行完毕(Handlers还未运行)
handlers: # 定义Handlers
- name: 重启apache # Handler名称(需与notify中的名称一致)
service:
name: httpd
state: restarted # 重启服务
enabled: yes # 设置开机自启
执行结果:
- 第一次执行:两个 yum 任务均为
changed
,Handlers 被通知,最终执行一次 “重启 apache”; - 第二次执行:yum 任务状态为
ok
(已安装),Handlers 不被通知,不执行。
强制立即执行 Handlers(meta 模块)
默认 Handlers 在所有任务执行完毕后运行,若需中途执行,可使用meta: flush_handlers
。
场景:安装数据库后,立即启动服务再创建用户。
---
- name: 部署数据库
hosts: node1
tasks:
- name: 安装mariadb
yum:
name:
- mariadb-server
- python3-PyMySQL
state: present
notify:
- 启动并启用数据库
- meta: flush_handlers # 强制立即执行已通知的Handlers
- name: 创建数据库用户bq
mysql_user:
name: bq
password: redhat
host: "%" # 允许远程登录
handlers:
- name: 启动并启用数据库
service:
name: mariadb
state: started
enabled: yes
调用多个 Handlers
可通过列表或分组(listen
)通知多个 Handlers。
方式 1:列表通知
直接在notify
中列出多个 Handler 名称:
---
- name: 部署服务
hosts: node1
tasks:
- name: 安装httpd相关包
yum:
name:
- httpd
- httpd-manual
state: present
notify:
- 启动apache
- 启用apache # 通知两个Handlers
handlers:
- name: 启动apache
service: name=httpd state=started
- name: 启用apache
service: name=httpd enabled=yes
方式 2:分组通知(listen)
用listen
给 Handlers 分组,notify
中指定组名即可触发所有分组的 Handler:
---
- name: 部署服务
hosts: node1
tasks:
- name: 安装httpd相关包
yum:
name:
- httpd
- httpd-manual
state: present
notify:
- 启动并启用apache # 通知组名
handlers:
- name: 启动apache
service: name=httpd state=started
listen: 启动并启用apache # 归为同一组
- name: 启用apache
service: name=httpd enabled=yes
listen: 启动并启用apache # 归为同一组
处理任务错误
Ansible 默认会在任务失败后终止当前主机的后续任务,以下是常见的错误处理方式。
忽略错误(ignore_errors)
让任务失败后仍继续执行后续任务,适用于 “预期可能失败” 的场景。
---
- name: 忽略错误示例
hosts: node1
tasks:
- name: 安装不存在的包(会失败)
yum:
name: notexistpackage # 不存在的包
state: present
ignore_errors: yes # 忽略此任务的错误
register: result # 保存结果
- name: 提示包不存在
debug:
msg: "包notexistpackage不存在"
when: result is failed # 若上一步失败,则执行此任务
强制执行 Handlers(force_handlers)
默认情况下,若任务失败,已通知的 Handlers 不会执行。force_handlers: yes
可强制执行。
---
- name: 强制执行Handlers
hosts: node1
force_handlers: yes # 即使任务失败,Handlers仍执行
tasks:
- name: 成功任务(通知Handler)
command: /bin/true # 成功执行
notify: 重启sshd
- name: 失败任务(安装不存在的包)
yum:
name: notexistpkg
state: latest # 会失败
handlers:
- name: 重启sshd
service: name=sshd state=restarted # 即使有任务失败,仍会执行
主动触发失败(fail 模块)
通过fail
模块主动让任务失败,通常结合when
条件使用。
- name: 主动失败示例
hosts: node1
gather_facts: no
tasks:
- debug:
msg: 任务1执行中
- fail: # 此任务必定失败
msg: "主动触发失败!" # 失败提示信息
- debug: # 不会执行(上一步已失败)
msg: 任务3执行中
自定义失败条件(failed_when)
默认任务返回非 0 退出码时失败,failed_when
可自定义失败条件(如根据命令输出判断)。
场景:执行脚本/root/adduser
,若输出含 “failed” 则任务失败。
环境准备:
# 在node1上创建脚本
[root@node1 ~]# cat /root/adduser
#!/bin/bash
# 尝试创建用户devops,成功则输出success,失败则输出failed
useradd devops &> /dev/null
if [ $? -eq 0 ];then
echo "add user devops success"
else
echo "add user devops failed"
fi
[root@node1 ~]# chmod +x /root/adduser # 加执行权限
Playbook:
- name: 自定义失败条件
hosts: node1
tasks:
- shell: /root/adduser # 执行脚本
register: result # 保存输出
failed_when: "'failed' in result.stdout" # 输出含"failed"则任务失败
自定义 changed 条件(changed_when)
默认命令成功执行即状态为changed
,changed_when
可自定义何时标记为changed
(如根据输出判断)。
场景:执行数据库升级脚本,仅当输出含 “Success” 时标记为changed
。
- name: 自定义changed条件
hosts: node1
tasks:
- name: 升级数据库
shell: /usr/local/bin/upgrade-database # 执行升级脚本
register: result
changed_when: "'Success' in result.stdout" # 输出含"Success"则标记为changed
notify: 重启数据库 # 只有changed时才通知
handlers:
- name: 重启数据库
service: name=mariadb state=restarted
若希望任务始终不标记为changed
(如查询操作),可设置changed_when: false
:
- name: 不标记为changed
hosts: node1
tasks:
- name: 查看系统版本
shell: cat /etc/redhat-release
changed_when: false # 始终为ok,不会触发Handlers
Ansible Block(任务块)
Block 用于将多个任务分组,可统一设置条件、错误处理等,类似编程中的 “代码块”。
核心用法:
block
:定义主要任务;rescue
:block
中任务失败时执行的 “补救” 任务;always
:无论block
和rescue
是否成功,始终执行的任务。
基础示例
场景:升级数据库,失败则回滚,最后始终重启数据库。
- name: 数据库升级流程
hosts: node1
tasks:
- block: # 主要任务:升级数据库
- name: 执行数据库升级
shell: /usr/local/lib/upgrade-database
rescue: # 补救任务:升级失败时回滚
- name: 回滚数据库
shell: /usr/local/lib/revert-database
always: # 始终执行:重启数据库
- name: 重启数据库
service: name=mariadb state=restarted
when: ansible_distribution == "RedHat" # 条件作用于整个block
实战案例:创建逻辑卷
需求:在所有受管节点上创建逻辑卷,满足:
- 若卷组research存在:
- 尝试创建 4000MiB 的逻辑卷
data
; - 若失败(空间不足),则创建 800MiB 的逻辑卷;
- 格式化逻辑卷为 ext4,挂载到
/data
。
- 尝试创建 4000MiB 的逻辑卷
- 若卷组
research
不存在,提示错误。
实验流程:
- 检查卷组
research
是否存在; - 存在则进入 block:尝试创建 4000MiB LV,失败则 rescue 中创建 800MiB LV;
- 无论创建成功与否,always 中执行格式化、创建目录、挂载操作;
- 不存在则直接提示错误。
Playbook:
---
- name: 创建并使用逻辑卷
hosts: all
tasks:
- block: # 卷组存在时执行
- name: 尝试创建4000MiB的LV
lvol:
vg: research # 卷组名称
lv: data # 逻辑卷名称
size: 4000 # 大小(MiB)
rescue: # 创建失败时执行(如空间不足)
- debug:
msg: "无法创建4000MiB的逻辑卷,改为创建800MiB"
- name: 创建800MiB的LV
lvol:
vg: research
lv: data
size: 800
always: # 始终执行(格式化、挂载)
- name: 格式化LV为ext4
filesystem:
fstype: ext4
dev: /dev/research/data # LV路径
- name: 创建/data目录
file:
path: /data
state: directory
- name: 挂载LV到/data
mount:
path: /data
src: /dev/research/data
fstype: ext4
state: mounted # 永久挂载(写入fstab)
when: ansible_lvm.vgs.research is defined # 条件:卷组存在
- name: 卷组不存在时提示
debug:
msg: "卷组research不存在"
when: ansible_lvm.vgs.research is not defined # 条件:卷组不存在
实施 Tags(标签)
Tags 用于只执行 Playbook 中的部分任务(无需修改 Playbook,通过命令行指定标签即可),适合大型 Playbook 的部分更新。
核心关键字:tags
,后跟标签名称(可多个)。
基础用法
场景:给 “安装 httpd” 和 “安装 postfix” 任务打标签,可单独执行。
---
- name: 标签示例
hosts: node1
gather_facts: no
tasks:
- name: 安装httpd
yum:
name: httpd
state: latest
tags: webserver # 打标签webserver
- name: 安装postfix
yum:
name: postfix
state: latest
tags: mailserver # 打标签mailserver
- name: 打印调试信息
debug:
msg: "这是一个无标签任务"
执行命令:
# 查看所有标签
[bq@controller web]$ ansible-playbook playbook.yaml --list-tags
# 只执行带webserver标签的任务
[bq@controller web]$ ansible-playbook playbook.yaml --tags webserver
# 执行所有任务,除了带webserver标签的
[bq@controller web]$ ansible-playbook playbook.yaml --skip-tags webserver
特殊标签
标签 | 说明 |
---|---|
always | 总是执行,除非用--skip-tags always 跳过 |
never | 从不执行,除非用--tags never 指定 |
all | 执行所有任务(默认,除never 外) |
tagged | 只执行带标签的任务 |
untagged | 只执行无标签的任务 |
示例:always
标签的任务总是执行:
---
- name: 特殊标签示例
hosts: node1
tasks:
- name: 总是执行的任务
debug:
msg: "我总会执行"
tags: always # 无论--tags如何指定,都会执行
如涉及版权问题请联系作者处理!!!!