从手工登录到一条命令:Ansible 批量运维的落地笔记
前因:五台机器,半小时

我们早期有个服务集群,五台应用服务器,每次发版都要逐个登录:
ssh app1
docker restart myapp
tail -f /var/log/app.log
# 等三十秒,确认起来
exit
ssh app2
...
五台下来,加上网络延迟、确认时间,一趟完整的发版要四十分钟。更麻烦的是验证环节:每台机器都要手动查进程、看端口、检查依赖服务状态。有一回 app3 的容器没起来,健康检查接口返回 503,我没有逐台看日志,直接在群里说「发布完了」,结果用户端故障持续了十分钟才发现。
这类问题有个共性:操作在多个机器上并行发生,但人在串行确认。批量任务天然适合自动化工具,而 Ansible 是我最常用的选择之一。
为什么是 Ansible,而不是 PXE 或 Ansible Tower
自动化部署的工具链很长:PXE 裸机安装适合大规模集群初始化,SaltStack 需要部署 agent,Puppet 依赖证书体系,Kubernetes Operator 适合容器化场景。但如果是五台到几百台之间的物理机或虚拟机,需要管理操作系统配置、分发文件、执行命令、管理包安装,Ansible 是最轻量的方案——因为它不需要在目标机器上装任何东西。
核心机制是 SSH:主控机(control node)通过 SSH 连接目标机,把 Python 脚本传输过去执行,返回结果后清理临时文件。整个过程对目标机无侵入,不需要开机自启服务,不需要防火墙例外端口(除了默认的 22)。
代价也有:SSH 连接开销在机器数量多时会放大,大批量(上千台)并行执行时主控机会成为瓶颈。这时更适合 SaltStack 或专门的配置管理平台。
核心概念:Inventory、Playbook、Module
Inventory:机器清单
/etc/ansible/hosts 是默认位置,也可以指定其他路径:
[app_servers]
app1 ansible_host=192.168.1.11
app2 ansible_host=192.168.1.12
app3 ansible_host=192.168.1.13
[db_servers]
db1 ansible_host=192.168.1.21
[prod:children]
app_servers
db_servers
变量可以在 inventory 里定义,也可以在运行时传入:
ansible all -m ping -e "env=prod version=2.3.1"
常见坑:ansible_host 和 ansible_ssh_host 是等价的,但混用时容易写错。ansible_user 和 ansible_ssh_user 同样,建议统一用 ansible_user。
Playbook:声明式执行流程
YAML 格式,描述「应该是什么状态」,而不是「执行什么命令」:
- hosts: app_servers
become: yes
tasks:
- name: Ensure Java 17 is installed
apt:
name: openjdk-17-jdk
state: present
- name: Deploy application jar
copy:
src: deploy/{{ version }}.jar
dest: /opt/app/app.jar
owner: appuser
group: appuser
mode: '0644'
- name: Restart service
systemd:
name: myapp
state: restarted
enabled: yes
关键字段说明:
become: yes:使用 sudo 提权,不需要切换到 root 用户登录state: present/absent/restarted/reloaded:声明式状态,模块会自行判断当前状态并执行必要的变更owner/group/mode:文件权限管理,避免手动 chmod 忘记改权限导致启动失败
Module:可复用的原子操作
Ansible 内置了几百个模块,常用分类:
| 类别 | 典型模块 | 用途 |
|---|---|---|
| 系统 | apt/yum/dnf | 包管理 |
| 文件 | copy/template/file | 文件分发、模板渲染 |
| 服务 | systemd/service | 服务管理 |
| 命令 | command/shell | 执行任意命令 |
| 用户 | user/group | 账户管理 |
| 数据库 | mysql_db/postgresql_db | 数据库操作 |
command 模块不经过 shell,安全但功能受限;shell 模块可以执行管道和重定向,但要警惕注入风险。
批量执行:一次命令搞定多台
基础命令格式:
# 测试连通性
ansible all -m ping
# 在 app_servers 组执行
ansible app_servers -m shell -a "systemctl status myapp"
# 带提权执行
ansible app_servers -m shell -a "apt update" -b
# 异步执行,不等待结果
ansible app_servers -m shell -a "long-running-task.sh" --async 3600 --poll 0
参数说明:
-m:指定模块名-a:模块参数-b:等同于--become,使用 sudo--forks:并行度,默认 5,机器多时可以调高--limit:限制执行范围,如--limit app1,app2
并行度的取舍:--forks 越大,执行越快,但主控机会同时打开更多 SSH 连接。如果目标机有连接数限制(如 MaxStartups),需要相应调整。生产环境通常设置在 20-50 之间。
真实案例:批量部署 Spring Boot 服务
下面是我实际使用过的 playbook,做了脱敏处理:
---
- name: Deploy backend service
hosts: app_servers
become: yes
vars:
app_name: user-service
app_port: 8080
deploy_dir: /opt/apps/{{ app_name }}
jar_version: "{{ lookup('env', 'DEPLOY_VERSION') }}"
pre_tasks:
- name: Create deploy directory
file:
path: "{{ deploy_dir }}"
state: directory
owner: appuser
group: appuser
- name: Backup current jar
command: "cp {{ deploy_dir }}/{{ app_name }}.jar {{ deploy_dir }}/{{ app_name }}.jar.bak.{{ ansible_date_time.iso8601 }}"
args:
creates: "{{ deploy_dir }}/{{ app_name }}.jar.bak.{{ ansible_date_time.iso8601 }}"
tasks:
- name: Upload new jar
copy:
src: "../build/libs/{{ app_name }}-{{ jar_version }}.jar"
dest: "{{ deploy_dir }}/{{ app_name }}.jar"
owner: appuser
group: appuser
mode: '0644'
- name: Restart service
systemd:
name: "{{ app_name }}"
state: restarted
daemon_reload: yes
- name: Wait for service to be ready
uri:
url: "http://localhost:{{ app_port }}/actuator/health"
status_code: 200
retries: 12
delay: 5
register: health_check
until: health_check.status == 200
post_tasks:
- name: Clean old backups
file:
path: "{{ deploy_dir }}/{{ app_name }}.jar.bak.*"
state: absent
args:
age: 7d
recurse: yes
这个 playbook 有几个设计点:
- 备份带时间戳:
creates参数确保同时间戳不会重复备份,避免磁盘被旧备份撑满 - 健康检查重试:
retries: 12+delay: 5意味着最多等一分钟,避免服务还没启动完就报告失败 - 清理旧备份:
age: 7d只保留最近七天的备份,防止历史堆积
一个常见的误解:template 模块只用于渲染配置文件,但实际上 copy 模块也可以渲染 Jinja2 模板,区别在于 template 始终走模板引擎,copy 只在文件是模板时才渲染。对于简单的环境变量替换,用 template 更直观。
问题排查:Ansible 的报错往往不够直观
SSH 连接失败
UNREACHABLE! => {
"changed": false,
"msg": "Failed to connect to the host via ssh.",
"unreachable": true
}
排查顺序:
- 手动
ssh target_host确认可达 - 检查
/etc/ansible/hosts里的ansible_host是否正确 - 确认 SSH 端口不是默认的 22,需要在 inventory 里指定
ansible_port - 检查私钥路径,
ansible_ssh_private_key_file或~/.ssh/id_rsa是否存在
权限不足
chmod: changing permissions of '/opt/app': Operation not permitted
确认 playbook 里有 become: yes,且执行用户有 sudo 权限。有时需要在 inventory 里指定 ansible_become_pass,或在主控机配置免密 sudo。
模块找不到
ERROR! couldn't resolve module/module_uri 'apt'
目标机没有安装 Python,或者 Python 版本过旧(< 2.6 或 >= 3.9)。Ansible 要求目标机有 Python,对于不包含 Python 的设备(如路由器),需要使用 raw 模块通过 SSH 直接执行命令。
并发导致的文件竞争
ERROR! Attempting to reconstruct a split diff failed because the original source file wasn't found
多个 playbook 同时修改同一文件时会冲突。解决方案:
- 降低
--forks数量 - 使用
serial关键字让 playbook 分批执行 - 改用锁定机制(
delegate_to到一台机器上统一管理)
适用边界:什么时候不该用 Ansible
Ansible 擅长的是配置管理和应用部署,不适用于:
- 实时配置推送:Ansible 是拉模式,需要主动触发。如果需要配置变更实时同步,不适合
- 大规模动态环境:容器、Serverless 等场景下机器生命周期很短,Ansible 的静态 inventory 跟不上变化
- 复杂的条件逻辑:虽然支持
when条件和循环,但复杂业务逻辑还是应该在应用代码里处理 - 长连接场景:Ansible 每个任务都是短连接,不适合需要保持长连接的监控或日志收集
对于这些场景,Kubernetes Operator、Consul、或专用的配置中心更合适。
总结
Ansible 的核心价值在于:把重复的人工操作变成可复现、可审计、可回滚的流程。从五台机器到五百台,playbook 几乎不需要改动,只需要调整 inventory 和并发度。
真正的难点不在于工具本身,而在于:
- 幂等性设计:每次执行结果应该一致,不会因为重复执行产生副作用
- 错误处理:一条命令失败时,后续操作是继续还是中止,需要在 playbook 里明确
- 版本管理:playbook 和 inventory 应该和代码一起进入版本控制,避免「这台机器是谁改的」类问题
工具不会解决所有问题,但能消除机械劳动中的犯错空间。对于批量运维而言,这个价值已经足够。

浙公网安备 33010602011771号