Ansible 自动化运维工具扩展资料

Ansible 自动化运维工具扩展资料 1. 自动化运维基础 1.1 运维工作与发展 1.1.1 运维岗位的工作内容 运维的核心职责是保障系统及服务的稳定性、可用性,使业务持续为用户提供服务。全年 7×24 小时是服务保障目标,实际还需定义可用性指标、维护窗口和故障恢复目标。 工作还包括: 提升效率

Ansible 自动化运维工具扩展资料

1. 自动化运维基础

1.1 运维工作与发展

1.1.1 运维岗位的工作内容

运维的核心职责是保障系统及服务的稳定性、可用性,使业务持续为用户提供服务。全年 7×24 小时是服务保障目标,实际还需定义可用性指标、维护窗口和故障恢复目标。

运维岗位的课堂示意

工作还包括:

  • 提升效率:通过工具、自动化和平台提高研发生命周期效率。
  • 控制成本:优化架构、资源和技术方案,在服务目标与成本间取得平衡。
  • 发布管理:让发版可控、可验证、可回退。
  • 变更管理:记录变更内容、人员、时间和结果。
  • 备份恢复:定期备份重要数据并验证恢复能力。
  • 灾难演练:演练故障预案,确认可操作性。
  • 系统分析:采集日志与指标,识别风险、定位问题并准备预案。
  • 技术选型:根据业务规模、团队能力和成本选择产品与架构。

平台架构、日常维护与性能成本优化

项目生命周期中,运维通常参与四个阶段:

阶段 运维工作
首次上线 组建平台架构,部署基础环境和业务服务。
日常维护 保障稳定运行,支持迭代升级与故障处理。
性能效率优化 分析瓶颈,提升性能、容量与资源利用率。
项目下线 备份归档数据,确认依赖与保留期限,再释放资源。

1.1.2 职业发展

运维可向应用运维、开发运维、云平台、云计算、大数据、系统架构等方向发展。路线随组织分工而异,不是必须逐级经过的职称序列。

运维工程师常见发展路线

基础运维、自动化、平台、智能运维与架构方向

1.1.3 DevOps 与 SRE

DevOps 强调开发和运维协作,通过工具与流程缩短交付反馈周期;SRE 用软件工程方法管理可靠性,围绕服务目标、自动化和持续改进减少重复劳动。SRE 是落实部分 DevOps 思路的一种实践方式,两者不是互斥岗位。

DevOps、SRE 与研发运维协作

自动化的目标包括自助交付和运营,让研发能够借助平台、工具及规范完成常规操作,运维通过代码与工程能力让服务保持可控、稳定和持续运行。

1.1.4 项目生命周期

产品从无到有,通常经历调研、设计、研发、测试、运营。各阶段由相关团队协作,并通过反馈推进后续迭代。

调研、设计、研发、测试和运营

需求评审、研发测试与上线反馈流程

1.1.5 项目环境

环境 主要人员 常见平台 特点 工作内容 阶段标准
个人开发 开发人员 个人工作站 可定制、与生产有差异 开发子模块 子模块完成。
公司开发 开发团队 内部服务器 统一的开发基线 模块、项目联调 项目阶段可运行。
测试 测试及相关研发 隔离的测试平台 可重复的测试环境 功能、性能及其他非功能测试 达到阶段测试准入条件。
预发布 运维、测试、研发 接近生产的隔离环境 尽量与生产一致 发布验证、专项测试 达到发布条件。
灰度 发布及业务团队 部分生产实例或流量 有限范围实际用户验证 分批发布与观察 达到扩大范围条件。
生产 运维及业务团队 生产平台 受控的标准运行环境 正式运营 满足约定服务目标。

“预发布”不意味着必须与正式生产共用服务器;“发布成功”需要业务验证及指标观察,不能只看进程启动。

项目环境、主机环境与配置环境的层次

一个主机业务环境又包含基础操作系统、设备与网络、应用、项目和配置等层次,环境标准应按产品需求及实际生产约束制定。

1.1.6 程序发布流程

发布前确认目标分支已达到测试和功能准入标准,已知问题已处理或有明确接受方案、备份与回退计划。安排发布时间时避开业务高峰,并考虑值守能力和业务窗口。

一次分批发布可以按以下顺序进行:

  1. 从调度器摘除一批主机,等待请求排空。
  2. 根据部署方式停止需更新的服务。
  3. 部署新版本及配套配置。
  4. 启动并检查服务。
  5. 验证业务、依赖和指标。
  6. 将验证通过的主机接回调度器。
  7. 再次观察后继续下一批,异常则停止推进并按预案恢复。

灰度和蓝绿可以降低变更影响范围,但都需要流量控制、容量和数据兼容设计,不等于只把版本分批复制。

1.2 自动化运维方式与工具

1.2.1 管理方式

手工、自动、封装和智能管理方式

方式 形成过程
手工 先实现功能,再规范操作和流程。
自动化 精简流程,编写脚本,再通过工具自动执行。
封装 封装基础环境与应用,再编排任务。
智能化 结合历史数据、规则和模型辅助分析与执行。

企业可能混合使用多种方式;规模与自动化成熟度并不是严格对应关系。“自动化加封装”与“封装加自动化”强调不同的基础能力和投入顺序,应按业务需求选择。

1.2.2 工具特点

脚本能完成自动化,但复杂流程、环境差异、交接和长期维护会增加成本。配置管理工具将资源、状态、任务和环境差异纳入统一管理;它们仍需要学习模型、语法和使用边界,专用文件多了也需要组织和维护。

Ansible、SaltStack、Puppet、Chef 与 Fabric

以下为课件记录的 GitHub 历史数据,不能当作当前热度:

工具 2021-12-23 Watch Star Fork 贡献者 主要语言
Ansible 1990 51083 21375 5292 Python
SaltStack 563 12091 5214 2334 Python
Puppet 483 6411 2217 565 Ruby
Chef 386 6769 2552 629 Ruby
Fabric 467 13173 1853 14 Python
工具 2025-05-26 Watch Star Fork 贡献者 主要语言
Ansible 1895 65158 24027 5561 Python
SaltStack 527 14529 5525 2450 Python
Puppet 462 7595 2202 589 Ruby
Chef 363 7781 2519 789 Ruby
Fabric 456 15117 1945 15 Python

课件中的 Ansible 源码页面

课件中的 Salt 源码页面

2. Ansible 架构、安装与认证

2.1 Agentless 架构

Ansible 常规 Linux 管理不要求受管节点常驻专用 Agent,通常复用 SSH。执行普通 Python 模块时,控制端会准备任务、经连接插件访问目标、传送或通过 pipelining 提供模块、执行并收集结果,最后尽力清理临时文件。

前置依赖按实际连接和模块决定:

  • 控制端与普通 Linux Python 模块所需的目标 Python 必须满足所用 ansible-core 支持矩阵,现代版本不能继续套用 Python 2 的通用要求。
  • SSH 用户、认证、权限、主机身份校验和目标监听端口须就绪;端口可以不是 22。
  • raw、script 等特定方式可用于没有目标 Python 的场景。
  • Windows、网络设备和 API 管理也可使用其他连接协议,不是所有目标都仅靠 OpenSSH。

无专用常驻 Agent 不表示零资源开销,任务运行仍消耗资源,SSH 服务也属于常驻服务。中断、不可达、异步任务或显式保留文件等情况下,不能保证“没有任何临时残留”。

与 Puppet、Chef、Salt 等工具比较,应区分其实际部署模式:它们并非全部只能定时拉取,也有实时事件、远程执行、独立或 SSH 模式。Agentless 通常降低初始部署负担,但大规模并发受连接、控制端能力和任务类型影响,不能按固定“超过一千台就低效”划线。

2.2 工作特性与组件

  • Python 实现为主,插件化扩展连接、清单、回调和模块等功能。
  • 常见依赖包括 PyYAML、Jinja2,连接可使用 OpenSSH 或特定插件;Paramiko 不是每种连接都必须走的固定环节。
  • YAML Playbook 描述编排,Role 组织任务、变量、模板、文件与 Handlers。
  • 模块可以用合适语言实现,但必须满足 Ansible 模块的调用、参数和结果协议,不能理解为任意程序直接可当模块。
  • 幂等性取决于模块和任务实现,shell、command、script 不会天然自动识别一切目标状态。
  • SSH 传输加密与主机信任、账号授权是不同问题;Agentless 不意味着不需要身份校验。

用户入口、控制端与受管主机

控制端处理来自 CLI、Playbook 或外部平台的请求,读取清单、变量和配置,经连接及动作插件调用模块并展示结果。外部 API 一般由自动化平台提供;安装 CLI 本身不会自动开放 Web API。

Ad-Hoc 适合临时和简单操作,Playbook 更适合可重复的复杂流程;选择不只由企业大小决定。可用于云资源、设备、应用、配置、持续交付和安全管理。

2.3 其他安装方式

2.3.1 Ubuntu 本地 DEB 与依赖修复

PPA 网络慢时,课件记录:

apt install ansible
获取:14 https://ppa.launchpadcontent.net/ansible/ansible/ubuntu noble/main amd64 ansible all 11.11.0-1ppa~noble [19.5 MB]
25% [14 ansible 220 kB/19.5 MB 1%] 14.9 kB/s 21分 33秒
26% [14 ansible 497 kB/19.5 MB 3%] 10.9 kB/s 29分 5秒

可从可信源获取与发行版匹配的软件包并核验完整性,再传到目标主机;这不会自动解决依赖下载问题。

课件浏览 PPA 软件包目录

ls
ansible_11.11.0-1ppa~noble_all.deb

直接使用 dpkg 的历史过程:

dpkg -i ./ansible_11.11.0-1ppa~noble_all.deb
dpkg: 依赖关系问题使得 ansible 的配置工作不能继续:
 ansible 依赖于 ansible-core;然而:
  未安装软件包 ansible-core。
dpkg: 处理软件包 ansible (--install)时出错:
 依赖关系问题 - 仍未被配置

课件再次执行:

apt install ./ansible_11.11.0-1ppa~noble_all.deb
您也许需要运行“apt --fix-broken install”来修正上面的错误。
ansible : 依赖: ansible-core 但是它还没有被安装
          推荐: python3-jmespath 但是它还没有被安装
          推荐: python3-winrm 但是它还没有被安装
E: 有未能满足的依赖关系。

按实际解析方案修复,再核对版本:

apt --fix-broken install
ansible --version

通常可以一开始就使用 apt install ./包名.deb 让 APT 处理依赖;需保留匹配的可用源。--fix-broken 可能提出安装或移除方案,不能无条件假定只会安装期望的包。

2.3.2 Rocky 软件包安装

dnf install epel-release
dnf install ansible
rpm -q ansible
ansible --version

是否需要 EPEL、仓库提供 ansible 还是 ansible-core,取决于发行版大版本与启用仓库,不能说官方安装仅支持 Ubuntu 与 pip。

课件历史输出实际上来自 EL9/Python 3.9 环境,虽然提示符写 rocky10-12,不能据此当成 Rocky 10 的安装结果:

ansible-7.7.0-1.el9.noarch
ansible [core 2.14.17]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.9/site-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.9.18 (main, Jan 24 2024, 00:00:00) [GCC 11.4.1 20231218 (Red Hat 11.4.1-3)] (/usr/bin/python3)
  jinja version = 3.1.2
  libyaml = True

2.3.3 Python 虚拟环境安装

Ubuntu 等发行版可能通过 PEP 668 保护系统 Python 的 externally managed 环境,这不是“root 用户不能使用 pip”的限制。使用 venv 可以隔离依赖,不覆盖系统管理的 Python 软件包。

apt install python3 python3-venv python3-pip -y
mkdir my_project
cd my_project
python3 -m venv venv
source venv/bin/activate
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple ansible
ansible --version

原实验:

ansible [core 2.18.6]
  config file = None
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /root/my_project/venv/lib/python3.12/site-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /root/my_project/venv/bin/ansible
  python version = 3.12.3 (main, Nov 6 2024, 18:32:19) [GCC 13.2.0] (/root/my_project/venv/bin/python3)
  jinja version = 3.1.4
  libyaml = True

(venv) 提示说明已激活虚拟环境,config file = None 表示未加载配置文件,不等于安装失败。退出:

deactivate

参考:安装指南中文镜像。版本支持和依赖仍须按所选发行版本核对。

2.3.4 Git 源码开发环境

源码运行适合开发或特定调试需求,不只是改一改版本号就能替代受支持安装。先准备兼容的 Python、隔离环境及该分支的开发依赖。

mkdir -p /data/codes
cd /data/codes
git clone https://github.com/ansible/ansible.git
cd ansible
source ./hacking/env-setup
ls -l bin/
python3 bin/ansible --version

源码入口改用官方地址;代理或镜像可按可信的实际网络环境配置。hacking/env-setup 的支持方式随分支变化,应按该版本开发文档操作,不假定自动安装全部依赖。

原课件 bin/ 内容包括以下符号链接,路径和链接形式随版本可能变化:

ansible           -> ../lib/ansible/cli/adhoc.py
ansible-config    -> ../lib/ansible/cli/config.py
ansible-console   -> ../lib/ansible/cli/console.py
ansible-doc       -> ../lib/ansible/cli/doc.py
ansible-galaxy    -> ../lib/ansible/cli/galaxy.py
ansible-inventory -> ../lib/ansible/cli/inventory.py
ansible-playbook  -> ../lib/ansible/cli/playbook.py
ansible-pull      -> ../lib/ansible/cli/pull.py
ansible-test      -> ../test/lib/ansible_test/_util/target/cli/ansible_test_cli_stub.py
ansible-vault     -> ../lib/ansible/cli/vault.py

原课件版本检查的关键输出:

config file = None
configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /data/codes/ansible/lib/ansible
ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
executable location = bin/ansible
python version = 3.12.3 (main, Nov 6 2024, 18:32:19) [GCC 13.2.0] (/usr/bin/python3)
jinja version = 3.1.2
libyaml = True

2.4 认证管理

2.4.1 手工输入连接密码

ansible -h | grep '\-k,'
ansible 10.0.0.13 -a 'ls /root' -k
ansible ansible_server -a 'ls /root' -k
-k, --ask-pass        ask for connection password
SSH password:
10.0.0.13 | CHANGED | rc=0 >>
ansible.cfg
snap

目标参数可以是清单中的主机名、IP 或组;仍要匹配已加载的清单。-k 请求 SSH 连接密码,-K 则请求提权密码,两者不同。

known_hosts 用来核验服务器身份,用户密码或私钥用来认证登录用户;清单定义目标和连接变量。 不能把 Ansible 认证本质归结为 known_hosts,或者认为清单只是中间人。

2.4.2 连接属性与 local 连接

课件在主机条目加入 ansible_connection=local 后,即使没有 SSH 凭据也能执行任务,因为它在控制节点本地运行,根本没有连接远程 IP。不能把这当作绕过 SSH 认证的远程连接实验。

备份清单后可在专用实验清单中设置:

[ansible_server]
10.0.0.13 ansible_connection=local ansible_python_interpreter=/usr/bin/python3

解释器路径须在控制端实际存在。local 模式中密码变量无助于远程认证,不需要再加入明文 ansible_ssh_pass。

cp -a /etc/ansible/hosts /etc/ansible/hosts.bak
ansible 10.0.0.13 -m ansible.builtin.ping
ansible ansible_server -m ansible.builtin.ping
10.0.0.13 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

原课件为了“清除认证”删除整个 .ssh,已去掉该做法;其中还可能有私钥、authorized_keys 和其他主机信任记录。无需删除它们来说明 local 与 SSH 的区别。

2.4.3 SSH 公钥认证

切回 SSH 清单;只在确认备份对应目标状态时还原:

cp /etc/ansible/hosts.bak /etc/ansible/hosts
[ansible_server]
10.0.0.13 ansible_python_interpreter=/usr/bin/python3

生成一对尚不存在的专用密钥,向授权的实验账号安装公钥:

ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_ansible
ssh-copy-id -i ~/.ssh/id_ed25519_ansible.pub root@10.0.0.13
ansible 10.0.0.13 -m ansible.builtin.ping --private-key ~/.ssh/id_ed25519_ansible
ansible ansible_server -m ansible.builtin.ping --private-key ~/.ssh/id_ed25519_ansible

原文使用 RSA,符合目标策略的 RSA 密钥同样可用;这里使用专用密钥,后续清单显式引用它。Root SSH 是否允许、账号是否需提权,取决于目标策略,不是 Ansible 自动授予。

2.5 目标主机匹配

2.5.1 匹配规则

形式 作用
all 当前清单中所有主机。
*、10.0.0.* 通配匹配,不是真正正则表达式。
web:mysql 并集。
web:&mysql 交集。
web:!storage 从 web 中排除 storage。
~正则表达式 使用正则形式的主机模式,区别于 *。
--limit / -l 在本次目标基础上进一步限制。

这些模式作用于已加载的清单;不会按网段自动扫描发现主机。含 *、&、! 的参数优先用单引号,避免 Shell 提前解释。

2.5.2 实验清单

原文表格与实际清单不一致。下面按其清单和后续 graph 输出统一:

IP 课堂主机名 所属组 用途
10.0.0.13 ubuntu24-13.ansible.com web、mysql 控制端、受管端。
10.0.0.12 rocky10-12.ansible.com web、storage 受管端;部分输出其实来自 Rocky 9。
10.0.0.16 ubuntu24-16.ansible.com mysql、storage 受管端。

/etc/ansible/hosts 示例:

10.0.0.13

[web]
10.0.0.12
10.0.0.13

[mysql]
10.0.0.13
10.0.0.16

[storage]
10.0.0.12
10.0.0.16

[all:vars]
ansible_user=root
ansible_ssh_private_key_file=~/.ssh/id_ed25519_ansible

独立列出的 .13 又属于其他组,因此最终不在 ungrouped 中。同一个主机可属于多个组,但只代表同一清单主机对象。

按前一节已有密钥安装公钥,不重复生成或覆盖:

for i in 12 13 16; do
  ssh-copy-id -i ~/.ssh/id_ed25519_ansible.pub root@10.0.0.$i
done
ansible all -m ansible.builtin.ping

原文还提到配套 01-root_ssh_auth.sh,此 PDF 没有脚本正文,不能当作本目录已经附带。

原实验三台主机均返回:

10.0.0.13 | SUCCESS => {"changed": false, "ping": "pong"}
10.0.0.16 | SUCCESS => {"changed": false, "ping": "pong"}
10.0.0.12 | SUCCESS => {"changed": false, "ping": "pong"}

ping 是 Ansible 模块执行测试,不是 ICMP ping;普通 Linux 目标还检查相应连接与 Python 模块运行能力。

2.5.3 过滤与逻辑操作

ansible all -m ansible.builtin.ping
ansible '*' -m ansible.builtin.ping
ansible '10.0.0.*' -m ansible.builtin.ping
ansible web -m ansible.builtin.ping
ansible all -m ansible.builtin.ping -l 10.0.0.13
ansible 'web:mysql' -m ansible.builtin.ping
ansible 'web:10.0.0.16' -m ansible.builtin.ping
ansible 'web:&mysql' -m ansible.builtin.ping
ansible ungrouped -m ansible.builtin.ping
ansible 'web:!storage' -m ansible.builtin.ping
ansible 'all:!web' -m ansible.builtin.ping
ansible 'web:&mysql:!storage' -m ansible.builtin.ping

按上面的清单:

模式 匹配结果
all、*、10.0.0.* .12、.13、.16。
web .12、.13。
web:mysql、web:10.0.0.16 .12、.13、.16。
web:&mysql .13。
ungrouped 空,原输出 [WARNING]: No hosts matched, nothing to do。
web:!storage .13。
all:!web .16。
web:&mysql:!storage .13。

原文写“web 和 mysql 共有”时使用 web:mysql:!storage,冒号实际上先求并集;本例恰好同样剩 .13,不能因为结果相同就把并集误当交集。原来的 :!web 已写成更清楚的 all:!web。

2.6 命令执行流程

配置文件通常按 ANSIBLE_CONFIG、当前目录、用户目录、系统目录等优先级查找;不是永远加载 /etc/ansible/ansible.cfg。不安全目录等还有额外规则,应以 ansible --version 的实际结果为准。

典型流程:

  1. 读取配置、清单和变量,选择主机模式。
  2. 加载动作、连接和模块等插件,默认 Ad-Hoc 模块通常为 command。
  3. 建立或复用连接,准备远端临时目录。
  4. 传送模块包装文件;启用 pipelining 等时流程可变化。
  5. 在目标端执行模块并收集标准输出、错误和返回状态。
  6. 尽力清理临时文件,由控制端回调展示结果。

~/.ansible/cp 主要保存 SSH 复用控制 socket,不是模块临时脚本目录;~/.ansible/tmp 等位置由配置决定。forks 限制并发工作进程,不能拿调试输出中出现几条 SSH 命令推断并发量。SSH ControlPersist 也可能在任务后继续保留连接。

执行原实验的调试入口:

ansible 10.0.0.12 -m ansible.builtin.command -a 'ls /root' -vvv

原记录的阶段与关键输出如下,保留其路径、模块和状态;重复的 OpenSSH 版本及协商行合并展示:

ansible [core 2.18.6]
  config file = /root/ansible.cfg
  ansible python module location = /usr/lib/python3/dist-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.12.3 (/usr/bin/python3)
  jinja version = 3.1.2
  libyaml = True
Using /root/ansible.cfg as config file
host_list declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
script declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
auto declined parsing /etc/ansible/hosts as it did not pass its verify_file() method
Parsed /etc/ansible/hosts inventory source with ini plugin
Skipping callback 'default', as we already have a stdout callback.
Skipping callback 'minimal', as we already have a stdout callback.
Skipping callback 'oneline', as we already have a stdout callback.
<10.0.0.12> ESTABLISH SSH CONNECTION FOR USER: None
SSH options: ControlMaster=auto ControlPersist=60s ConnectTimeout=10
ControlPath="/root/.ansible/cp/4121990a2e"
OpenSSH_9.6p1 Ubuntu-3ubuntu13.5, OpenSSL 3.0.13 30 Jan 2024
Connecting to 10.0.0.12 [10.0.0.12] port 22.

创建临时目录、传输、授权、执行与清理的原记录:

mkdir /root/.ansible/tmp/ansible-tmp-1733403659.621861-14045-101342830451514
Using module file /usr/lib/python3/dist-packages/ansible/modules/command.py
PUT /root/.ansible/tmp/ansible-local-14041b4k_0i9f/tmpf9amqhvy TO /root/.ansible/tmp/ansible-tmp-1733403659.621861-14045-101342830451514/AnsiballZ_command.py
SSH: EXEC sftp -b - ... '[10.0.0.12]'
chmod u+x /root/.ansible/tmp/ansible-tmp-1733403659.621861-14045-101342830451514/ /root/.ansible/tmp/ansible-tmp-1733403659.621861-14045-101342830451514/AnsiballZ_command.py
/usr/bin/python3 /root/.ansible/tmp/ansible-tmp-1733403659.621861-14045-101342830451514/AnsiballZ_command.py
rm -f -r /root/.ansible/tmp/ansible-tmp-1733403659.621861-14045-101342830451514/ > /dev/null 2>&1
Received exit status from master 0

模块结果:

{
  "changed": true,
  "stdout": "anaconda-ks.cfg",
  "stderr": "",
  "rc": 0,
  "cmd": ["ls", "/root"],
  "start": "2024-12-05 21:01:00.121814",
  "end": "2024-12-05 21:01:00.126763",
  "delta": "0:00:00.004949",
  "msg": "",
  "invocation": {
    "module_args": {
      "_raw_params": "ls /root",
      "_uses_shell": false,
      "expand_argument_vars": true,
      "stdin_add_newline": true,
      "strip_empty_ends": true,
      "argv": null,
      "chdir": null,
      "executable": null,
      "creates": null,
      "removes": null,
      "stdin": null
    }
  }
}
10.0.0.12 | CHANGED | rc=0 >>
anaconda-ks.cfg

输出中的 CHANGED 是 command 的默认变更报告,不代表 ls 真修改文件。调试日志里的 Broken pipe 若伴随复用会话正常结束及 rc=0,也不能仅按该字面判断任务失败。

3. 常用模块实践

本节优先使用 FQCN。ansible.posix、community.general 等 Collection 不是裸 ansible-core 的全部内置模块,需要准备与版本匹配的集合及目标依赖。

3.1 script:运行控制端脚本

模块名是 script,不是 scripts。它把控制端脚本传到目标后运行,不是执行远端已有脚本的通用入口。

script 模块参数帮助

参数 作用
chdir 执行前切换到目标目录。
creates 指定目标存在时跳过。
removes 指定目标不存在时跳过。
executable 在目标端使用的脚本解释器,不是必须提供的所有脚本参数。
decrypt 是否自动解密受 Vault 保护的源脚本。

调用通式:

ansible <目标> -m ansible.builtin.script -a '/控制端/脚本 参数 executable=/目标端/解释器'

远端已有脚本:使用 command/shell

使用 printf 创建,避免 /bin/sh 的 echo -e 差异:

ansible 10.0.0.16 -m ansible.builtin.shell -a 'printf "#!/bin/bash\necho \"\$1\"\n" > /tmp/test.sh'
ansible 10.0.0.16 -m ansible.builtin.command -a '/bin/bash /tmp/test.sh 0.15'

修正后预期输出 0.15。解释器读取脚本时不要求脚本本身有可执行位,但需要可读权限。

控制端脚本:使用 script

mkdir -p /data/ansible/scripts
cat > /data/ansible/scripts/test.sh <<'SCRIPT'
#!/bin/bash
printf '%s\n' "$1"
SCRIPT
ansible 10.0.0.16 -m ansible.builtin.script -a '/data/ansible/scripts/test.sh 0.15 executable=/bin/bash'

script 并不必然幂等,可借助 creates/removes 限制执行条件。

原课件误用及原始报错,保留用于理解根因:

ansible 10.0.0.16 -m script -a "/tmp/test.sh 0.15"
Could not find or access '/tmp/test.sh' on the Ansible Controller.
If you are using a module and expect the file to exist on the remote, see the remote_src option

这是控制端找不到脚本,不是远端脚本权限错误;通用错误提示也不意味着 script 支持 remote_src。

另一条原命令把控制端 /bin/bash 作为待传送的“脚本”:

ansible 10.0.0.16 -m script -a "/bin/bash /tmp/test.sh 0.15"
stdout: /tmp/test.sh: 行 1: -e: 未找到命令
        0.15

若控制端 Bash 二进制被复制到不同发行版目标,可能报:

.../bash: /lib64/libc.so.6: version `GLIBC_2.38' not found

修正方法是按前面正确用法使用目标自带解释器,不是为了这次误用升级系统 glibc。rc=0 或 CHANGED 也不能证明脚本内部没有错误,必须结合输出和脚本错误处理判断。

3.2 SELinux

ansible.posix.selinux 管理配置和支持的运行状态,需要对应系统、SELinux 策略及兼容的 Python 绑定。

ansible-doc -s ansible.posix.selinux
ansible 10.0.0.12 -m ansible.posix.selinux -a 'state=enforcing policy=targeted'
ansible 10.0.0.12 -a 'getenforce'

SELinux 模块参数

参数 含义
configfile 默认 /etc/selinux/config。
policy 策略,如 targeted;非 disabled 时需要。
state enforcing、permissive 或 disabled。

原输出:

{
  "changed": false,
  "configfile": "/etc/selinux/config",
  "msg": "",
  "policy": "targeted",
  "reboot_required": false,
  "state": "enforcing"
}
Enforcing

禁用状态的历史演示:

ansible 10.0.0.12 -m ansible.posix.selinux -a 'state=disabled policy=targeted'
ansible 10.0.0.12 -a 'getenforce'
[WARNING]: SELinux state temporarily changed from 'enforcing' to 'permissive'. State change will take effect next reboot.
{
  "changed": true,
  "configfile": "/etc/selinux/config",
  "msg": "Config SELinux state changed from 'enforcing' to 'disabled'",
  "policy": "targeted",
  "reboot_required": true,
  "state": "disabled"
}

此时 getenforce 仍为 Permissive,配置目标和运行状态不同。是否能切换及是否需重启,还取决于发行版、启动参数与当前 SELinux 状态,模块不会代为重启。

3.3 sysctl

ansible-doc -s ansible.posix.sysctl

sysctl 模块参数

参数 作用
name 内核参数名。
value,别名 val 参数值。
reload 文件变化时是否调用 sysctl 加载,默认 true。
state present 写入,absent 删除配置项。
sysctl_file 指定配置文件,默认 /etc/sysctl.conf。
sysctl_set 检查并设置当前内核值,默认 false。
ansible 10.0.0.16 -m ansible.posix.sysctl -a 'name=net.ipv4.ip_forward value=1'
ansible 10.0.0.16 -m ansible.builtin.command -a 'sysctl net.ipv4.ip_forward'
ansible 10.0.0.16 -a 'grep ip_forward /etc/sysctl.conf'
10.0.0.16 | CHANGED => {"changed": true}
net.ipv4.ip_forward = 1
#net.ipv4.ip_forward=1
net.ipv4.ip_forward=1

删除文件配置:

ansible 10.0.0.16 -m ansible.posix.sysctl -a 'name=net.ipv4.ip_forward state=absent'
ansible 10.0.0.16 -a 'grep ip_forward /etc/sysctl.conf'
ansible 10.0.0.16 -a 'sysctl net.ipv4.ip_forward'
#net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1

删除条目不会主动将内核值恢复为原值;加载不含该项的文件也不会知道原值。需要回退运行值时应显式设置已记录的原值。原文用默认 command 执行 sysctl -a | grep ...,管道不会被 Shell 解释,已改为直接查询。

3.4 fetch:从目标取回文件

fetch 模块参数

src 是目标上的单个文件,不支持直接用通配符递归取目录;dest 是控制端目的路径。默认生成 dest/主机名/原绝对路径;flat=true 可以去掉主机和源路径层次,但多主机同名文件可能覆盖。

单文件

不清空整个 /tmp,使用专用目的目录:

mkdir -p /tmp/ansible-fetch-lab
ansible 10.0.0.16 -m ansible.builtin.fetch -a 'src=/tmp/tpircs.sh dest=/tmp/ansible-fetch-lab/'
apt install tree -y
tree /tmp/ansible-fetch-lab/

tpircs.sh 沿用课件文件名,需先在目标准备;它与前面的 test.sh 是不同文件。

原始 checksum 记录不受目的目录调整影响:

checksum: 8dcb191fe46af8d3b2f4fa1fcbfb6fc3e297984d
md5sum: 4a215afda9311e91b32b0f99cdced81a
remote_checksum: 8dcb191fe46af8d3b2f4fa1fcbfb6fc3e297984d
remote_md5sum: null

修正后目录预期为:

/tmp/ansible-fetch-lab/
└── 10.0.0.16
    └── tmp
        └── tpircs.sh
3 directories, 1 file

多文件

原文失败示例:

ansible 10.0.0.16 -m ansible.builtin.fetch -a 'src=/var/log/*.log dest=/tmp/ansible-fetch-lab/'
FAILED! => {"changed": false, "msg": "the remote file does not exist, not transferring, ignored"}

先在目标归档,再抓取归档文件:

ansible 10.0.0.16 -m ansible.builtin.shell -a 'tar zcf /tmp/log.tar.gz /var/log/*.log'
ansible 10.0.0.16 -m ansible.builtin.fetch -a 'src=/tmp/log.tar.gz dest=/tmp/ansible-fetch-lab/'
tar tvf /tmp/ansible-fetch-lab/10.0.0.16/tmp/log.tar.gz

原 tar 提示及归档摘要:

tar: 从成员名中删除开头的“/”
tar: 从硬连接目标中删除开头的“/”
checksum: cc3ccfeece585acbe2405451be238b96e4a93f1e
md5sum: 1689ab279807ff1916f885058641a050

源中没有匹配日志时应先处理空列表,不要把失败归档当成完整备份。原文的压缩错误:

tar jcf /tmp/log.tar.xz /var/log/*.log
/bin/sh: 1: bzip2: not found
tar: /tmp/log.tar.xz: Wrote only 4096 of 10240 bytes
tar: Child returned status 127
tar: Error is not recoverable: exiting now

j 对应 bzip2,应配 .tar.bz2;xz 使用 J 和 .tar.xz。缺少压缩工具时要安装相应工具或选择可用格式,失败留下的文件不能作为有效归档。

3.5 stat

stat 模块参数

ansible 10.0.0.16 -m ansible.builtin.stat -a 'path=/tmp/script.sh'
ansible 10.0.0.16 -m ansible.builtin.stat -a 'path=/tmp/tpircs.sh'

前者原实验不存在:

{"changed": false, "stat": {"exists": false}}

后者存在时会返回路径、类型、权限、UID/GID、大小、时间、摘要等。原文只展示部分字段:

changed: false
stat:
  atime: 1547223162.4770179
  attr_flags: e
  attributes: [extents]
  block_size: 4096
  blocks: 8
  xusr: false

属性是否返回受平台、参数、目标权限和文件类型影响,使用变量前应检查字段存在。

3.6 get_url

get_url 模块参数

在受管节点从 HTTP、HTTPS 或 FTP 下载文件。dest 使用目标路径;force=false、现有文件和 checksum 的组合影响是否下载,不能说只要同名文件存在就一定跳过。checksum 用来验证下载内容的完整性,不是“正版洁癖”;摘要本身还需要可信来源。

ansible 10.0.0.16 -m ansible.builtin.get_url -a 'url=https://nginx.org/download/nginx-1.18.0.tar.gz dest=/usr/local/src/nginx.tar.gz checksum=md5:b2d33d24d89b8b1f87ff5d251aa27eb8'
ansible 10.0.0.16 -a 'ls -l /usr/local/src/nginx.tar.gz'

这里保留旧版本与 MD5 以复现课件;新交付选择受支持版本并优先用可信 SHA-256 等摘要。目标目录应已存在且可写,HTTPS 证书也须能验证。

原始 HTTP 下载输出:

changed: true
checksum_dest: null
checksum_src: 47b2c5ccd12e2a7088b03d629ff6b9ab18215180
dest: /usr/local/src/nginx.tar.gz
elapsed: 4
gid: 0
group: root
md5sum: b2d33d24d89b8b1f87ff5d251aa27eb8
mode: '0644'
msg: OK (1039530 bytes)
owner: root
size: 1039530
src: /root/.ansible/tmp/ansible-tmp-1733557270.8152976-4726-267946795702593/tmpeuxnhmdr
state: file
status_code: 200
uid: 0
url: http://nginx.org/download/nginx-1.18.0.tar.gz
-rw-r--r-- 1 root root 1039530 1月 12 00:23 /usr/local/src/nginx.tar.gz

不同版本内容与同一旧摘要不匹配的实验,使用独立目标,避免覆盖前一份有效文件:

ansible 10.0.0.16 -m ansible.builtin.get_url -a 'url=https://nginx.org/download/nginx-1.21.5.tar.gz dest=/usr/local/src/nginx-mismatch.tar.gz checksum=md5:b2d33d24d89b8b1f87ff5d251aa27eb8'

原失败输出:

changed: true
checksum_dest: null
checksum_src: c63c01da947ac925ac682a43bf097762a2cc9287
dest: /usr/local/src/nginx.tar.gz
elapsed: 7
msg: The checksum for /usr/local/src/nginx.tar.gz did not match b2d33d24d89b8b1f87ff5d251aa27eb8; it was 55783f98814279d5036cb0feb4a062b7.
src: /root/.ansible/tmp/ansible-tmp-1733557398.8789566-4763-119162282945995/tmpnmqe3l1u
url: http://nginx.org/download/nginx-1.21.5.tar.gz

3.7 archive

community.general.archive 在受管端打包,文件仍保存在受管端;与 fetch 取回控制端不同。

archive 模块帮助

ansible 10.0.0.16 -m ansible.builtin.shell -a 'mkdir -p /tmp/logs; cp /var/log/*.log /tmp/logs/'
ansible 10.0.0.16 -m ansible.builtin.command -a 'ls /tmp/logs/'

原始日志列表:

alternatives.log
apport.log
auth.log
boot.log
bootstrap.log
cloud-init.log
cloud-init-output.log
dpkg.log
fontconfig.log
gpu-manager.log
kern.log

目标需存在 sswang 用户、源日志和支持该格式的依赖。准备目的目录:

ansible 10.0.0.16 -m ansible.builtin.file -a 'path=/data state=directory'
ansible 10.0.0.16 -m community.general.archive -a 'path=/tmp/logs/ dest=/data/log.tar.gz format=gz owner=sswang mode=0600'
ansible 10.0.0.16 -a 'ls -l /data/'

原输出:

archived:
  /tmp/logs/bootstrap.log
  /tmp/logs/apport.log
  /tmp/logs/kern.log
  /tmp/logs/alternatives.log
  /tmp/logs/auth.log
  /tmp/logs/dpkg.log
  /tmp/logs/fontconfig.log
  /tmp/logs/cloud-init.log
  /tmp/logs/gpu-manager.log
  /tmp/logs/boot.log
  /tmp/logs/cloud-init-output.log
arcroot: /tmp/logs/
changed: true
dest: /data/log.tar.gz
dest_state: archive
expanded_exclude_paths: []
expanded_paths: [/tmp/logs/]
gid: 0
group: root
missing: []
mode: '0600'
owner: sswang
size: 60366
state: file
uid: 1000
总计 120
-rw------- 1 sswang root 60366 1月 12 00:28 log.tar.gz
drwxr-xr-x 3 root root 4096 12月 5 2024 softs

3.8 lineinfile

单行维护优先使用 ansible.builtin.lineinfile。regexp 匹配多行时,present 通常只替换最后一条匹配行;absent 会删除所有匹配行。需要替换多个匹配文本可使用 replace。

lineinfile 模块参数

准备独立测试文件:

ansible 10.0.0.16 -m ansible.builtin.copy -a 'content="server {\n    listen 8080;\n}\n" dest=/tmp/nginx_test.conf'
ansible 10.0.0.16 -a 'cat /tmp/nginx_test.conf'
server {
    listen 8080;
}

原 copy 输出摘要:changed=true、size=25、mode=0644,原始内容校验和为 f697aa7742b164c46a09dd8681740ab590472e9e、MD5 为 8d13f2a0aa1a28193041b653c59f078a。本稿规范缩进及结尾换行后,字节数和摘要会变化。

替换 listen 行并保留语法分号:

ansible 10.0.0.16 -m ansible.builtin.lineinfile -a 'path=/tmp/nginx_test.conf regexp="^\s*listen\s+" line="    listen 80;"'
CHANGED => {"backup": "", "changed": true, "msg": "line replaced"}

原文替换成 listen 80 缺少分号,已修正。

在 fstab 副本中删除注释,不改系统实际挂载配置:

ansible 10.0.0.16 -a 'cat /etc/fstab'
ansible 10.0.0.16 -m ansible.builtin.copy -a 'src=/etc/fstab dest=/tmp/fstab remote_src=true'
ansible 10.0.0.16 -m ansible.builtin.lineinfile -a 'path=/tmp/fstab state=absent regexp="^#"'
ansible 10.0.0.16 -a 'cat /tmp/fstab'

copy 默认源在控制端;这里要复制目标自己的 fstab,因此加 remote_src=true。原例双层双引号也已整理。

原文件含注释头、根分区和 Swap。删除后原实验输出:

CHANGED => {"backup": "", "changed": true, "found": 8, "msg": "8 line(s) removed"}
/dev/disk/by-uuid/5a39c07c-ed04-49e3-aca8-9a99684905e8 / ext4 defaults 0 1
/swap.img none swap sw 0 0

3.9 replace

ansible.builtin.replace 对匹配正则的内容进行替换,可以修改多处;它使用 Python 正则,跨行行为取决于表达式和标志,并非自动按每一行都执行 sed。

replace 模块帮助

在上一节副本中追加第二条含 defaults 的根分区记录,仅为替换演示:

ansible 10.0.0.16 -m ansible.builtin.shell -a 'grep default /etc/fstab >> /tmp/fstab; cat /tmp/fstab'
/dev/disk/by-uuid/5a39c07c-ed04-49e3-aca8-9a99684905e8 / ext4 defaults 0 1
/swap.img none swap sw 0 0
/dev/disk/by-uuid/5a39c07c-ed04-49e3-aca8-9a99684905e8 / ext4 defaults 0 1

原实验修改并还原 defaults 字段的命令:

ansible 10.0.0.16 -m ansible.builtin.replace -a "path=/tmp/fstab regexp='(defau.*)' replace='#\1'"
ansible 10.0.0.16 -a 'cat /tmp/fstab'
ansible 10.0.0.16 -m ansible.builtin.replace -a "path=/tmp/fstab regexp='#(defau.*)' replace='\1'"
ansible 10.0.0.16 -a 'cat /tmp/fstab'

两次均记录 changed=true、msg="2 replacements made"、rc=0。中间状态:

/dev/disk/by-uuid/5a39c07c-ed04-49e3-aca8-9a99684905e8 / ext4 #defaults 0 1
/swap.img none swap sw 0 0
/dev/disk/by-uuid/5a39c07c-ed04-49e3-aca8-9a99684905e8 / ext4 #defaults 0 1

这是测试副本的文本替换,不等于正确注释了 fstab 条目,不可将中间内容替换系统 fstab。若目标是注释整条挂载记录,应匹配完整行并在行首加 #,同时限定目标条目,避免误改其他挂载。

3.10 apt_repository:添加与删除源

ansible.builtin.apt_repository 管理 Debian 系列 APT 源,通常写入 /etc/apt/sources.list.d/*.list,需要匹配的 Python APT 依赖。

apt_repository 参数

参数 作用
repo 完整源条目或受支持的 PPA 形式。
filename 目的 list 文件基本名,默认由条目生成,不是默认直接写入 sources.list。
state present 添加,absent 移除对应条目。
update_cache 源变化后是否更新缓存,默认 true。

CLI 实验

此处保留 Chrome 源配置演示,使用 HTTPS,但没有提供签名密钥准备过程,不能把添加条目成功当作安装软件已就绪:

ansible 10.0.0.16 -m ansible.builtin.apt_repository -a 'repo="deb https://dl.google.com/linux/chrome/deb/ stable main" filename=chrome update_cache=false'
ansible 10.0.0.16 -a 'ls /etc/apt/sources.list.d'

原课件使用 HTTP,创建后输出:

changed: true
repo: deb http://dl.google.com/linux/chrome/deb/ stable main
sources_added: [/etc/apt/sources.list.d/chrome.list]
sources_removed: []
state: present

chrome.list
ubuntu.sources
ubuntu.sources.curtin.orig

删除时匹配本稿实际添加的 HTTPS 条目:

ansible 10.0.0.16 -m ansible.builtin.apt_repository -a 'repo="deb https://dl.google.com/linux/chrome/deb/ stable main" filename=chrome state=absent update_cache=false'
ansible 10.0.0.16 -a 'ls /etc/apt/sources.list.d'

若机器上是原课件 HTTP 条目,应删除对应的 HTTP 条目,不是机械照用 HTTPS。删除后源文件列表应只剩原有 Ubuntu 文件;如 list 文件还有其他条目,模块不保证把整个文件删除。

Playbook 实验

以下只演示增加一条 noble main 等组件源,不是完整替代 Ubuntu 安全更新源。已有 ubuntu.sources 时可能重复,执行前应核对现有源布局。

01-apt_add_repo.yml:

- hosts: 10.0.0.16
  become: true
  tasks:
    - name: Add the teaching mirror entry
      ansible.builtin.apt_repository:
        repo: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ noble main restricted universe multiverse
        filename: mirrors_tuna_tsinghua_edu_cn_ubuntu
        state: present
ansible-playbook 01-apt_add_repo.yml
ansible 10.0.0.16 -a 'ls /etc/apt/sources.list.d'
ansible 10.0.0.16 -m ansible.builtin.apt -a 'update_cache=true'

添加源的原始 Playbook 执行界面

mirrors_tuna_tsinghua_edu_cn_ubuntu.list
ubuntu.sources
ubuntu.sources.curtin.orig
cache_update_time: 1733562046
cache_updated: true
changed: true

02-apt_del_repo.yml:

- hosts: 10.0.0.16
  become: true
  tasks:
    - name: Remove the teaching mirror entry
      ansible.builtin.apt_repository:
        repo: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ noble main restricted universe multiverse
        filename: mirrors_tuna_tsinghua_edu_cn_ubuntu
        state: absent
ansible-playbook 02-apt_del_repo.yml
ansible 10.0.0.16 -a 'ls /etc/apt/sources.list.d'
ansible 10.0.0.16 -m ansible.builtin.apt -a 'update_cache=true'

删除源的原始 Playbook 执行界面

ubuntu.sources
ubuntu.sources.curtin.orig
cache_update_time: 1733562244
cache_updated: true
changed: true

原 Playbook 的 tasks 和模块缩进已修正。缓存时间及 changed 为原记录,不保证每次执行相同。

3.11 APT 签名密钥

apt_key 用来管理旧式 APT 信任密钥,其底层 apt-key 已弃用,现代发行版不应默认继续用全局信任库。

apt_key 的历史帮助

原课件的旧式安装实验:

ansible-doc -s apt_key
ansible 10.0.0.16 -m apt_key -a 'url=https://download.ceph.com/keys/release.asc state=present'

原输出:

{
  "after": ["6ABFA6073131CE23", "E84AC2C0460F3994", "D94AA3F0EFE21092", "871920D1991BC93C"],
  "before": ["6ABFA6073131CE23", "D94AA3F0EFE21092", "871920D1991BC93C"],
  "changed": true,
  "fp": "E84AC2C0460F3994",
  "id": "E84AC2C0460F3994",
  "key_id": "E84AC2C0460F3994",
  "short_id": "460F3994"
}
ansible 10.0.0.16 -m shell -a 'apt-key list | grep ceph'
uid [ unknown] Ceph.com (release key) <security@ceph.com>
Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).

课件移除后 after 恢复到添加前的三个 ID,before 包含 Ceph 的 E84AC2C0460F3994,changed=true。原移除命令只提供 URL,不应作为当前可靠接口;旧模块移除需按版本要求明确指定正确密钥 ID,先核对完整指纹及其使用者。

现代方式是把核验过的第三方公钥放在独立 keyring 文件,再用源条目的 signed-by 或 deb822 Signed-By 限定信任范围。例如只准备公钥文件:

- hosts: 10.0.0.16
  become: true
  tasks:
    - name: Prepare separate APT keyring directory
      ansible.builtin.file:
        path: /etc/apt/keyrings
        state: directory
        mode: '0755'
    - name: Obtain the repository public key
      ansible.builtin.get_url:
        url: https://download.ceph.com/keys/release.asc
        dest: /etc/apt/keyrings/ceph.asc
        mode: '0644'

这只下载公钥,不增加 Ceph 软件源;源地址、发行版支持和密钥指纹还须按实际版本核对。ASCII armored 公钥使用 .asc,不要仅更名为 .gpg 假装完成 dearmor。

3.12 RPM 软件包管理:yum 与 dnf

原文以 yum 模块和 EL9 输出教学。现代 ansible-core 已移除依赖旧 Python 2 的 yum 后端;DNF 系统优先使用 ansible.builtin.dnf,DNF5 等环境按实际包管理器及插件支持选择。

参数 作用
name 包名、支持的版本约束、本地 RPM 路径或远程 RPM URL。
state present/installed 安装,absent/removed 删除,latest 升级到当前仓库最新。
list 查询包、installed、updates、available、repos 等,与修改 name 的任务区分使用。
download_only、download_dir 只下载及目的目录。
update_only 只更新已安装包。
autoremove 移除不再被需要的依赖,行为受 DNF 配置和安装原因影响。
enablerepo、disablerepo 为本次操作启用或禁用仓库。
update_cache 检查及更新过期缓存,依模块和后端支持;不能说完全没有缓存刷新参数。
validate_certs HTTPS 证书验证。
disable_gpg_check 是否关闭 RPM 签名验证,不能解决依赖不满足。

validate_certs 与 GPG 包签名检查是不同层次;关闭它们也不能让不兼容的 EL8 包变成 EL9/EL10 原生包。

ansible 10.0.0.12 -m ansible.builtin.dnf -a 'list=nginx'
ansible 10.0.0.12 -m ansible.builtin.dnf -a 'list=repos'
ansible 10.0.0.12 -m ansible.builtin.dnf -a 'name=nginx state=present'
ansible 10.0.0.12 -m ansible.builtin.shell -a 'dnf list --installed | grep nginx'

原包查询结果:

ansible_facts: {pkg_mgr: dnf}
changed: false
arch: x86_64
envra: 2:nginx-1.20.1-20.el9.0.1.x86_64
epoch: '2'
name: nginx
nevra: 2:nginx-1.20.1-20.el9.0.1.x86_64
release: 20.el9.0.1
repo: appstream
version: 1.20.1
yumstate: available

原启用仓库为 epel、epel-cisco-openh264、baseos、appstream、extras;查询本身 changed=false。

原安装结果:

Installed: rocky-logos-httpd-90.15-2.el9.noarch
Installed: nginx-core-2:1.20.1-20.el9.0.1.x86_64
Installed: nginx-filesystem-2:1.20.1-20.el9.0.1.noarch
Installed: nginx-2:1.20.1-20.el9.0.1.x86_64

nginx.x86_64             2:1.20.1-20.el9.0.1 @appstream
nginx-core.x86_64        2:1.20.1-20.el9.0.1 @appstream
nginx-filesystem.noarch  2:1.20.1-20.el9.0.1 @appstream

删除实验:

ansible 10.0.0.12 -m ansible.builtin.dnf -a 'name=nginx state=absent autoremove=true'
ansible 10.0.0.12 -m ansible.builtin.shell -a 'dnf list --installed | grep nginx'

原结果只移除了 nginx-2:1.20.1-20.el9.0.1.x86_64,nginx-core、nginx-filesystem 仍存在。autoremove 不保证清掉所有相关包及配置文件,RPM 配置也有 .rpmsave/.rpmnew 等语义,不能等同 APT purge。

原文跨大版本 RPM 并跳过验证的记录,仅作为旧实验,不是修正后的安装方案:

ansible 10.0.0.12 -m yum -a 'name=https://mirrors.tuna.tsinghua.edu.cn/zabbix/zabbix/6.0/rhel/8/x86_64/zabbix-agent-6.0.0-1.el8.x86_64.rpm state=present validate_certs=no disable_gpg_check=yes'
Installed: openldap-compat-2.6.6-3.el9.x86_64
Installed: compat-openssl11-1:1.1.1k-4.el9_0.x86_64
Installed: zabbix-agent-6.0.0-1.el8.x86_64
zabbix-agent.x86_64 6.0.0-1.el8 @@commandline

修正方案是配置与目标发行版和架构匹配的可信 Zabbix 仓库、导入其签名公钥,再安装适用版本,不使用 EL8 URL 强行适配 EL9/EL10:

ansible 10.0.0.12 -m ansible.builtin.dnf -a 'name=zabbix-agent state=present'
ansible 10.0.0.12 -m ansible.builtin.dnf -a 'name=zabbix-agent state=absent'

本地 RPM 安装中的路径位于受管节点,安装前需先传过去;缺失依赖要由匹配仓库解决。

3.13 yum_repository

ansible.builtin.yum_repository 管理受管节点的 .repo 文件,常用于 DNF/YUM 仓库。

参数 作用
name 仓库 ID。
description 仓库描述。
baseurl 仓库地址。
enabled 是否启用。
gpgcheck 是否检查包签名;未指定时按后端与全局配置解释,建议明确设置。
gpgkey 签名公钥位置。
state present / absent。
timeout 下载超时,默认值及是否支持须按后端版本核对。
ansible-doc -s ansible.builtin.yum_repository
ansible 10.0.0.12 -m ansible.builtin.yum_repository -a 'name=nginx description=nginx-desc baseurl="https://nginx.org/packages/centos/$releasever/$basearch/" gpgcheck=true enabled=true gpgkey=https://nginx.org/keys/nginx_signing.key'
ansible 10.0.0.12 -m ansible.builtin.shell -a 'ls /etc/yum.repos.d/ngi*'
ansible 10.0.0.12 -a 'dnf makecache'

整体单引号保留 $releasever、$basearch 给目标包管理器展开。先确认 Nginx 官方仓库支持实际发行版大版本,不能仅靠替换变量推断一定有包。ls 的通配符需要 shell 模块,不应交给默认 command。

CHANGED => {"changed": true, "repo": "nginx", "state": "present"}
/etc/yum.repos.d/nginx.repo
nginx-desc 9.8 kB/s | 2.9 kB
元数据缓存已建立。

其余原输出包括 EPEL、EPEL OpenH264、Rocky 9 BaseOS、AppStream、Extras 的元数据;这是原 EL9 环境记录。

ansible 10.0.0.12 -m ansible.builtin.yum_repository -a 'name=nginx state=absent'
ansible 10.0.0.12 -m ansible.builtin.shell -a 'ls /etc/yum.repos.d/ngi*'
ansible 10.0.0.12 -a 'dnf makecache'

删除后原实验:

CHANGED => {"changed": true, "repo": "nginx", "state": "absent"}
FAILED | rc=2 >>
ls: 无法访问 '/etc/yum.repos.d/ngi*': 没有那个文件或目录

若没有其他匹配文件,ls 的非零状态是删除后的预期观察,不表示仓库删除任务失败。后续 makecache 不再包含 nginx-desc。

3.14 setup 与 Facts

ansible.builtin.setup 收集受管主机的 Facts,可在 Playbook 后续任务引用。filter 使用通配匹配过滤返回键,不是任意正则;默认 Facts 也不是操作系统所有可能属性的全集。

setup 参数

参数 用途
fact_path 目标自定义 Facts 目录,常见 /etc/ansible/facts.d。
filter 对返回 Facts 的键做匹配过滤,通常不减少底层全部采集成本。
gather_subset 控制采集范围,按目标支持的子集选择。

Play 的 gather_facts: false 关闭隐式采集,不会自动提供未采集的值;仍可通过 setup 显式收集或使用已配置的缓存。

简单查询

ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_memtotal_mb'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=*ens*'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_all_ipv4_addresses'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_default_ipv4'

原内存结果为 3868 MB;网卡字段中 ansible_ens33.active=true、device=ens33。所有 IPv4 列表为 ["10.0.0.16"],默认路由接口结果:

{
  "ansible_facts": {
    "ansible_default_ipv4": {
      "address": "10.0.0.16",
      "alias": "ens33",
      "broadcast": "10.0.0.255",
      "gateway": "10.0.0.2",
      "interface": "ens33",
      "macaddress": "00:50:56:26:58:e4",
      "mtu": 1500,
      "netmask": "255.255.255.0",
      "network": "10.0.0.0",
      "prefix": "24",
      "type": "ether"
    }
  },
  "changed": false
}

CLI 获取 IP

原命令通过单行回调输出切分 JSON:

ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_all_ipv4_addresses' -o | awk -F '=>' '{print $2}' | jq -r '.ansible_facts.ansible_all_ipv4_addresses[0]'
10.0.0.16

-o 是单行显示,不是把整个命令输出变成有稳定 schema 的纯 JSON;本例依赖旧版本回调、单主机且执行成功。不能把它当作跨版本可靠 API,异常输出也必须另外处理。

按原多行展示格式抽取的示例:

ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_all_ipv4_addresses' | awk -F'"' '/ansible_all_ipv4_addresses/ {getline; print $2}'
alias ansible_getip="ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_all_ipv4_addresses' | awk -F'\"' '/ansible_all_ipv4_addresses/ {getline; print \$2}'"
ansible_getip

同样依赖显示格式。IP 列表第一个地址不保证是主地址;主路由地址应查询 default_ipv4.address,不存在默认路由时字段可能不存在。结构化自动化优先在 Playbook 内引用 Facts,或选择兼容的结构化结果输出方案。

其他常用查询:

ansible 10.0.0.16 -m ansible.builtin.setup
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_nodename'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_hostname'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_domain'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_memtotal_mb'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_memory_mb'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_memfree_mb'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_os_family'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_distribution_major_version'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_distribution_version'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_processor_vcpus'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_all_ipv4_addresses'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_architecture'
ansible 10.0.0.16 -m ansible.builtin.setup -a 'filter=ansible_processor*'

3.15 debug

ansible.builtin.debug 在控制端输出消息、变量和诊断,msg 与 var 互斥,verbosity 选择显示所需的详细等级。通式中的有效参数不是 name=主机名。

debug 模块帮助

ansible 10.0.0.16 -m ansible.builtin.debug
ansible 10.0.0.16 -m ansible.builtin.debug -a 'msg="hello debug"'
ansible 10.0.0.16 -m ansible.builtin.debug -a 'var=buzhidao'
ansible 10.0.0.16 -m ansible.builtin.debug -a 'msg="this is test msg" verbosity=2'
ansible 10.0.0.16 -m ansible.builtin.debug -a 'msg="this is test msg" verbosity=2' -vv

原结果依次为:

SUCCESS => {"msg": "Hello world!"}
SUCCESS => {"msg": "hello debug"}
SUCCESS => {"buzhidao": "VARIABLE IS NOT DEFINED!"}
SKIPPED
SUCCESS => {"msg": "this is test msg"}

最后一条还输出 core 2.18.6、/root/ansible.cfg、Python 3.12.3、Jinja 3.1.2 及回调选择信息。verbosity 不是系统运行级别,不会更改目标服务。

默认消息与定制消息

03-debug-default.yml:

- hosts: 10.0.0.16
  tasks:
    - name: Output the default message
      ansible.builtin.debug: {}
ansible-playbook 03-debug-default.yml

默认 Hello world 消息的原始输出

04-debug-msg.yml:

- hosts: 10.0.0.16
  gather_facts: true
  tasks:
    - name: Output host name and default route address
      ansible.builtin.debug:
        msg: 'Host "{{ ansible_facts["nodename"] }}" is "{{ ansible_facts.get("default_ipv4", {}).get("address", "无默认 IPv4 路由") }}"'
ansible-playbook 04-debug-msg.yml

主机名与地址消息的原始输出

var 与 msg 不能放在同一任务

原 05-debug-varmsg-error.yml 的错误关键内容:

debug: var=nginx_num msg="System {{ inventory_hostname }} has nginx service."

混用 var 与 msg 导致报错

修正后的 06-debug-varmsg-ok.yml 将二者分开,也避免把查询任务报告为变更:

- hosts: 10.0.0.16
  remote_user: root
  tasks:
    - name: Count Nginx processes
      ansible.builtin.command: pgrep -c -x nginx
      register: nginx_num
      changed_when: false
      failed_when: nginx_num.rc not in [0, 1]
    - name: Show the full query result
      ansible.builtin.debug:
        var: nginx_num
    - name: Show the process count
      ansible.builtin.debug:
        msg: 'System {{ inventory_hostname }} has {{ nginx_num.stdout }} Nginx processes.'

pgrep 需安装,返回 1 表示没有匹配,其他错误仍失败;进程数不等于服务健康。原文用 ps -ef | grep nginx | grep -v grep | wc -l,截图计数为 0:

拆分 var 与 msg 后的原始输出

ansible-playbook 06-debug-varmsg-ok.yml

Facts 必须先收集

原 get_fact.yaml 关闭采集,还重复定义两个 tasks 键,前一个可能被覆盖,且引用未定义 Facts。原截图显示 VARIABLE IS NOT DEFINED:

原 Facts 示例变量未定义

修正为单一 tasks 列表并启用采集:

- name: Display Facts Variables
  hosts: 10.0.0.13
  gather_facts: true
  tasks:
    - name: Display OS family
      ansible.builtin.debug:
        var: ansible_facts['os_family']
    - name: Display default IPv4 address
      ansible.builtin.debug:
        msg: '{{ ansible_facts.get("default_ipv4", {}).get("address", "无默认 IPv4 路由") }}'
    - name: Display total memory in MB
      ansible.builtin.debug:
        var: ansible_facts['memtotal_mb']
ansible-playbook get_fact.yaml

汇总主机信息

get_msg_fact.yaml:

- name: 收集目标主机系统信息并提取 IPv4 地址
  hosts: all
  gather_facts: true
  tasks:
    - name: 显示主机基本信息
      ansible.builtin.debug:
        msg:
          - '主机名: {{ ansible_facts["hostname"] }}'
          - '操作系统: {{ ansible_facts["distribution"] }} {{ ansible_facts["distribution_version"] }}'
          - '内核版本: {{ ansible_facts["kernel"] }}'
    - name: 提取并显示所有 IPv4 地址
      ansible.builtin.debug:
        var: ansible_facts['all_ipv4_addresses']
      when: ansible_facts['all_ipv4_addresses'] is defined
    - name: 显示列表第一个 IPv4 地址
      ansible.builtin.debug:
        msg: '列表第一个 IPv4: {{ ansible_facts["all_ipv4_addresses"][0] }}'
      when:
        - ansible_facts['all_ipv4_addresses'] is defined
        - ansible_facts['all_ipv4_addresses'] | length > 0
ansible-playbook get_msg_fact.yaml

原课件这次清单另外包含 .14,不属于前面三节点清单。其原结果按主机整理:

主机 主机名 系统 内核
10.0.0.14 openeuler-14 openEuler 24.03 6.6.0-28.0.0.34.oe2403.x86_64
10.0.0.12 rocky-12 Rocky 9.4 5.14.0-427.13.1.el9_4.x86_64
10.0.0.13 ubuntu24-13 Ubuntu 24.04 6.8.0-31-generic
10.0.0.16 ubuntu24-16 Ubuntu 24.04 6.8.0-31-generic

每台 ansible_all_ipv4_addresses 分别为只含本机 IP 的单元素列表,因此原示例“主 IPv4”的显示恰好也是该 IP。这里已把名称改为列表第一个地址,不误称主地址。

PLAY [收集目标主机系统信息并提取IPv4地址]
TASK [Gathering Facts]              ok: [.16, .13, .14, .12]
TASK [显示主机基本信息]              每台显示上表的主机名、系统及内核
TASK [提取并显示所有IPv4地址]         分别显示本机 IP 列表
TASK [显示主IPv4地址(第一个地址)]    分别显示 10.0.0.14、.12、.13、.16
PLAY RECAP
10.0.0.12 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
10.0.0.13 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
10.0.0.14 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
10.0.0.16 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

4. Ansible 配套命令

4.1 ansible-console

交互式控制台可以在当前目标组运行模块,支持补全。其提示符通常显示:

执行用户@目标组 (主机数)[f:并发数]$ 模块 参数
ansible-console

进入 Ansible Console

控制台内常用命令:

help
?
list
cd web
forks 10
cd storage
ping
apt name=nginx state=present
service name=nginx state=started
exit

cd 在这里选择目标组,不是普通 Shell 切换目录。apt 的目标组必须都是支持 APT 的系统,不能照搬给混合发行版的 storage 组。命令会实际影响目标,并非仅本机练习。

Console 内建命令与模块列表

ping、列主机、切组和设置 forks

原 Console 安装软件记录

原 Console 启动服务记录

4.2 ansible-galaxy

Galaxy 提供共享的 Role 和 Collection。传统 Role 常命名为 namespace.role_name;Collection 是 namespace.collection_name,包含模块、插件和角色等,不是站点全部内容都只按 username.role 命名。

Galaxy 子命令帮助

Collection 的 download、init、build、publish、install 等动作

Role 的 init、remove、delete、list、search、import、setup、info、install 等动作

旧版本的登录、导入、删除远端内容等能力随 Galaxy 服务和版本变化,不能保证老命令和旧 API 现在仍可用。日常下载、管理本地 Role 建议显式写 role 子命令。

列表、安装路径和固定版本

ansible-galaxy role list
ansible-galaxy role list username.role_name
ansible-galaxy role install username.role_name
ansible-galaxy role install --roles-path . username.role_name
ansible-galaxy role install username.role_name,v1.0.0

--roles-path 不是原文拼错的 --role-pash。实际搜索和安装目录受配置及参数影响,常见有 ~/.ansible/roles、/usr/share/ansible/roles、/etc/ansible/roles,并不保证总装在 /etc/ansible/roles。

Git 源 Role 安装通式:

ansible-galaxy role install git+https://github.com/geerlingguy/ansible-role-apache.git,<实际分支、标签或提交>

requirements.yml 示例:

- src: username.role_name
- src: https://github.com/galaxy/nginx
- src: https://github.com/galaxy/nginx
  version: master
  name: nginx_role
- src: https://some.webserver.example.com/files/master.tar.gz
  name: http-role

以上来源是格式示例,不保证这些旧仓库仍存在;指定实际可访问、受信任的版本后安装:

ansible-galaxy role install -r requirements.yml

推荐用 requirements 的 name 选择本地名称。也可重命名目录,但后续角色引用、依赖和升级配置要同步,不能一概认为仅改目录就完全不受影响。

初始化 Role

mkdir -p /data/ansible/galaxy
cd /data/ansible/galaxy
ansible-galaxy role init roles_name
tree roles_name/
- Role roles_name was created successfully
roles_name/
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml
9 directories, 8 files
ansible-galaxy role list
ansible-galaxy role list --roles-path .

原记录默认全局路径未发现已安装角色,另有 /root/.ansible/roles、/usr/share/ansible/roles 不存在的 WARNING;使用当前目录后显示:

# /data/ansible/galaxy
- roles_name, (unknown version)
# /etc/ansible/roles

初始化只是生成结构,并没有自动上传到 Galaxy。发布、导入或远端删除要单独按服务端流程执行。

Redis Role 实验

ansible-galaxy role list
ansible-galaxy role search redis
ansible-galaxy role info aaronpederson.redis
ansible-galaxy role install davidwittman.redis

原搜索结果数量及项名:

Found 233 roles matching your search:
Name                 Description
aaronpederson.redis  Redis is an open source, BSD ...

Galaxy 安装多个角色时的下载错误

原 Redis Role 搜索结果

aaronpederson.redis 的信息

原安装成功记录:

Starting galaxy role install process
- downloading role 'redis', owned by davidwittman
- downloading role from https://github.com/DavidWittman/ansible-redis/archive/1.2.9.tar.gz
- extracting davidwittman.redis to /root/.ansible/roles/davidwittman.redis
- davidwittman.redis (1.2.9) was installed successfully

另一次 Redis Role 下载超时的原记录

出现超时应核对连通性、代理和源状态,并采用可验证的下载方案,不能把“失败是正常的,多试总会成功”作为结论。安装 Role 只是下载内容,不会自动安装 Redis 服务;运行前需要审阅角色及依赖。

ansible-galaxy role list
ls ~/.ansible/roles/
tree ~/.ansible/roles/davidwittman.redis/ -L 1
du -sh ~/.ansible/roles/davidwittman.redis
# /root/.ansible/roles
- davidwittman.redis, 1.2.9
/root/.ansible/roles/davidwittman.redis/
├── defaults
├── Gemfile
├── Gemfile.lock
├── handlers
├── LICENSE
├── meta
├── README.md
├── tasks
├── templates
├── test
└── vars
8 directories, 4 files
328K /root/.ansible/roles/davidwittman.redis

原改名及移除实验,确认所在目录再执行:

cd ~/.ansible/roles/
mv davidwittman.redis david.redis
ansible-galaxy role list
ansible-galaxy role remove david.redis
ansible-galaxy role list

改名后列表为 david.redis, 1.2.9;移除返回 successfully removed david.redis,本地列表重新为空。role remove 删除的是本地安装目录,不等于删除远端 Galaxy 项目。

4.3 ansible-pull

常规控制端集中发起任务属于 push;ansible-pull 在节点上获取 Git 仓库并运行 Playbook,常结合定时任务实现分散拉取。节点需要 Ansible、Git、仓库访问及任务所需依赖;它不是无需安装任何东西的远端执行魔法。

ansible-pull 参数帮助

mkdir -p /data/ansible/pull
cd /data/ansible/pull
apt install git
ansible-pull -o -C master -d /tmp/test_pull -i /tmp/test_pull/hosts -U https://gitee.com/wshs1117/kubernetes.git
参数 含义
-U Git 仓库 URL。
-C 签出的分支、标签或提交。
-d 本地检出目录。
-i 清单;本例假设仓库中实际有 hosts 文件。
-o 仓库更新时才运行,不是每次都执行。

未指定 Playbook 文件时按工具规则寻找主机名对应文件或 local.yml 等;仓库若没有正确文件,拉取成功也不代表部署完成。私有 HTTPS 仓库可以用受支持凭据,SSH URL 则要准备 SSH 身份和主机信任,不是私有仓库只能使用密钥。

原记录:

Starting Ansible Pull at 2024-12-07 22:19:58
[WARNING]: Could not match supplied host pattern, ignoring: ubuntu24-13.ansible.com
[WARNING]: Could not match supplied host pattern, ignoring: ubuntu24-13
localhost | SUCCESS => {
  "after": "108c284a330a82ce1a1f80238e4f54bf5e8b045a",
  "before": "108c284a330a82ce1a1f80238e4f54bf5e8b045a",
  "changed": false,
  "remote_url_changed": false
}
Repository has not changed, quitting.

本次日志表示仓库未更新,因此退出,没有运行配置任务;模式未匹配的警告仍需检查。定时触发应防止本机作业重叠,并为仓库凭据及可执行内容设置合适边界。

4.4 ansible-inventory

查看清单解析结果,--host、--graph、--list 互斥,--export 的导出视图与某些正常处理结果可能不同。

ansible-inventory 帮助

ansible-inventory --export --list
ansible-inventory --export --graph
ansible-inventory --host=10.0.0.16

原列表的结构部分:

_meta:
  hostvars: {}
all:
  children: [ungrouped, web, mysql, storage]
@all:
    |--@ungrouped:
    |--@web:
    |   |--10.0.0.12
    |   |--10.0.0.13
    |--@mysql:
    |   |--10.0.0.13
    |   |--10.0.0.16
    |--@storage:
    |   |--10.0.0.12
    |   |--10.0.0.16

原来的 --host=.16 返回 {},只表示没有可显示的主机变量,不证明该主机不在任何组。应结合 graph 和 inventory_hostname 等上下文判断。

原文加入以下明文变量后演示其展示效果,本稿仅作格式记录,不建议保存密码:

10.0.0.16 ansible_ssh_user=root ansible_ssh_pass=123456 custom_var1=value1 custom_var2=value2

原 --host 返回:

{
  "ansible_ssh_pass": 123456,
  "ansible_ssh_user": "root",
  "custom_var1": "value1",
  "custom_var2": "value2"
}

INI 值的类型解析可能导致纯数字密码变成数值,不能据此认为密码参数应该定义为整数。实际凭据优先使用 SSH 密钥、交互输入或 Vault 等合适方案。

ansible-inventory --vars --graph
ansible-inventory -y --list

变量图中 .16 在 mysql 与 storage 下都显示相同变量;它们是同一主机的视图,不是两台不同主机。原 YAML 输出:

all:
  children:
    mysql:
      hosts:
        10.0.0.13: {}
        10.0.0.16:
          ansible_ssh_pass: 123456
          ansible_ssh_user: root
          custom_var1: value1
          custom_var2: value2
    storage:
      hosts:
        10.0.0.12: {}
        10.0.0.16: {}
    web:
      hosts:
        10.0.0.12: {}
        10.0.0.13: {}

该历史输出不包含本稿清单新增的专用密钥变量;查询完整变量可能包含凭据,展示和存储时应按实际用途处理。

5. Playbook 错误处理与补偿

5.1 block / rescue / always 的实际边界

普通任务失败后,默认停止该主机后续正常任务,其他主机是否继续还受策略、serial、any_errors_fatal 等配置影响。已经完成的任务不会自动撤销。

结构 作用
block 组织一组任务,并可继承部分条件、标签和提权设置;不是独立功能模块。
rescue 在块中的任务产生可处理失败时执行补偿任务,补偿内容由作者显式编写。
always 在正常及可处理失败路径执行收尾任务;不是任何进程终止、语法错误或不可达场景下都保证运行。

这类似 try/catch/finally,但不提供数据库式原子性,也不自动恢复系统快照。无效任务定义、主机不可达等错误不会按普通任务失败进入 rescue;ignore_errors、failed_when 等也会改变失败判断。

成功 rescue 后,Ansible 可以继续后续任务,原失败仍会计入 rescued,命令总体可能成功。若补偿完成后仍要让发布失败,应显式执行 fail。不要把 rescued=1, failed=0 当作部署成功。

Handlers 通常延后执行,不应把其失败指望成此前部署 block 中的普通任务失败。需要在当前块捕获的关键配置检查和 reload,应明确写为块内普通任务;涉及 flush_handlers 的处理须结合版本和实际流程验证。

5.2 原 Nginx 案例的改造目标

原案例包含:创建 nginx-test 用户、站点目录与首页、安装 Nginx、复制主配置、启动服务、备份并更新子配置、重启,以及异常恢复和端口日志。

原 rescue 无条件删除用户和站点、重装 Nginx,再停启服务,不能恢复部署前的用户、内容、配置、版本和启用状态;重装也不保证覆盖被修改过的配置文件。标签过滤还可能让部分回退、always 根本不运行。

本稿将初次安装与子配置变更补偿分开:初次安装失败保留现场并明确失败;更新只备份和恢复本次管理的子配置,不清空已有业务目录。保持 installtask、changetask 标签,使原实验调用方式仍可理解。

5.3 修正后的完整示例

08-playbook-nginx-update.yml 适用于独立 Ubuntu/Debian 实验节点,先用 --limit 10.0.0.16,不能对包含 Rocky 的整组直接运行 APT 安装。控制端当前目录须有本课程配套 nginx.conf 与 nginx-define.conf;主配置需正确包含 /etc/nginx/conf.d/*.conf,并匹配站点及用户。

原 PDF 未在此重新列出主配置,可参照 Ansible 进阶教材中的完整 Role 基础项目 准备文件。本段不是隐藏依赖的一键部署脚本。

- name: Install the teaching service or update its managed subconfiguration
  hosts: web
  remote_user: root
  gather_facts: true
  vars:
    nginx_subconfig: /etc/nginx/conf.d/nginx-define.conf
  pre_tasks:
    - name: Limit this APT example to Debian-family hosts
      ansible.builtin.assert:
        that:
          - ansible_facts['os_family'] == 'Debian'
        fail_msg: 本示例仅用于 Debian/Ubuntu,混合清单必须限制目标。
      tags: always
  tasks:
    - name: Initial installation without destructive pseudo-rollback
      tags: installtask
      block:
        - name: Install Nginx
          ansible.builtin.apt:
            name: nginx
            state: present
            update_cache: true
        - name: Create the dedicated teaching user
          ansible.builtin.user:
            name: nginx-test
            system: true
            shell: /usr/sbin/nologin
        - name: Show the teaching user
          ansible.builtin.command: getent passwd nginx-test
          changed_when: false
        - name: Create the web root
          ansible.builtin.file:
            path: /data/webserver/html
            owner: nginx-test
            state: directory
            mode: '0755'
        - name: Write the teaching index
          ansible.builtin.copy:
            content: '<h1>welcome to ansible</h1>'
            dest: /data/webserver/html/index.html
            owner: nginx-test
            mode: '0644'
        - name: Copy the prepared main configuration with backup
          ansible.builtin.copy:
            src: nginx.conf
            dest: /etc/nginx/nginx.conf
            backup: true
            mode: '0644'
          register: nginx_main_copy
        - name: Validate the main configuration
          ansible.builtin.command: /usr/sbin/nginx -t
          changed_when: false
        - name: Start and enable the teaching service
          ansible.builtin.service:
            name: nginx
            state: started
            enabled: true
      rescue:
        - name: Preserve the failed installation for explicit recovery
          ansible.builtin.fail:
            msg: >-
              初次安装失败,保留现场;没有删除用户或站点,也没有声称原子回滚。
              主配置备份为 {{ (nginx_main_copy | default({})).get('backup_file', '未生成') }}。

    - name: Inspect the subconfiguration before this change
      ansible.builtin.stat:
        path: '{{ nginx_subconfig }}'
        follow: false
      register: nginx_before
      tags: [installtask, changetask]
    - name: Require an ordinary file or an absent path
      ansible.builtin.assert:
        that:
          - not nginx_before.stat.exists or nginx_before.stat.isreg | default(false)
        fail_msg: 本示例不覆盖目录或符号链接,请先确认配置管理范围。
      tags: [installtask, changetask]
    - name: Record the service state
      ansible.builtin.service_facts: {}
      tags: [installtask, changetask]
    - name: Require an installed service
      ansible.builtin.assert:
        that:
          - "'nginx.service' in ansible_facts.services"
        fail_msg: changetask 要求先完成基础安装。
      tags: [installtask, changetask]
    - name: Record whether the service was running
      ansible.builtin.set_fact:
        nginx_was_running: "{{ ansible_facts.services['nginx.service']['state'] == 'running' }}"
      tags: [installtask, changetask]
    - name: Allocate a unique backup for an existing managed file
      ansible.builtin.tempfile:
        state: file
        prefix: nginx-subconfig-before-
      register: nginx_backup
      when: nginx_before.stat.exists
      tags: [installtask, changetask]
    - name: Save the exact previous managed file
      ansible.builtin.copy:
        src: '{{ nginx_subconfig }}'
        dest: '{{ nginx_backup.path }}'
        remote_src: true
        mode: preserve
      when: nginx_before.stat.exists
      tags: [installtask, changetask]

    - name: Update only the managed subconfiguration
      tags: [installtask, changetask]
      block:
        - name: Publish the candidate subconfiguration
          ansible.builtin.copy:
            src: nginx-define.conf
            dest: '{{ nginx_subconfig }}'
            mode: '0644'
          register: nginx_candidate
        - name: Validate the complete configuration before reload
          ansible.builtin.command: /usr/sbin/nginx -t
          changed_when: false
        - name: Reload an originally running service when content changed
          ansible.builtin.service:
            name: nginx
            state: reloaded
          when:
            - nginx_was_running | bool
            - nginx_candidate.changed or (nginx_main_copy | default({})).get('changed', false)
      rescue:
        - name: Restore the previous managed configuration
          ansible.builtin.copy:
            src: '{{ nginx_backup.path }}'
            dest: '{{ nginx_subconfig }}'
            remote_src: true
            mode: '{{ nginx_before.stat.mode }}'
            owner: '{{ nginx_before.stat.uid }}'
            group: '{{ nginx_before.stat.gid }}'
          when: nginx_before.stat.exists
        - name: Remove only the newly introduced configuration
          ansible.builtin.file:
            path: '{{ nginx_subconfig }}'
            state: absent
          when: not nginx_before.stat.exists
        - name: Validate the restored configuration
          ansible.builtin.command: /usr/sbin/nginx -t
          changed_when: false
        - name: Recover a service that was originally running
          ansible.builtin.service:
            name: nginx
            state: restarted
          when: nginx_was_running | bool
        - name: Keep the deployment result failed after compensation
          ansible.builtin.fail:
            msg: 子配置更新失败,已完成上述补偿;部署未成功,备份保留供核对。
      always:
        - name: Collect the final listening state
          ansible.builtin.command: ss -tln
          register: nginx_listeners
          changed_when: false
          failed_when: false
        - name: Record diagnostic output and its return code
          ansible.builtin.copy:
            dest: /tmp/nginx-deploy-status.log
            mode: '0600'
            content: >-
              {{ {'rc': nginx_listeners.rc | default(-1),
                  'stdout': nginx_listeners.stdout | default(''),
                  'stderr': nginx_listeners.stderr | default(''),
                  'msg': nginx_listeners.msg | default('')} | to_nice_json }}
        - name: Show audit paths without claiming deployment success
          ansible.builtin.debug:
            msg:
              - 端口诊断见 /tmp/nginx-deploy-status.log,需结合任务结果和业务检查判断。
              - "原有子配置备份: {{ (nginx_backup | default({})).get('path', '原路径不存在,无旧文件') }}"

备份保留在目标 /tmp,仅用于本次补偿,不替代正式配置版本管理与异地备份。若恢复本身失败,后续任务不保证完成,必须检查失败日志;always 也不是跨主机全局回滚。

备份发生在覆盖前,若备份失败则停止,不继续替换。这里去掉原 Play 级的 host-tags,让隐式 Facts 采集正常保留 always 行为,避免只选 installtask/changetask 时未采集 os_family 就先做断言。重载条件同时检查本次主配置和子配置是否改变,避免已有服务仅更新主配置时没有应用变更。服务原本停止时只检查配置,不擅自启动;初次安装会按 installtask 启动。回退覆盖文件只恢复本示例记录的内容与基本权限,不承诺 ACL、SELinux 上下文或外部资源全部恢复。

5.4 正常与异常实验

先准备有效文件:

server {
    listen 10097;
    root /data/webserver/html;
    location / {
    }
}
ansible-playbook -l 10.0.0.16 --tags installtask 08-playbook-nginx-update.yml

原文故意破坏后的 nginx-define.conf:

server {
    listen 10097
    root /data/webserver/html;
    location / {
    }
}

缺少 listen 行分号。原课件直接 restart 才发现错误:

ansible-playbook -l 10.0.0.16 --tags changetask 08-playbook-nginx-update.yml

原始运行记录:

TASK [copy subconfig前备份原有配置]
changed: [10.0.0.16]
TASK [copy subconfig]
changed: [10.0.0.16]
TASK [restart nginx service]
[ERROR]: Task failed: Module failed: Unable to restart service nginx:
Job for nginx.service failed because the control process exited with error code.
See "systemctl status nginx.service" and "journalctl -xeu nginx.service" for details.
Origin: /root/nginx/08-playbook-nginx-update.yml:37:11
fatal: [10.0.0.16]: FAILED!
TASK [部署异常:回滚清理环境]
ok: [10.0.0.16] => {"msg": "===== Nginx子配置更新失败,执行自动回滚 ====="}
TASK [恢复变更前的子配置]
changed: [10.0.0.16]
TASK [删除备份文件]
changed: [10.0.0.16]
TASK [start nginx after rollback]
changed: [10.0.0.16]
PLAY RECAP
10.0.0.16 : ok=6 changed=5 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0

该 recap 属于原方案,不能套在修正后的 Playbook 上。修正后应在 reload 前由 nginx -t 失败,恢复旧配置或删除新增文件,再显式返回部署失败;原来正在运行的服务不会因为先执行坏配置 restart 而被主动停掉。这里是流程预期,未执行远程实验。

5.5 使用要点

  1. 对需要处理的失败写清楚检测、补偿与最终失败传播,不用 ignore_errors 掩盖关键问题。
  2. 明确每一步修改的资源及原始状态,避免无条件删除已存在用户、目录或其他应用配置。
  3. 校验候选配置再重载,完整应用配置有 include 时不能只验证一个孤立 server 文件。
  4. 标签会过滤块内任务,补偿和审计也要覆盖实际运行路径;更改标签后重新审阅故障路径。
  5. 多主机补偿、软件升级、数据库迁移和跨系统事务需要各自设计,block 不提供自动的全局原子性。

6. Role 进阶实践

本章各组原始结果来自不同的实验阶段及清理后的目标状态。复制或还原控制端项目不会清理被控节点;when 跳过不撤销既有配置,改用新配置文件名也不会删除旧文件。为复现单阶段结果,应使用可重建的独立实验基线,或先备份并仅迁移本课程管理的旧配置;保留旧文件连续运行时,预期同时保留其监听端口,不套用下面的历史端口列表。

6.1 角色调用与参数

Role 不是名为 roles 的功能模块;Play 的 roles 关键字组织角色调用。列表可以用标准 role 字段或简写:

- hosts: websrvs
  remote_user: root
  roles:
    - role: mysql
    - memcached
    - nginx

这些角色目录必须已经安装或存在于角色路径。需要参数时传入映射:

- hosts: web
  remote_user: root
  roles:
    - role: role_nginx
      nginx_port: 8888

流式写法等价:

- hosts: web
  remote_user: root
  roles:
    - {role: role_nginx, nginx_port: 8888}

角色参数的优先级与变量来源有关,不等于所有变量都简单全局共享;覆盖策略需结合该版本变量优先级。

6.2 将子配置改为模板

以下延续前述进阶教材已建好的 /data/ansible/role/nginx_pro 基础项目,不是空目录直接执行的完整仓库。先备份:

cd /data/ansible/role
cp -a nginx_pro nginx_pro-base
cd nginx_pro/roles/role_nginx
mkdir -p templates
cp files/nginx-define.conf templates/nginx-define.conf.j2

旧项目可能把静态配置放在 nginx_conf,按其真实路径复制,不要把不存在的目录当作固定约定。模板端口行改为:

server {
    listen {{ nginx_port }};
    root /data/webserver/html;
    location / {
    }
}

roles/role_nginx/tasks/configs.yaml 中更新子配置任务:

- name: Render the managed subconfiguration
  ansible.builtin.template:
    src: nginx-define.conf.j2
    dest: /etc/nginx/conf.d/nginx-define.conf
    mode: '0644'

这里在 Role 的 templates 路径内查找源,不需要重复硬编码 templates/ 前缀。原来的主配置和目录初始化任务继续保留;基础角色还要有配置校验、启动或重载动作,不是 template 自己自动重载服务。

nginx_role.yaml 使用上一节 nginx_port: 8888,检查和执行:

cd /data/ansible/role/nginx_pro
ansible-playbook nginx_role.yaml -C
ansible-playbook nginx_role.yaml
ansible web -m ansible.builtin.shell -a "grep 'listen' /etc/nginx/conf.d/nginx-define.conf"
ansible web -m ansible.builtin.shell -a 'ss -tlnp | grep nginx'
ansible web -m ansible.builtin.command -a 'curl -s localhost:8888'

-C 为检查模式,部分模块或任务不能完整预测依赖和运行结果,不能将其等同于完整部署测试。混合系统组要由基础 Role 处理对应包管理器或使用专用清单。

6.3 按条件调用角色

可对静态角色调用附加 when,条件通常继承到角色任务,并在各任务执行时求值;条件依赖的 Facts 需先收集。

原概述写“IP 是 .14”,实际条件与输出按主机名 top-node 判断,统一按后者解释。

ansible web -m ansible.builtin.setup -a 'filter=ansible_hostname'

原该轮实验清单为 .13/.16/.19,不是最初三节点清单:

10.0.0.16: ansible_hostname = top-node
10.0.0.13: ansible_hostname = top-node1
10.0.0.19: ansible_hostname = top-node
- hosts: web
  remote_user: root
  gather_facts: true
  roles:
    - role: role_nginx
      nginx_port: 10086
      when: ansible_facts['hostname'] == 'top-node'
cd /data/ansible/role/nginx_pro
ansible-playbook nginx_role.yaml -C
ansible-playbook nginx_role.yaml

原监听查询结果:

10.0.0.13 | FAILED | rc=1 >>
non-zero return code
10.0.0.19 | CHANGED | rc=0 >>
tcp 0 0 0.0.0.0:10086 0.0.0.0:* LISTEN 21981/nginx: master
10.0.0.16 | CHANGED | rc=0 >>
tcp 0 0 0.0.0.0:10086 0.0.0.0:* LISTEN 22054/nginx: master

.13 不满足条件,该次原实验在其已无 Nginx 监听的前提下,查询没有匹配而返回 1;这不是角色部署到该节点后失败。如果连续执行上一节而保留 8888 部署,when 跳过不会删除它,查询仍可能找到旧端口,不能预言必定无匹配。条件字符串可以包含合法标点,例如 top-node 的连字符;并非只能由数字和字母构成。

6.4 按标签选择角色

原文称“基于 when 管理标签”,这里实际使用 tags。备份并复制第二个角色:

cd /data/ansible/role
cp -a nginx_pro nginx_pro_when
cp -a nginx_pro_when/roles/role_nginx nginx_pro/roles/role_nginx1
cd nginx_pro

确保各备份目标未存在;同名目录存在时 cp -a 可能形成非预期嵌套。

第一个角色将子配置按端口保存,避免覆盖同一个文件:

- name: Render the port-specific subconfiguration
  ansible.builtin.template:
    src: nginx-define.conf.j2
    dest: '/etc/nginx/conf.d/nginx-{{ nginx_port }}.conf'
    mode: '0644'

两个角色示例:

- hosts: web
  remote_user: root
  roles:
    - {role: role_nginx, nginx_port: 200, tags: [nginx, web]}
    - {role: role_nginx1, nginx_port: 100, tags: [nginx1, web]}

role_nginx1 是复制修改前的角色,原实验仍写 nginx-define.conf,所以两个目录的任务并不完全相同。要统一文件命名,应同时修改两个角色。

cd /data/ansible/role/nginx_pro
ansible-playbook -C -t nginx nginx_role.yaml -l 10.0.0.16
ansible-playbook -t nginx nginx_role.yaml -l 10.0.0.16
ansible 10.0.0.16 -a 'ls /etc/nginx/conf.d'
ansible 10.0.0.16 -m ansible.builtin.shell -a 'ss -tlnp | grep nginx'
ansible-playbook -C -t nginx1 nginx_role.yaml -l 10.0.0.19
ansible-playbook -t nginx1 nginx_role.yaml -l 10.0.0.19
ansible 10.0.0.19 -a 'ls /etc/nginx/conf.d'
ansible 10.0.0.19 -m ansible.builtin.shell -a 'ss -tlnp | grep nginx'

原结果:

10.0.0.16: nginx-200.conf
          0.0.0.0:200 LISTEN 23165/nginx: master
10.0.0.19: nginx-define.conf
          0.0.0.0:100 LISTEN 23119/nginx: master

这说明标签可控制角色选择;不说明两个同名服务实例被自动隔离。100/200 为原实验低端口,绑定权限和服务主进程用户需符合目标配置。

6.5 变量、Handlers 与模板循环整合

6.5.1 准备与目录

本节在已保存的基础角色上继续,不对已有共享业务主机执行整机清理。下面只还原控制端项目;被控端若仍有 nginx-define.conf、nginx-200.conf 等旧实验文件,新建 ansible_nodename.conf 后它们仍会生效。要复现只监听 81/82/83 的单阶段输出,先在专用基线上确认没有这些旧管理配置;保留它们时应把额外端口纳入预期。备份当前项目,再恢复基础副本:

cd /data/ansible/role
mv nginx_pro nginx_pro-tags
cp -a nginx_pro_when nginx_pro
cd nginx_pro
mkdir -p roles/role_nginx/vars roles/role_nginx/handlers

为保留原课件中的改造过程,下面仍展示 vars 文件;希望用户可覆盖的默认值通常更适合 defaults。Role vars 并非只能放常量,合法 Jinja 表达式也可使用,只需理解求值时机和变量优先级。

roles/role_nginx/vars/main.yaml:

server_name: ansible_nodename
pre_name: 'www.'
tail_name: '.com'

这里 server_name 的值是字面字符串 ansible_nodename,用于配置文件基本名,不是实际读取主机 Facts。若希望按真实节点名命名,可改用 "{{ ansible_facts['nodename'] }}" 等明确表达式,并核对最终路径。

6.5.2 Handlers

roles/role_nginx/handlers/main.yaml:

- name: validate nginx
  ansible.builtin.command: /usr/sbin/nginx -t
  changed_when: false
  listen: apply nginx configuration

- name: reload nginx
  ansible.builtin.service:
    name: nginx
    state: reloaded
  listen: apply nginx configuration

两者按照定义顺序响应同一个通知,先检查再重载;校验失败不应继续重载。服务需由基础安装任务启动,初始依赖和失败恢复按前文设计。

roles/role_nginx/tasks/configs.yaml 中的子配置任务:

- name: Render the virtual host configuration
  ansible.builtin.template:
    src: nginx-define.conf.j2
    dest: '/etc/nginx/conf.d/{{ server_name }}.conf'
    mode: '0644'
  notify: apply nginx configuration

Role 的 handlers/main.yaml 会自动加载。不要在 tasks/main.yaml 使用 include_tasks 导入 handlers 文件,否则任务会像普通任务立即执行,破坏“只在变更时通知”的机制。原文这条 include 已移除。

基础 configs.yaml 中的主配置 copy 也要通知同一个主题,不能只给子模板添加 notify;否则仅主配置变化时,后面的 started 不会重载已运行的服务。替换原有主配置复制任务,例如:

- name: Copy the main configuration and notify on change
  ansible.builtin.copy:
    src: nginx.conf
    dest: /etc/nginx/nginx.conf
    backup: true
    mode: '0644'
  notify: apply nginx configuration

主任务文件继续按依赖调用原基础文件,例如:

- name: Run the existing group preparation tasks
  ansible.builtin.include_tasks: groups.yaml
- name: Run the existing user preparation tasks
  ansible.builtin.include_tasks: users.yaml
- name: Run the existing package preparation tasks
  ansible.builtin.include_tasks: packages.yaml
- name: Run the existing configuration tasks
  ansible.builtin.include_tasks: configs.yaml
- name: Run the existing service preparation tasks
  ansible.builtin.include_tasks: services.yaml

这些文件沿用链接中已有基础项目。将基础 services.yaml 的无条件 restart 改成下面的校验与 started,避免每次运行都重启,让重载由变更通知负责:

- name: Validate the assembled configuration
  ansible.builtin.command: /usr/sbin/nginx -t
  changed_when: false
- name: Ensure the service is started and enabled
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

文件名以现有项目为准。若基线主任务还调用了其他合法步骤,应保留,不要直接覆盖丢失它们。配置校验放在初次启动之前;通知到正常 Handler 执行点时再校验及重载。

6.5.3 循环模板

roles/role_nginx/templates/nginx-define.conf.j2:

{% for vhost in nginx_vhosts %}
server {
    listen {{ vhost.port }};
    server_name {{ pre_name }}{{ vhost.name }}{{ tail_name }};
    root /data/webserver/html;
    location / {
    }
}
{% endfor %}

原模板允许 port 不存在就不写 listen,可能隐式使用默认端口并造成冲突。这里要求每项明确提供 port/name,与下面清单一致。

nginx_role.yaml:

- hosts: web
  remote_user: root
  vars:
    nginx_vhosts:
      - port: 81
        name: a
      - port: 82
        name: b
      - port: 83
        name: c
  roles:
    - role: role_nginx

原列表写成 - vhost1: {port: 81, name: a} 一类额外嵌套结构,却使用 vhost.port 读取,层级不匹配。这里改成包含 port/name 的扁平字典列表;不保留无法生成声称结果的写法。

6.5.4 测试与原结果

ansible-playbook nginx_role.yaml -C
ansible-playbook nginx_role.yaml
ansible web -m ansible.builtin.shell -a 'ss -tlnp | grep nginx'
ansible web -a 'ls /etc/nginx/conf.d'
ansible web -a 'grep server_name /etc/nginx/conf.d/ansible_nodename.conf'

原课件监听输出:

10.0.0.16 | CHANGED | rc=0 >>
tcp 0 0 0.0.0.0:83 0.0.0.0:* LISTEN 24643/nginx: master
tcp 0 0 0.0.0.0:82 0.0.0.0:* LISTEN 24643/nginx: master
tcp 0 0 0.0.0.0:81 0.0.0.0:* LISTEN 24643/nginx: master
10.0.0.19 | CHANGED | rc=0 >>
tcp 0 0 0.0.0.0:81 0.0.0.0:* LISTEN 24847/nginx: master
tcp 0 0 0.0.0.0:83 0.0.0.0:* LISTEN 24847/nginx: master
tcp 0 0 0.0.0.0:82 0.0.0.0:* LISTEN 24847/nginx: master
10.0.0.13 | CHANGED | rc=0 >>
tcp 0 0 0.0.0.0:81 0.0.0.0:* LISTEN 37167/nginx: master
tcp 0 0 0.0.0.0:82 0.0.0.0:* LISTEN 37167/nginx: master
tcp 0 0 0.0.0.0:83 0.0.0.0:* LISTEN 37167/nginx: master

各节点的原文件名均为 ansible_nodename.conf,与字面变量值一致。每个文件内原输出:

server_name www.a.com;
server_name www.b.com;
server_name www.c.com;

这里三行来自三个不同 server 段,不是同一个 server 重复声明。示例域名仅沿用课件,实际业务应使用有权管理的域名。

6.6 实验清理

原课件在多个阶段清空 /etc/nginx/conf.d、站点目录并删除测试账号。这只适用于可以整体销毁的专用实验环境;不能对承载其他业务的目标原样执行。

以下是确认目标为专用实验节点、数据已无保留需要时的阶段性操作示例,仅列原实验管理的配置,不清空整个 conf.d:

ansible 10.0.0.16 -m ansible.builtin.service -a 'name=nginx state=stopped'
ansible 10.0.0.16 -m ansible.builtin.file -a 'path=/etc/nginx/conf.d/nginx-define.conf state=absent'
ansible 10.0.0.16 -m ansible.builtin.file -a 'path=/etc/nginx/conf.d/nginx-200.conf state=absent'
ansible 10.0.0.16 -m ansible.builtin.file -a 'path=/etc/nginx/conf.d/ansible_nodename.conf state=absent'
ansible 10.0.0.16 -m ansible.builtin.apt -a 'name=nginx state=absent purge=true autoremove=true'
ansible 10.0.0.16 -m ansible.builtin.file -a 'path=/data/webserver/html state=absent'
ansible 10.0.0.16 -m ansible.builtin.user -a 'name=nginx-test state=absent'

其他实验节点及端口特定文件按清单分别处理;卸载 nginx 主包不保证全部子包都已删除,应核对实际包及配置状态。不要删除仍被现存配置引用的用户,也不要将本节“清理”称为恢复部署前状态。

参考资料

评论