服务器批量管理方案

结论: 一套可用的服务器批量管理方案由五部分组成——统一的主机清单(inventory)与分组、免密且可审计的接入通道(密钥分发 + 堡垒机)、并行执行引擎(Ansible 或 pssh)、配置一致性校验机制、以及灰度执行与回滚设计。缺少任何一环,批量操作都会从"效率工具"变成"批量事故制造机"。

引言:批量管理的风险与收益是同比例的

批量管理的收益很明显:一条命令在 100 台机器上生效,把 3 小时的工作压缩到 30 秒。但风险同样是 100 倍的——一条写错的命令也会在 100 台机器上生效,把一次小失误放大成全站故障。

我见过最典型的批量事故是:某人想清理 /tmp 下的旧文件,写成 rm -rf /tmp/old 却在某台机器上变量未展开变成 rm -rf /tmp/;还有人批量下发 sudoers 配置,语法错误导致所有机器的 sudo 全部失效,最后只能一台台走救援模式。这些事故的共同点不是工具用错了,而是缺少"批量操作的约束机制"。

所以本文讲的不只是"怎么批量执行",而是完整方案:从主机清单与分组开始(管谁),到接入通道(怎么连),到执行引擎(怎么跑),到一致性校验(跑完对不对),再到灰度与回滚(错了怎么办)。文中的命令以 Rocky Linux 9 / Ubuntu 24.04 LTS 为例,截至 2026 年 Ansible、pssh、堡垒机仍是主流选择。

一、主机清单与分组:批量管理的地基

批量管理的第一步不是装工具,而是建立权威的主机清单。没有清单,一切批量操作都是"凭记忆敲 IP",出错只是时间问题。

1.1 inventory 的分层设计

Ansible 的 inventory 支持"分组 + 变量 + 嵌套",足以表达绝大多数生产拓扑:

ini
# inventory/prod.ini
# 按角色分组
[web]
web[01:20] ansible_host=10.0.1.{{ 10 + groups.web.index(inventory_hostname) }}

上面这种动态写法可读性差,生产环境更推荐显式列举 + 按环境/角色双维度分组:

ini
# inventory/prod.ini
[web]
web01 ansible_host=10.0.1.11
web02 ansible_host=10.0.1.12
web03 ansible_host=10.0.1.13

[app]
app01 ansible_host=10.0.2.11
app02 ansible_host=10.0.2.12

[db]
db01  ansible_host=10.0.3.11
db02  ansible_host=10.0.3.12

[cache]
cache01 ansible_host=10.0.4.11

# 按地域二次分组(用于灰度)
[web_az1]
web01
web02

[web_az2]
web03

# 组嵌套:prod 包含所有生产机器
[prod:children]
web
app
db
cache

# 生产通用变量
[prod:vars]
ansible_user=opsuser
ansible_port=2222
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_common_args='-o StrictHostKeyChecking=accept-new'

# 数据库组专属变量
[db:vars]
mysql_port=3306
backup_retention_days=7

分组的三个维度建议同时具备:按角色(web/app/db)、按环境(prod/staging/test)、按地域或可用区(az1/az2,用于分批)。这样灰度时可以用 --limit web_az1 精确控制爆炸半径。

1.2 清单的维护方式

方式适用场景优点缺点
静态文件(inventory.ini)服务器数量稳定、变更少简单直观,易 review需手工维护,易与实际脱节
动态 inventory 脚本云环境、弹性伸缩自动发现新增机器需开发,调试成本
CMDB 系统中大型团队权威数据源,可关联业务信息建设成本高
云厂商 API 插件全量用云主机零维护,实时准确混合云场景覆盖不足

关键建议:无论用哪种方式,inventory 必须进 Git 并做 code review。 清单本身是基础设施的一部分,改错一个 IP 就可能导致操作打偏到别的机器。

二、接入通道:密钥分发与堡垒机

2.1 免密 SSH 的批量分发

批量执行的前提是控制端能免密登录所有机器。分发公钥的做法:

bash
# 1. 在控制端生成专用密钥(不要用个人密钥)
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_ansible -N '' -C "ansible-control"

# 2. 批量分发(首次需输入密码,用 sshpass 批量完成)
dnf install -y sshpass      # Ubuntu: apt install -y sshpass
while read -r host; do
  sshpass -p '<初始密码>' ssh-copy-id \
    -i ~/.ssh/id_ed25519_ansible.pub \
    -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
    opsuser@"$host"
done < hosts.txt

# 3. 验证连通性
ansible all -m ping -i inventory/prod.ini --private-key ~/.ssh/id_ed25519_ansible
bash
# 4. 目标机上的 sudo 免密配置(控制批量操作能否执行特权命令)
# /etc/sudoers.d/opsuser   (务必用 visudo 语法校验)
cat > /etc/sudoers.d/opsuser <<'EOF'
opsuser ALL=(ALL) NOPASSWD:/usr/bin/systemctl,/usr/bin/dnf,/usr/bin/apt,/usr/sbin/nginx
EOF
chmod 440 /etc/sudoers.d/opsuser
visudo -cf /etc/sudoers.d/opsuser      # 校验语法,配错会导致 sudo 全废

安全权衡:NOPASSWD:ALL 方便但风险极高(拿到 opsuser 就等于 root)。建议按命令白名单授权,只放行确实需要的命令。

2.2 堡垒机(跳板机)架构

服务器数量超过 20 台、或涉及多人协作与合规要求时,应引入堡垒机(Bastion Host / Jump Server)。

方案类型代表特点适用
开源堡垒机JumpServer、Teleport支持录屏审计、权限审批、资产分组中小企业首选,中文支持好
云厂商堡垒机各家云控制台产品托管、免运维、与云账号打通全量上云且预算允许
SSH ProxyJump原生 SSH -J零成本、无审计能力临时、小规模
VPN + 直连WireGuard/OpenVPN网络层收敛与堡垒机互补

原生 SSH 的跳板写法(小规模可用):

bash
# ~/.ssh/config
Host bastion
    HostName 203.0.113.10
    User opsuser
    Port 2222
    IdentityFile ~/.ssh/id_ed25519

Host web* app* db*
    User opsuser
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
    ProxyJump bastion

# 之后可直接 ssh web01,自动经堡垒机跳转
ssh web01

堡垒机带来的三个不可替代的价值:统一入口(生产机不再对公网开放 SSH)、操作审计(录屏与命令留痕,满足合规要求)、权限管控(谁能访问哪台机器、需要谁审批)。这三点在等保合规与离职审计时是硬需求。

三、执行引擎:Ansible 与 pssh 的分工

3.1 Ansible:复杂编排与幂等变更

适合"下发配置、装软件、改状态"这类需要幂等和可重复的操作。

bash
# 批量执行(ad-hoc)
ansible web -m shell -a "nginx -v" -i inventory/prod.ini
ansible all -m shell -a "uptime" -i inventory/prod.ini -o    # -o 单行输出,便于对比

# 批量分发文件
ansible web -m copy -a "src=./hosts dest=/etc/hosts mode=0644 backup=yes" -i inventory/prod.ini --become

# 批量执行脚本(先 copy 再执行,比直接 script 模块更可控)
ansible web -m script -a "./check_disk.sh" -i inventory/prod.ini

# 收集结果并保存
ansible all -m shell -a "df -h /" -i inventory/prod.ini -o > /tmp/df_report.txt

3.2 pssh / pdsh:极简的并行执行

Ansible 启动有固定开销(建立连接、收集 facts),对"只在 200 台机器上跑一条命令看看"这种场景,pssh 更轻快。

bash
# 安装
dnf install -y pssh        # 包名在 RHEL 系为 pssh,命令为 pssh/pscp/pslurp
# Ubuntu: apt install -y pssh

# 准备主机文件(格式:user@host:port)
cat > hosts_pssh.txt <<'EOF'
opsuser@10.0.1.11:2222
opsuser@10.0.1.12:2222
opsuser@10.0.1.13:2222
EOF

# 并行执行(-i 交互输入密码,-l 用户,-h 主机文件,-t 超时,-p 并发数)
pssh -h hosts_pssh.txt -l opsuser -i -t 30 -p 20 "uptime"
pssh -h hosts_pssh.txt -l opsuser -i -t 60 -p 10 "systemctl restart nginx"

# 并行上传
pscp -h hosts_pssh.txt -l opsuser ./nginx.conf /tmp/nginx.conf

# 并行下载(收集各机日志)
pslurp -h hosts_pssh.txt -l opsuser -L /tmp/collected /var/log/messages messages

# 输出目录控制(-o 保存输出到目录,便于事后逐台查看)
pssh -h hosts_pssh.txt -l opsuser -o /tmp/pssh_out -t 30 "df -h | grep '/$'"
ls /tmp/pssh_out
bash
# pdsh 用法(另一个常见选择,支持 genders 分组)
pdsh -w ssh:opsuser@web[01-20] "uptime"
pdsh -g web "hostname; uptime"        # 需配置 genders 文件

两者的分工建议:

场景推荐工具原因
装软件、下发配置、改状态Ansible幂等、可重复、有 --check 预览
快速在几百台上看一条命令pssh启动快、无 facts 收集开销
需要结构化收集结果(JSON)Ansible输出可编程处理
临时批量重启服务pssh + 分批简单直接
需要长期维护、进 GitAnsible版本化、可 review

四、配置一致性:批量管理的核心目标

批量管理的终极目标不是"能批量执行",而是所有机器配置一致。一致性需要可度量。

4.1 一致性校验方法

bash
# 1. 批量收集关键配置指纹并对比
ansible all -m shell -a "md5sum /etc/nginx/nginx.conf /etc/ssh/sshd_config /etc/my.cnf 2>/dev/null" \
  -i inventory/prod.ini -o | sort

# 2. 用 Ansible 的 diff 模式发现漂移(不实际修改)
ansible-playbook site.yml -i inventory/prod.ini --check --diff --limit web

# 3. 批量对比软件版本
ansible all -m shell -a "rpm -q nginx mysql-server 2>/dev/null || dpkg -l | grep -E 'nginx|mysql' | awk '{print \$2, \$3}'" \
  -i inventory/prod.ini -o

# 4. 批量检查关键基线项
ansible all -m shell -a "systemctl is-enabled nginx; ss -lnt | grep -c ':80'; getenforce" \
  -i inventory/prod.ini -o

更好的做法是把一致性检查写成定期任务,输出成一张表:

检查项期望值实际差异机器处理方式
nginx.conf md5与 Git 中一致web07重跑 playbook 收敛
Nginx 版本1.26.x无—
sshd PermitRootLoginnoweb12, app03重跑加固 playbook
/etc/hosts 条目8 条db02(7 条)手工确认后收敛

4.2 收敛策略:定期重跑 vs 只变更时跑

  • 只变更时跑(推模式常态):改了 playbook 才执行。优点是安全可控,缺点是期间发生的手工改动不会被发现。
  • 定期收敛(推荐):配置一个定时任务(如每天凌晨)以 --check 模式跑一遍并输出差异报告;对非核心配置可开启自动收敛(不加 --check,直接让 Ansible 把状态拉回)。
bash
# crontab:每天 03:00 检查配置漂移并邮件报告
0 3 * * * cd /opt/ansible && ansible-playbook site.yml -i inventory/prod.ini --check --diff 2>&1 | mail -s "配置漂移日报 $(date +\%F)" ops@example.com

注意:自动收敛(不加 --check)有风险——如果有人手工改了配置且未同步 playbook,收敛会静默覆盖掉他的改动。建议先跑报告模式,确认差异来源后再决定是否自动收敛。

五、批量改密与密钥轮换

批量改密是常见的合规要求(如等保要求口令定期更换),也是最容易搞砸的操作之一。

5.1 批量改密的安全做法

bash
# 生成随机密码(每台不同,避免一密通用)
openssl rand -base64 16

# Ansible 批量改密(用 vault 保管密码变量)
ansible-vault create vars/passwords.yml
# 内容示例:
# new_password_web: "<随机强口令>"
yaml
# change_password.yml
---
- name: 批量修改用户口令
  hosts: all
  become: yes
  vars_files:
    - vars/passwords.yml
  tasks:
    - name: 修改 opsuser 口令
      ansible.builtin.user:
        name: opsuser
        password: "{{ new_password | password_hash('sha512') }}"
        update_password: always

    - name: 强制下次登录修改(可选)
      ansible.builtin.command: "chage -d 0 opsuser"
bash
# 执行(先灰度一台,确认能登录后再全量)
ansible-playbook change_password.yml -i inventory/prod.ini --limit web01 --ask-vault-pass
# 验证新密码可登录后再全量
ansible-playbook change_password.yml -i inventory/prod.ini --ask-vault-pass

批量改密的三条铁律:一是先灰度一台并验证新密码能登录,再全量;二是保留一个已登录的会话不退出,防止全部锁死;三是密码必须各不相同并存入密码管理器或 Vault,一密通用等于一次泄露全盘失守。

5.2 SSH 密钥轮换

bash
# 1. 生成新密钥对
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_new -N '' -C "rotated-$(date +%F)"

# 2. 先"追加"新公钥(不要先删旧的,避免失败后连不上)
ansible all -m authorized_key -a "user=opsuser key='{{ lookup('file', '~/.ssh/id_ed25519_new.pub') }}' state=present" \
  -i inventory/prod.ini --become

# 3. 验证新密钥可登录
ansible all -m ping -i inventory/prod.ini --private-key ~/.ssh/id_ed25519_new

# 4. 确认无误后再删除旧公钥
ansible all -m authorized_key -a "user=opsuser key='{{ lookup('file', '~/.ssh/id_ed25519_old.pub') }}' state=absent" \
  -i inventory/prod.ini --become

这个"先加后删"的顺序是密钥轮换的标准做法,能确保在任意步骤失败时都有可用的登录通道。

5.3 清理失控的 authorized_keys

bash
# 批量审计:列出所有机器上的 root 与 opsuser 公钥,找出未授权的
ansible all -m shell -a "echo '--- root ---'; cat /root/.ssh/authorized_keys 2>/dev/null; \
  echo '--- opsuser ---'; cat /home/opsuser/.ssh/authorized_keys 2>/dev/null" \
  -i inventory/prod.ini -o > /tmp/keys_audit.txt

# 与授权清单对比,找出多余公钥后精确删除
ansible web07 -m authorized_key -a "user=root key='ssh-ed25519 AAAA...旧公钥...' state=absent" --become

六、灰度执行与回滚:批量操作的安全带

这是批量管理方案里最重要、也最容易被省略的一节。

6.1 灰度策略

策略做法适用
单机灰度--limit web01所有变更的第一步
按组灰度--limit web_az1 → --limit web_az2有可用区划分时
按百分比--forks 控制并发 + 分批 inventory大批量
金丝雀指定 1 台作为金丝雀,观察 30 分钟核心业务变更
bash
# 标准灰度流程
# 第 1 步:预览
ansible-playbook site.yml -i inventory/prod.ini --check --diff

# 第 2 步:单机灰度
ansible-playbook site.yml -i inventory/prod.ini --limit web01

# 第 3 步:验证(业务健康检查 + 日志)
ansible web01 -m shell -a "systemctl is-active nginx && curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1/healthz" -i inventory/prod.ini
ansible web01 -m shell -a "journalctl -u nginx --since '5 min ago' --no-pager | tail -20" -i inventory/prod.ini

# 第 4 步:观察 10~30 分钟后,按组分批全量
ansible-playbook site.yml -i inventory/prod.ini --limit web_az1
sleep 300
ansible-playbook site.yml -i inventory/prod.ini --limit web_az2

6.2 回滚设计

回滚能力必须在变更前就准备好,而不是出事后现想。三类回滚手段:

bash
# 1. 配置回滚:下发时自动备份(Ansible copy/template 模块的 backup 参数)
- name: 下发配置并自动备份
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    backup: yes            # 生成 nginx.conf.<时间戳>~ 备份文件
    validate: 'nginx -t -c %s'
bash
# 2. Git 回滚:playbook 版本化,出问题 revert 后重跑
git revert HEAD && ansible-playbook site.yml -i inventory/prod.ini

# 3. 整机回滚:云快照/镜像(最彻底,但耗时最长)
# 变更前的标准动作:对将变更的机器打快照

回滚的检查清单(变更前的准备项):

  • [ ] 变更前是否已备份原配置文件?
  • [ ] 回滚脚本是否已写好并测试过?
  • [ ] 回滚预计耗时是否在可接受范围(对比 RTO)?
  • [ ] 数据库变更是否有对应的向下兼容方案(回滚代码但数据结构已变,是最难的情况)?
  • [ ] 谁有权决定回滚?决策链路是否明确?

6.3 批次控制与失败中止

yaml
# playbook 中加入批次与失败阈值控制
- hosts: web
  serial: 2              # 每批 2 台,一批失败则中止后续批次
  max_fail_percentage: 10
  any_errors_fatal: true
  become: yes
  tasks:
    - name: 变更任务
      ansible.builtin.template:
        src: app.conf.j2
        dest: /etc/app/app.conf
      notify: restart app

  post_tasks:
    - name: 健康检查
      ansible.builtin.uri:
        url: http://127.0.0.1:8080/healthz
        status_code: 200
      retries: 5
      delay: 10
      register: health
      until: health.status == 200

serial: 2 + max_fail_percentage + 健康检查的 until 重试,这三者的组合能让批量变更具备"自动熔断"能力——第一批出问题就不再继续,把爆炸半径限制在最小。