返回文章归档
AnsibleDevOps自动化运维Linux

Ansible 项目复盘

从服务器拓扑、Inventory、变量和 Role 编排出发,复盘一套覆盖高可用、数据库、共享存储与多业务部署的 Ansible 项目。

一套自动化项目真正困难的部分,通常不是会不会写 yumtemplatesystemd,而是能不能把服务器之间的关系表达清楚。

我在 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 节点通过它通信。

对外访问不直接落到 lb01lb02,而是统一指向 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 节点需要 stateprioritynopreempt,这些变量会进入 Keepalived 模板,决定谁是主节点、谁是备用节点。

managed:children 同样不是为了好看。它把所有需要接受网络初始化的分组聚合为一个 逻辑集合。以后新增节点时,只要将它归入相应业务组,就能继承统一的网络阶段,而 不必在 Playbook 中重复维护一长串主机名。

Inventory 因此不只是“Ansible 去哪里执行命令”,它同时回答:

  1. 这台机器属于哪一层?
  2. 它和哪些机器使用同一套部署能力?
  3. 它有哪些区别于同组节点的属性?
  4. 哪些跨主机关系可以由分组自动推导?

当这些信息被结构化后,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

完整执行链可以概括为:

  1. 从控制机完成 SSH 引导;
  2. 逐台配置被管服务器网络;
  3. 应用系统基线;
  4. 部署 Redis 和数据库;
  5. 部署备份、NFS 和共享目录;
  6. 部署 Web 运行环境与业务;
  7. 最后部署负载均衡和 VIP;
  8. 补充 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 和变量体系的一部分。

关键设计三:高可用入口必须独立于具体节点

负载均衡层由 lb01lb02 组成,Nginx 负责代理业务,Keepalived 负责 VIP 漂移。Inventory 中为两台节点设置不同的状态和优先级,Role 再据此渲染配置。

项目还处理了 HTTPS 证书的两种情况:

  1. 如果 Role 的文件目录中存在自定义证书和私钥,就复制到 LB;
  2. 如果学习环境没有证书,则自动生成自签名证书,保证部署链路可以继续验证。

这个降级策略适合实验环境,因为它让“缺少真实证书”不会阻断其他模块学习。但生产 环境必须使用受信任证书,并把私钥放在受控的密钥系统或服务器安全目录中,不能 提交到 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. 部署前后都需要验证

建议在执行链中加入四层检查:

  1. 语法与变量检查--syntax-checkassert 和必填变量验证;
  2. 变更预览:在支持的模块上使用 --check --diff
  3. 服务检查:端口、进程、配置语法和 systemd 状态;
  4. 业务检查:从 VIP 访问真实页面或健康接口。

例如 Nginx 模板更新后,不应直接重启,可以先执行 nginx -t;数据库导入后不应 只检查进程,而应执行最小查询;Keepalived 部署后应验证 VIP 当前归属以及切换。

5. 高风险变更需要分批和恢复路径

网络配置已经使用 serial: 1,同样的思想也应扩展到 Web 节点更新。可以先从负载 均衡中摘除一个节点,更新并完成健康检查,再放回 upstream,然后处理下一台。

数据库、共享存储和网络变更则需要明确备份、恢复和回滚策略。Ansible 能保证任务 按描述执行,但不会自动替你设计数据回滚。

最后的认识

做完这个项目后,我对“企业级 Ansible 项目”的理解不再是 Role 数量很多,或者 Playbook 能安装很多软件。

它更应该具备四个特征:

  1. 拓扑显式:Inventory 能表达节点职责和主机差异;
  2. 依赖显式:变量由分组关系推导,而不是散落硬编码;
  3. 顺序显式:总 Playbook 能看出基础设施和业务的依赖链;
  4. 风险显式:高风险操作有断言、分批、验证和恢复路径。

一条部署命令的价值,不是把几十条 Shell 命令藏了起来,而是把原本依赖个人记忆的 系统知识,变成任何人都能阅读、审查和重复执行的工程结构。

这也是我在 linux_ansible 中最想记录的部分:自动化的终点不是“无人操作”,而是让每一次操作都有明确输入、 可预测过程和可以验证的结果。

Discussion

评论

加载中
正在检查登录状态…