一套自动化项目真正困难的部分,通常不是会不会写 yum、template 或
systemd,而是能不能把服务器之间的关系表达清楚。
我在
linux_ansible
项目中尝试解决的,正是这个问题。它不是一个只安装 Nginx 的练习,而是一套面向
Kylin Linux Advanced Server V10 的完整部署工程:从 SSH 初始化和系统基线开始,
一直编排到 MySQL、Redis、NFS、备份、负载均衡、WordPress、phpMyAdmin、ZrLog
和 RuoYi。
最终,整套环境可以从 Ansible 控制机通过一个入口执行:
cd /ansible
ansible-playbook playbooks/site.yml
但“一条命令”只是表面结果。真正值得复盘的是:这条命令背后,如何描述 13 台 服务器、两个网络、多个数据服务、一组业务节点和一个可漂移的高可用入口。
先确定边界:这是一个什么项目
项目编写和验证时使用的环境是:
Kylin Linux Advanced Server V10 (Lance)
内核:4.19.90-52.22.v2207.ky10.x86_64
Ansible Core:2.13.13
这个边界非常重要。自动化代码从来不是脱离操作系统存在的:软件包名称、服务名、 网卡配置方式、RPM 依赖、默认目录甚至 Python 解释器,都可能随发行版和版本变化。
因此,这个项目没有宣称“一套代码兼容所有 Linux”。相反,我选择先把目标环境 收窄,把一套具体架构部署通,再讨论抽象和兼容性。对学习项目而言,这比一开始 堆积大量发行版判断更容易理解,也更容易验证。
项目中的 13 台机器可以分成控制入口、基础设施和业务节点三层:
| 节点 | 数量 | 职责 |
|---|---|---|
| jump | 1 | 跳板机 |
| ansible | 1 | 自动化控制机 |
| lb01、lb02 | 2 | Nginx 负载均衡与 Keepalived 主备 |
| web01、web02 | 2 | PHP、Java 和常规 Web 业务 |
| web03、web04 | 2 | RuoYi 后端与前端 |
| nfs01 | 1 | 共享存储 |
| backup01 | 1 | rsync 备份 |
| db01、db02 | 2 | MySQL 主从节点 |
| rd01 | 1 | Redis |
这里还存在两个不同用途的网络:
10.0.0.0/24是管理网络,Ansible 通过它连接各节点;172.16.1.0/24是业务内网,数据库、Redis、NFS 和 Web 节点通过它通信。
对外访问不直接落到 lb01 或 lb02,而是统一指向 VIP
10.0.0.203。Keepalived 根据节点状态让 VIP 在两台负载均衡服务器之间漂移。
这样,具体 LB 节点发生故障时,客户端入口不需要跟着改变。
这也是整个项目的第一个关键认识:
在写 Playbook 之前,必须先明确主机职责、网络路径、服务依赖和故障切换方式。
如果拓扑本身没有想清楚,自动化只会更快地复制混乱。
Inventory 不只是主机清单
很多 Ansible 入门示例把 Inventory 写成几行 IP 地址,但在这个项目中,Inventory 承担的是“架构模型”的职责。
[lb]
lb01 ansible_host=10.0.0.205 internal_ip=172.16.1.205 \
router_id=lb01 state=MASTER priority=150 nopreempt=false
lb02 ansible_host=10.0.0.206 internal_ip=172.16.1.206 \
router_id=lb02 state=BACKUP priority=100 nopreempt=true
[web]
web01 ansible_host=10.0.0.207 internal_ip=172.16.1.207
web02 ansible_host=10.0.0.208 internal_ip=172.16.1.208
[db]
db01 ansible_host=10.0.0.251 internal_ip=172.16.1.251
db02 ansible_host=10.0.0.252 internal_ip=172.16.1.252
[managed:children]
lb
web
nfs
backup
db
rd
ruoyi_web
从这段配置可以读出三类信息。
第一类是连接信息。ansible_host 表示控制机应通过哪个管理地址连接目标主机。
第二类是业务网络信息。internal_ip 不是 Ansible 的连接地址,而是服务间通信
使用的地址。数据库、缓存、共享存储和后端节点不必绕回管理网络。
第三类是角色特有信息。只有 LB 节点需要 state、priority 和
nopreempt,这些变量会进入 Keepalived 模板,决定谁是主节点、谁是备用节点。
managed:children 同样不是为了好看。它把所有需要接受网络初始化的分组聚合为一个
逻辑集合。以后新增节点时,只要将它归入相应业务组,就能继承统一的网络阶段,而
不必在 Playbook 中重复维护一长串主机名。
Inventory 因此不只是“Ansible 去哪里执行命令”,它同时回答:
- 这台机器属于哪一层?
- 它和哪些机器使用同一套部署能力?
- 它有哪些区别于同组节点的属性?
- 哪些跨主机关系可以由分组自动推导?
当这些信息被结构化后,Playbook 才有机会摆脱散落的硬编码。
用 group_vars 表达依赖关系
主机被分组之后,下一步不是立刻写 Task,而是建立服务之间的引用关系。
项目在 inventories/production/group_vars/all.yml 中集中计算公共依赖:
service_ip_var: internal_ip
app_network_cidr: 172.16.1.0/24
db_primary_host: >-
{{ hostvars[groups['db'][0]][service_ip_var]
| default(hostvars[groups['db'][0]].ansible_host) }}
redis_host: >-
{{ hostvars[groups['rd'][0]][service_ip_var]
| default(hostvars[groups['rd'][0]].ansible_host) }}
nfs_host: >-
{{ hostvars[groups['nfs'][0]][service_ip_var]
| default(hostvars[groups['nfs'][0]].ansible_host) }}
web_backend_hosts: >-
{{ groups['web'] | map('extract', hostvars, service_ip_var) | list }}
这里最有价值的不是 Jinja2 语法,而是变量的来源。
数据库主节点来自 db 分组的第一个成员;Redis 地址来自 rd 分组;NFS 地址来自
nfs 分组;负载均衡后端则从 web 分组批量提取业务内网地址。也就是说,服务
发现依赖 Inventory,而不是依赖某个 Role 中写死的 IP。
这样做带来两个直接收益。
首先,拓扑发生变化时,修改点更集中。替换数据库节点、增加 Web 节点或者调整业务 内网地址,优先修改 Inventory,不需要进入多个模板逐个搜索旧地址。
其次,变量名称表达了架构意图。模板使用 db_primary_host,比直接使用
172.16.1.251 更容易理解,也让后续的主从切换或环境拆分有清晰入口。
这套写法仍然有边界。通过 groups['db'][0] 认定第一个数据库节点是主库,适合
结构固定的学习环境,但生产环境更适合使用明确的主机变量,例如
mysql_role=primary,再通过断言确保只存在一个主库。隐含的顺序约定越多,
Inventory 调整时越容易产生意外。
Role 是能力边界,不是目录包装
项目将部署能力拆成多个 Role:
roles/
├── ssh
├── network
├── basic
├── nginx
├── php
├── mysql
├── redis
├── nfs
├── backup
├── mount
├── lb
├── web
├── tomcat
├── ruoyi-back
└── ruoyi-front
我对 Role 的理解是:它应该描述一种相对独立、可以被编排的能力。
例如 nginx 负责安装和启动 Nginx,lb 负责生成负载均衡与 Keepalived 配置,
mount 负责让业务节点挂载共享目录。虽然这些能力最终都可能作用在 Web 链路上,
但生命周期和复用范围不同,不适合塞进同一个巨大 Role。
这种拆分方式有几个现实好处:
- 发生故障时,可以更快定位到具体能力边界;
- 相同 Role 可以应用到不同主机组;
- 变量、模板、文件和 Handler 能围绕一个职责组织;
- 可以通过 tags、测试 Playbook 或
--limit缩小变更范围; - 新增服务时,通常只需新增 Role,再接入总 Playbook。
当然,Role 也不是拆得越细越好。如果一个 Role 只有一条任务,并且永远不会独立 复用或验证,过度拆分反而会让执行链难以阅读。判断标准不是文件数量,而是这部分 能力是否拥有独立的输入、输出和失败边界。
site.yml 是发布顺序
所有 Role 最终由 playbooks/site.yml 编排。它最关键的价值不是“调用了哪些
Role”,而是明确了先后关系。
- name: Bootstrap SSH key authentication
hosts: localhost
gather_facts: false
roles:
- ssh
- name: Configure managed server network
hosts: managed
gather_facts: false
serial: 1
roles:
- role: network
tags: network
- name: System baseline
hosts: all
roles:
- role: basic
tags: basic
- hosts: rd
roles:
- redis
- hosts: db
roles:
- mysql
完整执行链可以概括为:
- 从控制机完成 SSH 引导;
- 逐台配置被管服务器网络;
- 应用系统基线;
- 部署 Redis 和数据库;
- 部署备份、NFS 和共享目录;
- 部署 Web 运行环境与业务;
- 最后部署负载均衡和 VIP;
- 补充 RuoYi 前后端。
这个顺序反映了真实依赖:业务应用需要数据库和缓存,Web 节点需要共享存储,负载 均衡只有在后端可用后才有意义。把顺序显式写进总 Playbook,比依赖操作者记住 “先跑哪个脚本”可靠得多。
网络阶段使用 serial: 1 是一个值得保留的设计。修改远程网卡配置属于高风险
操作,如果同时对所有节点执行,一次变量错误可能让整套环境瞬间失联。逐台处理
虽然更慢,却把故障范围限制在单个节点,也给观察连接恢复留下空间。
不过,serial: 1 只能降低爆炸半径,不能替代验证。更完整的实现应在应用网络
配置前检查变量,在 Handler 执行后通过 wait_for_connection 验证节点重新上线,
必要时保留旧连接或配置恢复路径。
关键设计一:自动化开始前,先解决 SSH
Ansible 是无 Agent 的,它通过 SSH 管理 Linux 主机。这带来一个看似简单却绕不开 的问题:在自动配置 SSH key 之前,控制机凭什么第一次连接目标服务器?
项目的处理方式是让第一个 Play 在 localhost 上执行 ssh Role,使用学习环境
预先约定的初始账号完成公钥分发。后续执行再转为 SSH key。
这相当于把“人工初始化阶段”也纳入了自动化边界。否则,即使后面的几十个 Role 完全自动,操作者仍需逐台登录服务器复制公钥,流程依然无法从零重复。
生产环境不应把初始密码明文保存在仓库变量中。更合适的方式包括:
- 首次运行时交互输入或使用临时凭据;
- 将引导凭据加密到 Ansible Vault;
- 由云平台、镜像制作或配置管理系统预置公钥;
- 完成引导后立刻吊销临时密码;
- 对控制机私钥设置严格权限并纳入轮换制度。
SSH 引导的核心不是“免密登录”,而是定义可信入口如何建立、如何使用、何时退出。
关键设计二:双网段不是多写一个 IP
每台业务服务器同时拥有管理地址和业务内网地址,这意味着自动化必须清楚地区分 “我通过哪里管理它”和“服务之间通过哪里访问它”。
项目使用 ansible_host 作为管理入口,使用 internal_ip 构建服务依赖,并通过
service_ip_var 统一选择业务地址。网络 Role 在真正写入配置前先执行断言:
- name: Validate management network variables
assert:
that:
- network_management_interface | length > 0
- network_management_ip | length > 0
- network_management_prefix | int > 0
- network_gateway | length > 0
- network_dns | length > 0
这类断言看似只是几行检查,却能阻止空变量被渲染进网卡配置。相比任务执行到一半 才失联,提前失败通常是更好的结果。
双网段设计也让后续安全策略更清晰:管理端口可以限制在管理网,数据库、Redis 和 NFS 只对业务内网开放,外部流量则只进入 VIP。网络隔离不再是每个服务各自约定, 而成为整个 Inventory 和变量体系的一部分。
关键设计三:高可用入口必须独立于具体节点
负载均衡层由 lb01 和 lb02 组成,Nginx 负责代理业务,Keepalived 负责 VIP
漂移。Inventory 中为两台节点设置不同的状态和优先级,Role 再据此渲染配置。
项目还处理了 HTTPS 证书的两种情况:
- 如果 Role 的文件目录中存在自定义证书和私钥,就复制到 LB;
- 如果学习环境没有证书,则自动生成自签名证书,保证部署链路可以继续验证。
这个降级策略适合实验环境,因为它让“缺少真实证书”不会阻断其他模块学习。但生产 环境必须使用受信任证书,并把私钥放在受控的密钥系统或服务器安全目录中,不能 提交到 Git 仓库。
高可用也不能只看 VIP 是否存在。更重要的是健康检查是否真正代表服务可用。一个 只检查 Nginx 进程的脚本,无法发现所有后端都已经失败。生产化时应同时关注:
- 本机代理进程是否可用;
- 核心 upstream 是否仍有健康节点;
- VIP 漂移后连接是否恢复;
- 主节点恢复时是否允许抢占;
- 切换期间是否产生会话或写入问题。
VIP 解决的是入口连续性,不会自动解决后端状态、数据一致性和业务健康。
一条命令带来了什么
完成这些分层后,site.yml 能统一部署:
- 系统网络和主机基线;
- Redis;
- MySQL/MariaDB 与业务数据库;
- NFS 共享存储与挂载;
- rsync 备份服务;
- Nginx、PHP-FPM、Tomcat;
- Nginx 负载均衡与 Keepalived;
- WordPress、phpMyAdmin、ZrLog;
- RuoYi 前端和后端。
更重要的是,各模块不必每次整体执行。Ansible 官方提供的执行控制可以把验证范围 缩小到具体主机或标签:
# 先做语法检查
ansible-playbook playbooks/site.yml --syntax-check
# 仅观察可能发生的变化
ansible-playbook playbooks/site.yml --check --diff
# 只处理网络标签
ansible-playbook playbooks/site.yml --tags network
# 只限制到一台 Web 节点
ansible-playbook playbooks/site.yml --limit web01
--check 不是所有模块都能完美模拟,也不能替代测试环境,但它能帮助操作者在真正
修改前发现明显偏差。--diff 对模板和配置文件尤其有价值,--limit 和 tags
则能降低局部修复时的影响范围。
自动化成熟度不应只用“能否从头跑完”衡量,还要看它是否支持小范围验证、重复执行 和故障定位。
项目离真正生产级还有多远
这套项目完成了架构建模和全链路部署,但它仍然是一套学习工程。复盘时最重要的 不是隐藏不足,而是知道下一步该往哪里演进。
1. 凭据需要退出明文变量
学习环境为了降低使用门槛,采用了统一凭据。真实环境中,SSH、数据库、Redis、 Keepalived 和业务后台必须使用不同的强凭据,并通过 Ansible Vault 或外部密钥 管理系统保存。
ansible-vault encrypt inventories/production/group_vars/vault.yml
ansible-playbook playbooks/site.yml --ask-vault-pass
更进一步,可以使用 Vault ID 区分不同环境和不同密钥来源,避免所有秘密共用一个 解密入口。
2. Inventory 应拆分环境
当前项目只有一套 production 结构。真实团队通常至少需要:
inventories/
├── development/
├── staging/
└── production/
Role 保持复用,主机、域名、资源规格、证书和凭据由不同 Inventory 覆盖。变更先在 测试环境验证,再进入生产环境,而不是直接在生产主机上试错。
3. 系统版本耦合需要被显式管理
项目依赖 Kylin V10 的包、服务和网络脚本。要扩展到其他发行版,不能简单地把
yum 替换为 apt,还需要梳理:
- 软件包和仓库来源;
- systemd 服务名;
- 网卡管理工具;
- SELinux 或 AppArmor;
- 默认文件路径;
- PHP、Java、数据库等组件版本;
- RPM 或二进制包的架构兼容性。
如果短期内没有多发行版需求,保留清晰的系统前置条件比制造“伪兼容”更可靠。
4. 部署前后都需要验证
建议在执行链中加入四层检查:
- 语法与变量检查:
--syntax-check、assert和必填变量验证; - 变更预览:在支持的模块上使用
--check --diff; - 服务检查:端口、进程、配置语法和 systemd 状态;
- 业务检查:从 VIP 访问真实页面或健康接口。
例如 Nginx 模板更新后,不应直接重启,可以先执行 nginx -t;数据库导入后不应
只检查进程,而应执行最小查询;Keepalived 部署后应验证 VIP 当前归属以及切换。
5. 高风险变更需要分批和恢复路径
网络配置已经使用 serial: 1,同样的思想也应扩展到 Web 节点更新。可以先从负载
均衡中摘除一个节点,更新并完成健康检查,再放回 upstream,然后处理下一台。
数据库、共享存储和网络变更则需要明确备份、恢复和回滚策略。Ansible 能保证任务 按描述执行,但不会自动替你设计数据回滚。
最后的认识
做完这个项目后,我对“企业级 Ansible 项目”的理解不再是 Role 数量很多,或者 Playbook 能安装很多软件。
它更应该具备四个特征:
- 拓扑显式:Inventory 能表达节点职责和主机差异;
- 依赖显式:变量由分组关系推导,而不是散落硬编码;
- 顺序显式:总 Playbook 能看出基础设施和业务的依赖链;
- 风险显式:高风险操作有断言、分批、验证和恢复路径。
一条部署命令的价值,不是把几十条 Shell 命令藏了起来,而是把原本依赖个人记忆的 系统知识,变成任何人都能阅读、审查和重复执行的工程结构。
这也是我在
linux_ansible
中最想记录的部分:自动化的终点不是“无人操作”,而是让每一次操作都有明确输入、
可预测过程和可以验证的结果。
Discussion
评论