当服务器从三台变成三十台,运维的工作方式必须换。以前登录每台机器改配置、重启服务,一个人还能忙得过来;三十台机器时,同样的操作重复三十遍,不仅浪费时间,还容易漏掉一台、或者某台命令敲错。真正的痛点在于:你不知道三十台机器的配置是不是完全一致,出了问题也很难复现。Ansible 就是解决这类问题的工具,它让你用一份描述文件,把配置一次性推到所有机器,而且推完还能保证结果一致。
这篇文章讲清 Ansible 的核心概念:为什么它不需要在被管理机上装 Agent、inventory 清单怎么组织、ad-hoc 命令怎么用、playbook 怎么写、常用模块有哪些、roles 怎么组织代码,最后给一个多机批量部署的完整示例。贯穿全文的一个关键词是"幂等性"——同一个 playbook 跑一遍和跑十遍结果一样,这是自动化工具和普通脚本最本质的区别。
无 Agent 架构:为什么它这么轻
Ansible 不需要在被管理机安装任何常驻程序,它靠 SSH 连上去,把一段临时的 Python 脚本推过去执行,执行完就删掉。这就是它最吸引人的地方:只要目标机有 SSH 和 Python,什么都不用装就能管。
对比一下主流的配置管理工具,各自的定位差异很明显:
| 工具 | 架构 | 学习成本 | 幂等 | 适合场景 |
|---|---|---|---|---|
| Ansible | 无 Agent,SSH 推送 | 低(YAML) | 是 | 中小规模,快速上手 |
| Puppet | C/S,需装 Agent | 中高(自有 DSL) | 是 | 大规模,强约束 |
| Chef | C/S,需装 Agent | 高(Ruby) | 是 | 开发主导的复杂环境 |
| SaltStack | C/S 或 SSH | 中 | 是 | 大规模,秒级下发 |
| Shell 脚本 | 无 | 最低 | 否 | 一次性任务,小规模 |
Ansible 的优势是"无 Agent + 幂等 + YAML 易读"。它的劣势在大规模场景会显现:SSH 逐台连接,几千台并发时对控制机压力大(虽然可以调 forks 参数),实时性不如 SaltStack 的常驻通道。所以几百台以内,Ansible 是性价比最高的选择。
安装很简单,控制机装上即可:
# RHEL、Rocky、CentOS
sudo dnf install -y epel-release
sudo dnf install -y ansible
# Debian、Ubuntu
sudo apt update
sudo apt install -y ansible
ansible --version
被管理机只需要有 SSH 服务和 Python(大多数发行版默认就有,极简系统可能需要 dnf install python3 或 apt install python3)。
inventory:管理哪些机器
inventory 是 Ansible 的机器清单,定义有哪些主机、怎么分组。最简单的写法是一个 INI 文件 /etc/ansible/hosts:
[web]
web-01 ansible_host=192.0.2.20
web-02 ansible_host=192.0.2.21
[db]
db-01 ansible_host=192.0.2.30
[all:vars]
ansible_user=ops
ansible_ssh_private_key_file=/home/ops/.ssh/id_rsa
[web:vars]
ansible_port=22
方括号里是组名,可以嵌套([web:children]),也可以用 [all:vars] 给所有主机设公共变量。生产环境建议用独立的 inventory 目录:
inventory/
├── production/
│ ├── hosts # 清单
│ └── group_vars/
│ ├── all.yml # 所有主机变量
│ └── web.yml # web 组变量
└── staging/
└── hosts
主机变量还可以单独放 host_vars/web-01.yml。这个结构清晰,配合 Git 管理非常方便。测试连通性用 ping 模块(这里的 ping 是 SSH 连通性检查,不是 ICMP):
ansible all -i inventory/production/hosts -m ping
ansible web -i inventory/production/hosts -m ping
返回 "ping": "pong" 就说明通。
风险提示: Ansible 的 SSH 私钥等于所有被管理机的钥匙,权限必须严格限制(chmod 600),不要放在共享目录或提交进 Git。控制机本身是高价值目标,要做好访问控制和审计。后面批量命令的文章会详细讲 SSH 密钥的安全管理。
ad-hoc 命令:临时任务利器
不需要写文件,一条命令就能对一批机器执行操作,这叫 ad-hoc。基本格式是 ansible <主机模式> -m <模块> -a <参数>。
# 查看所有机器内存
ansible all -i hosts -m shell -a "free -h"
# 查看磁盘
ansible web -i hosts -m shell -a "df -h /"
# 重启所有 web 服务(用 systemd 模块,幂等)
ansible web -i hosts -m systemd -a "name=nginx state=restarted"
# 复制文件到所有机器
ansible all -i hosts -m copy -a "src=/tmp/app.conf dest=/etc/app/app.conf mode=0644"
# 安装软件包
ansible all -i hosts -m package -a "name=htop state=present"
# 查服务状态
ansible db -i hosts -m shell -a "systemctl is-active mysql"
shell 和 command 模块的区别要分清:command 不经过 shell,不支持管道、重定向、变量展开;shell 会调 /bin/sh,支持这些但安全性稍低(有注入风险)。能用专用模块就别用 shell,比如装包用 package、改文件用 copy、管服务用 systemd,专用模块才有幂等性,shell 没有。
常用参数:-f 20 设置并发进程数(默认 5),机器多时可以提高;-b 提权到 root(become);-K 交互输入提权密码;-u 指定远程用户;-o 输出压缩成一行。比如:
ansible all -i hosts -m shell -a "uptime" -b -f 20 -o
ad-hoc 适合临时排查、简单变更。一旦任务超过两三个步骤,就该写成 playbook,因为 ad-hoc 不可复用、不可版本管理,用完即弃。
playbook:把操作写成文件
playbook 是 YAML 格式的任务文件,是 Ansible 的核心。一个最基础的结构:
---
- name: 部署 Web 服务 # play 的名称
hosts: web # 目标主机组
become: yes # 提权
vars: # 变量
app_port: 8080
app_dir: /opt/myapp
tasks: # 任务列表,从上往下执行
- name: 安装依赖包
package:
name:
- nginx
- curl
state: present
- name: 创建应用目录
file:
path: "{{ app_dir }}"
state: directory
mode: '0755'
- name: 下发配置文件
template:
src: app.conf.j2
dest: "{{ app_dir }}/app.conf"
mode: '0644'
notify: 重启应用 # 文件变化才通知 handler
- name: 复制程序文件
copy:
src: files/app
dest: "{{ app_dir }}/app"
mode: '0755'
- name: 确保服务运行
systemd:
name: myapp
state: started
enabled: yes
handlers: # 被 notify 触发的任务,只在变化时执行
- name: 重启应用
systemd:
name: myapp
state: restarted
几个关键概念:
任务(tasks)按顺序执行,每个任务调用一个模块,name 是给人看的描述,出问题时日志里会显示。
handler 是一种特殊任务,只被 notify 触发,而且一个 play 里不管通知几次,handler 只在最后执行一次。这是配置管理的精髓:只有配置文件真的变了才重启服务,没变就不重启,避免无谓的重启。
变量用 {{ }} 引用,可以定义在 play 的 vars、inventory 的 group_vars、命令行 -e 或寄存器。变量优先级有讲究,实战中记住一条:命令行 -e 最高,适合覆盖。
模板用 Jinja2 语法,app.conf.j2 可以这样写:
server {
listen {{ app_port }};
server_name {{ ansible_fqdn }};
root {{ app_dir }}/public;
}
ansible_fqdn 是 Ansible 自动收集的事实变量(fact),不用自己定义就能用。执行前可以用 ansible web -m setup 查看某台机器收集到的所有 fact。
执行 playbook:
ansible-playbook -i inventory/production/hosts deploy.yml
# 检查语法
ansible-playbook --syntax-check deploy.yml
# 试运行,不改动任何东西
ansible-playbook -i hosts deploy.yml --check
# 指定变量
ansible-playbook -i hosts deploy.yml -e "app_port=9090"
--check 是写 playbook 时最该常用的参数,它模拟执行,告诉你"将会改什么",不真正动手。配合 --diff 还能看文件具体差异:
ansible-playbook -i hosts deploy.yml --check --diff
常用模块速查
| 模块 | 作用 | 常用参数 | 幂等 |
|---|---|---|---|
copy |
复制文件 | src、dest、mode、owner | 是 |
template |
渲染模板 | src、dest、mode | 是 |
file |
管理文件/目录/链接 | path、state、mode | 是 |
package |
通用包管理 | name、state | 是 |
yum/dnf/apt |
指定包管理器 | name、state | 是 |
systemd |
管理服务 | name、state、enabled | 是 |
service |
通用服务管理 | name、state、enabled | 是 |
user |
管理用户 | name、state、groups | 是 |
lineinfile |
修改某一行 | path、regexp、line | 是 |
command |
执行命令 | cmd、creates | 否 |
shell |
执行 shell 命令 | cmd | 否 |
uri |
HTTP 请求 | url、method | 否 |
git |
拉代码 | repo、dest、version | 是 |
用 package 而不是 yum/apt 的好处是跨发行版通用,Ansible 会自动选择。但有时需要发行版特有参数(比如 yum 的 enablerepo),就得用具体模块。
command 和 shell 没有幂等性,每次都会执行。如果确实要用,加 creates 或 removes 参数来获得一定幂等性:
- name: 初始化数据库(仅当标记文件不存在时)
command: /opt/myapp/init_db.sh
args:
creates: /var/lib/myapp/.initialized
这样只有 .initialized 文件不存在时才执行,避免了重复初始化。
roles:把代码组织起来
playbook 一多,公共逻辑(装包、配时区、开防火墙)会到处重复。roles 就是把这些可复用的部分封装成标准结构:
roles/
└── nginx/
├── tasks/main.yml # 任务入口
├── handlers/main.yml # 处理器
├── templates/ # 模板文件
├── files/ # 静态文件
├── vars/main.yml # 变量(优先级高)
├── defaults/main.yml # 默认变量(优先级低,可被覆盖)
└── meta/main.yml # 依赖关系
在 playbook 里引用:
---
- name: 配置 Web 服务器
hosts: web
become: yes
roles:
- common # 公共基础配置
- nginx
- name: 配置数据库
hosts: db
become: yes
roles:
- common
- role: mysql
vars:
mysql_port: 3306
defaults/main.yml 和 vars/main.yml 的区别常被问:defaults 优先级最低,适合放"希望被用户覆盖"的默认值;vars 优先级高,适合放"不希望被随意改"的固定值。写角色时把可配置项放 defaults。
role 也可以通过 Galaxy 下载社区现成的:
ansible-galaxy install geerlingguy.nginx
ansible-galaxy list
用别人的 role 能省时间,但要看清它做了什么、是否还在维护、是否引入意外变更,别盲目用。
批量部署完整示例
假设要把一个应用部署到十台 web 机,包含:装依赖、建用户、下发配置、部署程序、启动服务。用 playbook 写:
---
- name: 批量部署 Web 应用
hosts: web
become: yes
serial: 2 # 每次只操作 2 台,滚动更新
vars:
app_user: myapp
app_dir: /opt/myapp
app_port: 8080
tasks:
- name: 安装系统依赖
package:
name:
- nginx
- python3
state: present
- name: 创建应用用户
user:
name: "{{ app_user }}"
shell: /sbin/nologin
system: yes
- name: 创建目录
file:
path: "{{ item }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'
loop:
- "{{ app_dir }}"
- "{{ app_dir }}/logs"
- name: 下发配置
template:
src: templates/app.conf.j2
dest: "{{ app_dir }}/app.conf"
owner: "{{ app_user }}"
mode: '0644'
notify: restart app
- name: 部署二进制
copy:
src: files/app
dest: "{{ app_dir }}/app"
owner: "{{ app_user }}"
mode: '0755'
notify: restart app
- name: 启动服务
systemd:
name: myapp
state: started
enabled: yes
handlers:
- name: restart app
systemd:
name: myapp
state: restarted
这里 serial: 2 让 Ansible 每次只处理两台机器,全部成功才继续下一批。这是发布时的正确姿势:万一新版有问题,只影响少量机器,能及时停下。配合 max_fail_percentage: 20 可以在失败超过两成时自动中止。
loop 是循环,{{ item }} 是当前项。老版本用 with_items,现在推荐 loop。
执行并观察结果:
ansible-playbook -i inventory/production/hosts deploy.yml --check --diff # 先试运行
ansible-playbook -i inventory/production/hosts deploy.yml # 正式执行
输出里每个任务是 ok(无需变动)、changed(发生了变动)、skipped(跳过)还是 failed(失败)。幂等性的验证方式就是:第一次跑有一堆 changed,再跑一次应该几乎全是 ok。如果第二次还有大量 changed,说明某个任务不幂等,需要修。
常见问题
Ansible 连接报 "Permission denied" 或超时怎么办?
先手动用同样的用户和密钥 ssh 到目标机,确认能通。常见原因:密钥权限太开放(600 才对)、目标机 authorized_keys 没放对、SSH 端口不是默认 22、防火墙挡了、become 需要密码但没加 -K。可以加 -vvv 看详细调试信息。
为什么第二次跑 playbook 还是有一堆 changed?
一般是用了 command/shell 这类不幂等的模块,或者模板/文件的 mode、owner 没对齐导致每次都被重写。给 command 加 creates 参数,检查模板内容是否每次都变(比如里面嵌了时间戳),都能解决。幂等是自动化质量的核心指标,值得花时间修。
怎么只对部分主机执行?
用 --limit 参数,或主机模式匹配:
ansible-playbook -i hosts deploy.yml --limit "web-01"
ansible-playbook -i hosts deploy.yml --limit "web-01:web-02"
ansible all -i hosts -m shell -a "hostname" --limit "@retry"
遇到失败时,Ansible 会生成一个 .retry 文件,用 --limit @文件 可以只对失败的主机重试,非常实用。
生产环境怎么控制密钥和敏感变量?
敏感变量(数据库密码等)用 ansible-vault 加密:
ansible-vault encrypt group_vars/all/secrets.yml
ansible-playbook -i hosts deploy.yml --ask-vault-pass
密码文件本身不要提交进 Git。SSH 私钥也要严格权限管理,控制机的访问权限本身就是安全边界。
总结
- Ansible 无 Agent,靠 SSH 加临时 Python 脚本工作,只要目标机有 SSH 和 Python 就能管,学习成本低。
- inventory 定义机器和分组,配合
group_vars/host_vars组织变量,建议纳入 Git 管理。 - ad-hoc 用于临时任务,三步骤以上的操作就该写成幂等的 playbook,能用专用模块就别用 shell。
- handler 是配置管理的精髓,只在文件真正变化时重启服务,避免无谓重启。
- roles 把可复用逻辑封装成标准结构,defaults 放可覆盖的默认值,vars 放固定值。
- 幂等性是衡量自动化质量的核心——第一次 changed 一堆,第二次应几乎全 ok。
- 发布用
serial做滚动更新,失败及时中止,--check --diff先试运行再动手。
企业QQ咨询




