Week 02: 核心原理, Facts 探测与任务逻辑编排
解析 Ansible 幂等性的数学模型, 深入探讨 Facts 搜集机制与任务控制逻辑.
1. 跨节点容错: block/rescue/always
在生产级 Playbook 中, 仅仅保证幂等性是不够的, 我们还需要处理由于网络抖动或环境异常导致的非预期中断.
1.1 结构化异常处理
block 允许我们将多个任务归类为一个逻辑单元, 并在失败时执行补救措施.
- Block: 定义正常执行的任务流.
- Rescue: 只有在
block内部的任务执行失败 (failed) 时才触发. 适合执行回退操作或发送告警. - Always: 无论成败, 始终执行. 常用于清理临时文件或重启监控状态.
- name: Database Migration with Rollback
block:
- name: Run Schema Update
command: /usr/bin/migrate_db.sh
rescue:
- name: Revert to Snapshot
command: /usr/bin/rollback_db.sh
always:
- name: Clean up temporary lock files
file:
path: /tmp/db.lock
state: absent2. 状态驱动与幂等性 (Idempotency)
2.1 深度解析: 幂等性模型
幂等性是 Ansible 的灵魂, 定义为: $f(f(x)) = f(x)$. 无论执行多少次, 系统的状态始终保持一致.
2.1.1 模块如何实现幂等性?
每一个编写良好的 Ansible 模块都会执行以下三步检查:
- Check Current State: 检测远程系统的当前配置 (如文件是否存在, 内容是否匹配).
- Compare: 对比当前状态与 Playbook 中定义的目标状态.
- Apply if Needed: 仅在两者不一致时执行修改. 返回
changed: true, 否则返回ok.
2.2 状态劫持: changed_when 与 failed_when
在执行 shell 或 command 模块时, Ansible 默认无法判断脚本内部逻辑是否由于幂等性而跳过, 通常会直接返回 changed: true. 此外, 默认失败判定仅依赖于退出码 (RC != 0).
2.2.1 changed_when: 定义变更标准
用于强制指定任务在何种条件下标记为 "已变更":
- 抑制变更:
changed_when: false. 适用于巡检类指令 (如uptime), 确保 Playbook 运行结果干净, 不会出现虚假的黄色变更标记. - 基于输出判定: 当脚本输出特定字符串时才视为变更.
- name: Run complex migration script
shell: /opt/tools/migrate.sh
register: script_res
# 只有当脚本输出包含 "Success" 且不包含 "Already up-to-date" 时才标记为 changed
changed_when:
- "'Success' in script_res.stdout"
- "'Already up-to-date' not in script_res.stdout"2.2.2 failed_when: 自定义失败逻辑
用于覆盖默认的退出码判定机制, 实现精细化的容错处理:
- 忽略特定错误: 某些工具执行成功但退出码不为 0, 或执行失败但输出中包含可忽略的警告.
- name: Perform health check
command: /usr/bin/health_check.sh
register: health_res
# 即使退出码为 0, 如果输出包含 "CRITICAL", 依然判定为失败
# 反之, 即使退出码不为 0, 只要输出包含 "WARNING", 也不触发失败
failed_when:
- "'CRITICAL' in health_res.stdout or health_res.rc != 0"
- "'WARNING' not in health_res.stdout"3. 信息观测站: Ansible Facts
Ansible Facts 是对远程主机信息的"全自动普查". 它并非静态配置, 而是在 Playbook 执行之初通过 setup 模块实时获取的系统元数据.
3.1 底层搜集与执行机制
当 Playbook 启动 Gathering Facts 时, 发生了以下底层交互:
- 模块分发: Control Node 将
setup模块的 Python 代码打包并传输至远程节点. - 指令执行: 远程节点上的 Python 解释器运行该代码, 它会通过系统调用 (System Calls) 或执行底层指令 (如
dmidecode,lsblk,ip link,facter,ohai等) 获取硬件, 内核, 网络和文件系统状态. - 结果封装: 搜集到的原始数据被序列化为 JSON 格式, 通过 SSH 加密通道回传给 Control Node.
- 内存归档: Control Node 解析 JSON 并将其存储在内存中的
hostvars变量池中.
3.2 变量映射与探索
在旧版本中, Facts 直接映射为全局变量 (如 {{ ansible_distribution }}). 为了避免变量命名冲突, 现代 Ansible 推荐使用 ansible_facts 字典:
- 推荐写法:
{{ ansible_facts['distribution'] }}或{{ ansible_facts.distribution }}. - 交互式探索: 使用 Ad-hoc 命令可以查看所有可用事实:
# 查看全量信息 ansible <host> -m setup # 结合 filter 过滤特定信息 ansible <host> -m setup -a "filter=ansible_distribution*"
3.3 局部事实 (Local Facts / Custom Facts)
除了系统内置的 Facts, Ansible 允许在远程节点定义自定义事实. 这是处理复杂业务逻辑 (如标记服务器的角色, 所属业务组) 的利器.
- 路径:
/etc/ansible/facts.d/*.fact - 格式: 支持 JSON 或 INI 格式.
- 访问方式: 统一归档在
ansible_facts.ansible_local下.
# 在远程节点创建一个自定义事实
mkdir -p /etc/ansible/facts.d
echo '{"app_version": "2.4.1"}' > /etc/ansible/facts.d/app.fact3.4 性能优化: Fact Caching & Gathering Subset
在大规模集群中, 频繁搜集 Facts 会显著增加执行时间 (SSH 通道开销与 Python 启动开销).
- Fact Caching: 将 Facts 持久化到 Redis, Memcached 或 Jsonfile 中, 过期时间内不再重复搜集.
# ansible.cfg [defaults] gathering = smart fact_caching = jsonfile fact_caching_connection = /tmp/ansible_facts_cache fact_caching_timeout = 86400 - Gather Subset: 如果只需要网络信息, 可以限制搜集范围:
- name: Quick Play hosts: all gather_facts: yes gather_subset: - '!all' - 'network' - 禁用搜集: 若 Playbook 纯粹是分发文件或重启服务, 且不涉及变量判断, 声明
gather_facts: false可获得极致速度.
3.5 Facts 超时与重试机制
在网络不稳定或 Facts 收集耗时过长的场景下, 需要配置超时和重试策略:
# ansible.cfg
[defaults]
# 全局任务超时 (秒)
timeout = 30
# Facts 收集专用超时
gather_timeout = 30
[connection]
# SSH 连接超时
connect_timeout = 10任务级别的超时控制:
- name: Gather facts with extended timeout
setup:
gather_subset:
- hardware
- network
async: 120
poll: 5
register: facts_result
- name: Retry on failure
setup:
retries: 3
delay: 10
until: facts_result is succeeded3.6 自定义 Facts 收集器开发
除了在远程主机部署静态 .fact 文件外, 还可以开发动态的 Facts 收集脚本:
#!/bin/bash
# /etc/ansible/facts.d/app_status.fact
# 必须可执行: chmod +x /etc/ansible/facts.d/app_status.fact
# 动态收集应用状态
APP_PID=$(pgrep -f "myapp" 2>/dev/null || echo "")
APP_VERSION=$(cat /opt/myapp/version.txt 2>/dev/null || echo "unknown")
APP_UPTIME=""
if [ -n "$APP_PID" ]; then
APP_UPTIME=$(ps -o etime= -p $APP_PID | tr -d ' ')
fi
cat << EOF
{
"app": {
"name": "myapp",
"version": "${APP_VERSION}",
"pid": "${APP_PID}",
"uptime": "${APP_UPTIME}",
"is_running": $([ -n "$APP_PID" ] && echo "true" || echo "false")
}
}
EOF访问方式:
- name: Check application status
debug:
msg: |
App Version: {{ ansible_facts['ansible_local']['app_status']['app']['version'] }}
Running: {{ ansible_facts['ansible_local']['app_status']['app']['is_running'] }}
when: ansible_facts['ansible_local']['app_status'] is defined3.7 Facts 安全过滤 (敏感信息脱敏)
在某些合规场景下, Facts 中可能包含敏感信息 (如硬件序列号, MAC 地址, 用户列表), 需要进行过滤:
方式 1: 使用 gather_subset 排除敏感类别:
- hosts: all
gather_facts: yes
gather_subset:
- "!hardware" # 排除硬件信息 (含序列号)
- "!facter" # 排除 facter 数据
- network
- virtual方式 2: 使用 Callback 插件过滤 Facts:
# callback_plugins/fact_filter.py
from ansible.plugins.callback import CallbackBase
import copy
SENSITIVE_KEYS = [
'ansible_product_serial',
'ansible_chassis_serial',
'ansible_board_serial',
'ansible_all_ipv4_addresses',
'ansible_all_ipv6_addresses',
]
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'fact_filter'
def v2_runner_on_ok(self, result):
if result._task.action == 'setup' or result._task.action == 'gather_facts':
facts = result._result.get('ansible_facts', {})
for key in SENSITIVE_KEYS:
if key in facts:
facts[key] = '[REDACTED]'方式 3: 审计 Facts 使用情况:
- name: Log which facts are accessed
debug:
msg: "Accessed fact: ansible_facts['distribution']"
tags: [audit]4. Ad-hoc 指令
如果说 Playbook 是编写好的 "剧本", 那么 Ad-hoc 就是 "即兴表演". 它允许用户在不编写 YAML 文件的情况下, 直接通过命令行向一台或多台主机发送单条指令.
4.1 使用场景: 什么时候该用 Ad-hoc?
- 临时巡检: 快速查看集群的磁盘空间, 内存使用率或进程状态.
- 应急处理: 批量重启某个服务或下发一个临时补丁.
- 测试验证: 在编写 Playbook 前, 验证某个模块在特定发行版下的行为.
- 一次性任务: 仅需执行一次的操作 (如批量分发公钥, 清理旧日志).
4.2 命令结构解析
基础语法: ansible <pattern> -m <module> -a "<arguments>" [options]
- Pattern: 主机组或 IP 匹配规则 (如
all,web_servers,192.168.1.*). - Module: 要调用的功能模块 (默认为
command). - Arguments: 传递给模块的参数.
- Forks (-f): 控制并行度. Ansible 默认并发数为 5.
4.3 常用模块与实战示例
| 模块名 | 功能描述 | Ad-hoc 示例 |
|---|---|---|
| command | 执行简单指令 (不支持环境变量, 变量和管道) | ansible all -a "uptime" |
| shell | 在远程主机调用 shell 执行 (支持管道 '|' 和变量) | ansible all -m shell -a "df -h | grep /dev/sda1" |
| ping | 测试控制端与被控端的连通性及 Python 环境 | ansible all -m ping |
| copy | 从控制端传输文件到远程主机 | ansible all -m copy -a "src=~/app.py dest=/tmp/app.py" |
| file | 管理文件状态, 权限或建立软链接 | ansible web -m file -a "path=/opt/log state=directory mode=0755" |
| yum / apt | OS 特定的软件包管理 (RedHat/Debian 系列) | ansible all -m yum -a "name=vim state=present" |
| package | 通用软件包管理器接口 (自动适配 yum/apt/dnf) | ansible all -m package -a "name=nginx state=present" |
| service | 管理系统服务状态 | ansible all -m service -a "name=ntp state=started" |
| cron | 管理远程主机的 crontab 计划任务 | ansible all -m cron -a "name='backup' hour='2' job='/usr/bin/backup.sh'" |
| user / group | 管理系统用户及组账号 (支持 uid/gid 指定) | ansible all -m user -a "name=deploy uid=2000 group=wheel" |
| get_url | 从 HTTP/HTTPS/FTP 下载文件到远程主机 | ansible all -m get_url -a "url=http://site.com/conf dest=/tmp/" |
| unarchive | 解压本地或远程压缩包到指定目录 | ansible all -m unarchive -a "src=/tmp/pkg.tar.gz dest=/opt/" |
| mount | 配置并挂载文件系统 | ansible all -m mount -a "path=/data src=/dev/sdb1 fstype=xfs state=mounted" |
| filesystem | 在块设备上创建文件系统 (格式化) | ansible all -m filesystem -a "fstype=ext4 dev=/dev/sdb1" |
| synchronize | 利用 rsync 高效同步目录 (需安装 rsync) | ansible all -m synchronize -a "src=/local/data/ dest=/backup/" |
4.4 底层执行流与并发机制 (Parallelism)
当执行一条 Ad-hoc 命令时, 后台发生了如下底层交互:
- 解析清单 (Inventory): 根据 Pattern 确定目标主机.
- 生成逻辑单元: Control Node 将所选模块和参数编译成一个临时的 Python 脚本 (Zip 压缩包).
- 开启并发 (Forks): 开启多个子进程 (默认为 5), 通过 SSH 连接到远程节点.
- 传输与执行: 将脚本传输至远程节点的内存缓存或临时目录 (
~/.ansible/tmp/), 并调用/usr/bin/python执行. - 结果收回: 读取执行输出并解析为标准 JSON 格式, 最后回传给 Control Node 并在终端渲染.
深度提示: 增加 forks 可以显著缩短大规模集群的操作时间, 但会消耗控制节点的 CPU 和带宽. 通常建议设置为 CPU 核心数 * 10 以内, 并配合 pipelining = True (在 ansible.cfg 中) 以减少 SSH 交互次数.
5. 编排基石: Playbook 基础结构
Playbook 是由一个或多个 "Play" 组成的列表. 每个 Play 的核心使命是将一组变量, 任务和角色映射到特定的主机组上.
5.1 核心组成部分
一个标准 Play 通常包含以下四个维度:
- Target (目标):
hosts定义了任务在哪些机器上运行. - Privilege (权限):
become: yes允许任务以 root 或其他用户身份提权执行. - Variable (变量):
vars定义了 Play 级别的静态变量. - Action (动作):
tasks是有序的任务列表, 按顺序在所有目标主机上执行.
5.2 运行指令与状态验证
编写完成后, 使用 ansible-playbook 命令触发编排. 为了确保生产安全, Ansible 提供了两种极其重要的验证模式:
- Check Mode (
--check):- 原理: 模拟执行, 模块会预测结果但不会真实修改系统.
- 局限性: 某些模块 (如
shell) 默认不支持 Check mode, 会直接跳过或报错. 可以在任务中设置check_mode: no强制其在检查模式下也真实执行, 或check_mode: yes强制模拟.
- Diff Mode (
--diff):- 原理: 针对文本文件 (如
template,copy), 展示"当前值"与"目标值"的具体差异 (类似diff -u). 常与--check结合使用.
- 原理: 针对文本文件 (如
# 模拟并展示文件变更差异 (生产推荐)
ansible-playbook -i inventory.ini site.yml --check --diff
# 限制运行范围
ansible-playbook -i inventory.ini site.yml --limit webservers5.3 极简示例: 管理 Nginx
---
- name: Deploy and Start Nginx
hosts: web_servers
become: yes
vars:
http_port: 80
tasks:
- name: Install Nginx package
yum:
name: nginx
state: present
- name: Ensure Nginx is running and enabled
service:
name: nginx
state: started
enabled: yes5.4 进阶示例: 用户与组管理
在生产环境中, 通常需要先创建特定的管理组, 再创建属于该组的服务账号.
---
- name: Infrastructure User Management
hosts: all
become: yes
tasks:
- name: Create developer group
group:
name: devops
gid: 3000
state: present
- name: Create deployment user
user:
name: deployer
uid: 3001
group: devops # 主组 (可填组名或 GID)
shell: /bin/bash
create_home: yes
groups: wheel,docker # 附加组
append: yes # 增量添加, 不覆盖原有组
state: present6. 任务编排与逻辑控制
如果说 Facts 是"传感器", 那么控制流就是 Ansible 的"大脑", 决定了 Playbook 面对复杂环境时的响应能力.
6.1 结果捕获: register
register 关键字允许你将一个任务的执行结果存储在一个变量中, 以供后续任务分析.
- 数据结构: 注册变量是一个复杂的字典 (Dictionary), 包含
stdout,stderr,rc(Return Code),changed,failed等关键 Key. - 链式决策: 常用于先执行一个探测性脚本, 捕获其结果, 然后在后续任务中根据该值进行逻辑分支.
- name: Check if custom config exists
stat:
path: /etc/myapp/custom.conf
register: config_status
- name: Create fallback config if missing
copy:
src: default.conf
dest: /etc/myapp/custom.conf
when: not config_status.stat.exists6.2 条件判断: when
when 子句是 Ansible 实现动态逻辑的核心. 它利用 Jinja2 表达式对任务的执行环境进行实时判定.
6.2.1 运算符与逻辑深度
Ansible 支持标准的 Python/Jinja2 比较与逻辑运算:
| 类型 | 运算符 / 关键字 | 示例 |
|---|---|---|
| 比较 | ==, !=, >, <, >=, <= | ansible_machine == "x86_64" |
| 逻辑 | and, or, not, () | (A or B) and not C |
| 包含 | in, not in | "RedHat" in ansible_os_family |
| 存在性 | is defined, is undefined | my_var is defined |
| 状态测试 | is success, is failed, is skipped | result is success |
| 类型测试 | is number, is string, is directory | path is directory |
6.2.2 列表形式的隐式 "与" (AND)
除了在单行书写复杂的 and 逻辑, Ansible 允许将多个条件写成一个列表. 这种方式可读性更好, 且所有条件必须同时满足 (隐式 AND):
when:
- ansible_facts['distribution'] == "CentOS"
- ansible_facts['distribution_major_version'] | int >= 7
- max_memory_gb > 86.2.3 综合实战示例
- name: Advanced Conditional Example
shell: /usr/bin/verify_system.sh
register: check_result
ignore_errors: yes
- name: Remediation Task
service:
name: myservice
state: restarted
when:
# 逻辑 1: 命令执行失败 (通过 register 结果判断)
- check_result.rc != 0
# 逻辑 2: 且变量已定义且不为空
- fix_enabled is defined and fix_enabled | bool
# 逻辑 3: 且不是在 Ubuntu 系统上
- ansible_facts['os_family'] != "Debian"6.3 循环演进: loop
从 Ansible 2.5 开始, loop 逐渐替代了繁杂的 with_items, with_dict 等语法.
- 本质:
loop会将列表中的每个元素迭代地传递给模块执行, 本质上是将一个 Task 定义转换为了多个原子任务执行单元. - 结合 Filter: 结合
dict2items等过滤器, 可以轻松处理复杂的嵌套数据结构. - 性能考量: 大规模循环时, 若模块本身支持批量操作, 应优先使用模块本身的批量特性, 以减少 SSH 会话开销.
- name: Create multiple users simultaneously
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
group: "{{ item.primary_group }}"
groups: "{{ item.extra_groups }}"
append: yes
state: present
loop:
- { name: 'alice', uid: 2001, primary_group: 'wheel', extra_groups: 'docker,bin' }
- { name: 'bob', uid: 2002, primary_group: 'devops', extra_groups: 'wheel' }6.4 状态变更触发器: handlers
handlers 用于实现"只有当发生变化时才执行"的逻辑, 是实现生产环境解耦的关键.
- 通知机制 (
notify): 当一个任务返回changed: true时, 它会向任务堆栈发送一个信号, 标记特定 Handler 需要运行. - 执行时机: 默认情况下, 所有标志为"待运行"的 Handlers 都会在整个 Play 的末尾按顺序执行一次. 这种机制保证了即使多个任务修改了 nginx 配置,
restart nginx也只会被执行一遍. - 强制执行: 如果 Play 运行中途失败, 后续的 Handlers 默认不会运行. 若需确保状态一致性, 可使用
force_handlers: true配置.
tasks:
- name: Update configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart Nginx
handlers:
- name: Restart Nginx
service:
name: nginx
state: restarted7. 本周实战任务
7.1 Ad-hoc 快速演练
使用命令行快速完成以下任务, 验证对基础模块的掌握:
- 连通性测试: 使用
ping模块确认所有主机的 Python 环境正常. - 临时巡检: 使用
shell模块结合管道, 查看所有节点/var/log目录下占用空间最大的前三个文件. - 用户清理: 使用
user模块删除一个名为guest的临时账号, 并确保其家目录一并清理.
7.2 编写 "状态优先" 的基础架构编排
编写一个综合 Playbook (infra.yml), 实现以下逻辑:
- 组与用户初始化:
- 创建 GID 为
4000的组app_admin. - 使用
loop批量创建两个用户web_user(UID: 4001) 和db_user(UID: 4002), 均加入app_admin组, 且附加wheel权限.
- 创建 GID 为
- 目录环境准备:
- 使用
loop检查/data/web和/data/log路径. - 结合
register和when, 仅在路径不存在时创建目录, 并设置属主为web_user.
- 使用
- 服务状态闭环:
- 部署一个基础配置文件 (使用
copy或template). - 配置
notify触发handler, 实现服务配置变更后的自动重启.
- 部署一个基础配置文件 (使用
- 幂等性验证: 连续运行两次 Playbook, 观察第二次运行是否实现全绿 (所有 Task 状态为
ok, 无changed).
7.3 事实搜集与缓存优化
- 在
ansible.cfg中开启jsonfile缓存, 并设置过期时间为 1 天. - 对比开启缓存前后的
Gathering Facts阶段耗时. - 使用
ansible-inventory --list或直接查看缓存文件, 观察 Facts 的存储结构.
不要把 Playbook 看成脚本, 而要把它看成是一张"期望状态图".