聊到 Ansible,很多刚开始接触自动化运维的朋友都会问一句:它到底是什么,和一堆 shell 脚本有什么区别,值不值得花时间学。Ansible 的定位其实很朴素——它是一套基于 SSH 的自动化工具,能帮你批量执行命令、下发配置、部署应用、编排复杂任务,而且用的是人人都能读懂的 YAML 文件。这一篇是 Ansible 自动化介绍的第一篇,我会从选型思路、核心概念、环境搭建、实际写剧本到常见问题排查,把 Ansible 从零开始讲清楚,适合刚入行运维、想从手动操作转向自动化的同学,也适合已经在写脚本但想统一管理、提升效率的从业者。
我自己用 Ansible 差不多五年,中间也踩过不少坑。到了今天,团队里新装一台服务器、调整一个 nginx 配置、批量创建用户、甚至滚动发布都靠它。这不是因为它最“时髦”,而是因为它足够简单,简单到可以让你在半小时内跑通第一个 playbook,同时又有足够的深度撑起复杂生产环境。下面我会按照从思路到实操的顺序,尽量把每一个决策背后的原因也讲明白,而不是只给你堆命令。
1. 为什么是 Ansible:自动化运维的选型思考
1.1 从手动运维到自动化:痛点与转变
回忆一下没有自动化工具时的典型场景:你接手了三十台服务器,需要给每台机器创建一个普通用户、安装 nginx、把配置文件放上去、再启动服务。第一种做法是写一个 shell 脚本,用 for 循环 + ssh 批量执行。听起来很高效,但实际跑几轮就会遇到各种问题:某台机器上的 ssh 需要输入密码、某些机器上软件源不同导致安装失败、某台机器之前残留了旧版本配置文件、脚本跑了一半崩溃,你根本不知道哪些机器已经执行、哪些还没执行。更麻烦的是,这个脚本如果换一个人来维护,他完全看不懂你当时的逻辑,更不敢随便改。
Ansible 的出现解决的正是这一长串问题。它不需要你预先设计复杂的分布式结构,你只需要一台控制机,通过 SSH 去连接所有目标机器。目标机器不需要安装 agent,不需要被推任何额外软件,这在很多企业网络环境下是非常大的优势。因为你不需要审批每一台机器的“是否允许安装代理”,也不需要操心 agent 版本不一致的问题。
从手动运维转变为 Ansible 驱动的自动化,本质上是把“对单台机器操作”变成“对整个集群状态进行声明”。你思考的不再是“我现在要在 10.0.0.5 上执行什么命令”,而是“这批机器最终应该处于什么状态”。Ansible 负责把这套期望状态变成具体操作,并且在重复执行时不会产生副作用。这也是 Ansible playbook 和传统 shell 脚本之间最大的差异:playbook 描述的是目标状态,脚本描述的是操作过程。
1.2 对比其他自动化工具:Puppet、SaltStack 与自定义脚本
在选择 Ansible 之前,我也认真对比过其他方案。这里我可以直接说结论:Ansible 不是所有场景的最优解,但对于大多数运维团队来说,是学习成本最低、落地速度最快的选择。
拿 Puppet 来说,它是最老牌的配置管理工具,有声明式语言、有服务端和客户端方案,模型很成熟,适合超大规模(上万台)的配置管理。但 Puppet 的语言对新手不太友好,需要额外学习一套 DSL,而且它的 Agent 模式天然要求所有被管节点都装上 agent,这在混合云、容器化环境里会显得很笨重。
SaltStack 的极速通信是它的卖点,支持 ZeroMQ 消息队列,秒级推送命令到几千台机器很惊艳。但同样的,它也使用 agent 模式,而且状态管理用的 SLS 文件格式,缩进和语法稍有不注意就造成问题,排错体验一般。
还有人会说“我直接写 shell + parallel-ssh 就够了”。这种方案适合一次性紧急操作,但不适合沉淀长期可维护的自动化资产。你的命令是基于特定环境的,换一台机器、换一个系统版本就可能失效;脚本缺少幂等性设计,跑第二次可能结果完全不同;剪贴板里到处找命令,很难形成“配置即代码”的资产沉淀。
我用下来,Ansible 最大的竞争力可以归纳成四点:第一,无 agent,只要 SSH 能通就行;第二,语法是 YAML,写出来的 playbook 本身就是很好的文档,非开发同事也能看懂;第三,官方和社区模块极其丰富,从系统服务、包管理、文件操作到云厂商 API 都有现成模块;第四,执行是幂等优先,大部分模块会先检查当前状态,只有需要变更时才动手。
这里给一个相对直观的对比表格,方便你做选型参考:
| 维度 | Ansible | Puppet | SaltStack | Shell 脚本 |
|---|---|---|---|---|
| 是否需要 Agent | 不需要 | 需要 | 需要 | 不需要 |
| 配置语言 | YAML | Puppet DSL | YAML/SLS | Shell |
| 幂等性 | 强 | 强 | 强 | 需自行实现 |
| 学习曲线 | 低 | 高 | 中 | 低 |
| 适用规模 | 中小到大型 | 超大型 | 中到大型 | 小型 |
| 适合场景 | 配置管理、部署、编排 | 大规模配置一致性 | 快速批量命令 | 临时操作 |
2. 核心概念拆解:Inventory、模块、Playbook 到底在讲什么
2.1 Inventory:你的资产清单怎么维护
Ansible 操作的所有机器,都要先写进一个清单文件,也就是 Inventory。默认位置是/etc/ansible/hosts,但更推荐在项目目录里建一个独立的 inventory 文件,跟随项目走,方便不同环境区分管理。
Inventory 文件最简单的形式就是 IP 地址列表,按组归类:
[web] 192.168.10.10 192.168.10.11 [db] 192.168.11.20 192.168.11.21你可以用中括号定义主机组,然后在运行指令时按组操作。比如ansible web -m ping就只对 web 组执行 ping 模块。如果想给特定机器设置连接参数,可以用带冒号的形式:
[web] 192.168.10.10 ansible_user=root ansible_port=2200也可以定义共享变量,比如所有 web 机器的用户都是 deploy:
[web:vars] ansible_user=deploy除了静态文件,Ansible 还支持动态 Inventory,也就是从云平台 API、调研系统、CMDB 自动获取主机列表。生产环境维护一个动态 Inventory 是很好的实践,因为你的机器资产是流动的,人工维护静态清单迟早会过时。不过刚入门阶段,从静态文件开始就够了,把分组逻辑和变量习惯先建立起来。
2.2 模块:不需要写 shell 也能干活的“现成工具箱”
模块是 Ansible 最核心的组成。你写 playbook 时,每一次实际“干活”的动作,其实都是调用一个模块。模块封装了具体操作,并且实现幂等逻辑。比如copy模块负责拷贝文件,如果目标文件已经存在且内容和源文件一致,它就不会执行拷贝,结果是changed: false;如果内容不一致,才覆盖,结果是changed: true。
刚开始使用 Ansible 时,最忌的一点是依然保持 shell 思维,什么操作都想用command或shell模块去执行原始命令。比如你想创建用户,很多人第一直觉是useradd,但 Ansible 里有user模块;你想安装软件,第一时间应该想到yum模块或apt模块,而不是用 shell 去执行yum install -y。为什么?因为高级模块是经过大量验证的,它处理了跨平台差异,并且天然支持幂等,你不需要自己去判断软件是否已安装。如果你一直在用shell模块,那 Ansible 对你来说只是一个“批量 ssh 工具”,而不是真正的配置管理工具。
几个日常操作里高频使用的模块,我整理如下:
| 模块名 | 作用 | 常用参数 |
|---|---|---|
| ping | 连通性测试,实际上是运行 Python 判断能否返回 pong | 无 |
| copy | 本地到远程的文件复制并管理权限 | src, dest, owner, group, mode |
| file | 管理文件、目录、符号链接的路径状态 | path, state, mode, src |
| user | 管理本地用户账号 | name, uid, group, shell, state |
| yum / apt | 对应系统的包管理 | name, state, update_cache |
| service / systemd | 管理系统服务 | name, state, enabled, daemon_reload |
| template | 用 Jinja2 模板渲染配置并下发 | src, dest, owner, mode |
| get_url | 从 URL 下载文件 | url, dest, checksum |
| command / shell | 执行任意命令 | argv / cmd, chdir, creates |
2.3 Playbook:把操作变成可读剧本
Playbook 是 Ansible 的“剧本”,它使用 YAML 语法,描述了一批主机上要执行的多个任务。一个 playbook 里可以有一个或多个 play,每个 play 指定要操作哪些主机、以什么用户身份执行、执行哪些任务。
一个最简单的 playbook 长这样:
--- - name: Ensure nginx is installed hosts: web become: yes tasks: - name: Install nginx yum: name: nginx state: present我这里解释一下这个文件里每个字段的含义:hosts指定目标主机组,become: yes表示执行任务时切换到 root 权限(等价于 sudo),tasks是任务列表,每个任务都有一个名字和实际调用的模块。执行时 Ansible 会按照 tasks 中的顺序逐个执行,并将每项结果打印在屏幕上,如果某个任务失败,默认会中止整个 playbook。
Playbook 的价值在可读性。团队成员看到Install nginx这个任务名,不需要进入模块内部就知道它想干什么。这比一长串 shell 命令好理解得多。更重要的是,playbook 可以表达“编排”逻辑,比如先安装数据库、再部署前端、最后重启服务,这些步骤之间有先后依赖,通过 playbook 可以清晰编排。
3. 从零搭建第一个可用环境:控制端配置与免密登录
3.1 控制端安装与主机清单准备
Ansible 采用控制端连接被管端的架构。控制端一般是一台 Linux 机器,也可以是 macOS。Windows 上也能通过 WSL 使用,但正式环境不建议。被管端则几乎可以是任何支持 SSH 的系统,包括 Linux、Unix、macOS,以及 Windows(配合 WinRM)。
安装 Ansible 本身非常简单。在 RHEL/CentOS 类的机器上,推荐用 EPEL 源:
yum install epel-release -y yum install ansible -y在 Debian/Ubuntu 上,可以直接用官方 PPA 或 apt 安装:
apt update apt install ansible -y如果你是本机有 Python 环境的开发者,也可以通过 pip 安装:
pip install ansible安装完成后,验证版本:
ansible --version输出中会显示 ansible 版本号,以及 Python 路径。注意配置文件 config file 的位置,默认是/etc/ansible/ansible.cfg。我建议在自己的项目目录下创建一个ansible.cfg,这样项目可以很干净地隔离。
然后在项目目录创建 inventory 文件。我习惯用inventory.ini这个名字,内容如下:
[web] 192.168.10.10 192.168.10.11 [web:vars] ansible_user=deploy用ansible_user=deploy而不是 root,是因为生产环境通常禁止 root 直接 ssh 登录,用普通用户加 sudo 更符合安全最佳实践。
3.2 基于密钥的 SSH 免密配置
Ansible 基于 SSH 工作,如果你每次连接都需要输入密码,那再好的自动化体验也会变得非常糟糕。所以第一步就是把控制端到被管端的免密登录配置好。
使用密钥认证的好处不只是免密输入,更重要的是安全性比密码认证高得多,同时不会因为不同机器的密码不一致而断开连接。
配置流程是在控制端生成密钥对:
ssh-keygen -t ed25519一路回车即可,默认生成在~/.ssh/id_ed25519和~/.ssh/id_ed25519.pub。如果已经有了历史密钥,不想覆盖,也可以指定不同文件名。
然后使用ssh-copy-id将公钥复制到目标机器:
ssh-copy-id deploy@192.168.10.10 ssh-copy-id deploy@192.168.10.11这一步会要求你输入一次 deploy 用户的密码,之后登录就不需要密码了。如果目标机器上 deploy 用户有 sudo 权限,那么配合上become: yes即可完成所有需要 root 权限的操作。
有个容易被忽略的细节:如果你的 SSH 服务监听在非 22 端口,或者网络环境里配置了跳板机,需要在 inventory 里为每台机器设置ansible_port或使用~/.ssh/config配置别名。Ansible 会尊重 ssh config 里的配置,这也是一个比较取巧且实用的管理方式。
3.3 第一个命令:ping 模块与 ad-hoc 命令
一切准备好后,可以先跑一个最简单的命令验证连通性:
ansible all -i inventory.ini -m ping这里的-i指定了 inventory 文件,-m ping表示调用 ping 模块。正常输出类似:
192.168.10.11 | SUCCESS => { "changed": false, "ping": "pong" }看到SUCCESS和"ping": "pong"就代表这台机器通了。有一点要注意,这个 ping 模块不是普通的 ICMP ping,它是通过 SSH 发送一个 Python 命令到目标机器,目标机器返回结果后确认通信链路通畅,同时能验证目标机器的 Python 环境也能正常执行。
除了 ping,你还可以使用 ad-hoc 模式快速执行单条任务,比如查看所有 web 机器上的磁盘空间:
ansible web -i inventory.ini -m shell -a "df -h"ad-hoc 适合做临时操作,但如果操作频繁使用,还是要整理成 playbook。因为 ad-hoc 没有留下任何可追溯的记录,也不便于后续维护。
3.4 写一个最简单的 playbook:安装 nginx 并启动它
现在进入正题,用 playbook 完成一个真实的自动化运维场景:在 web 组所有机器上安装 nginx、将配置目录准备好、启动服务并设为开机自启。
在项目目录创建nginx.yml文件:
--- - name: Setup nginx on web servers hosts: web become: yes tasks: - name: Install nginx yum: name: nginx state: present when: ansible_os_family == "RedHat" - name: Create nginx config directories file: path: /etc/nginx/conf.d state: directory owner: root group: root mode: 0755 - name: Start nginx service service: name: nginx state: started enabled: yes然后执行:
ansible-playbook -i inventory.ini nginx.yml执行过程会显示每个任务的结果。第一次执行时,安装和启动都会触发任务,显示changed: 1之类的信息。第二次再执行,所有任务都应该是ok: 1,而不是changed,这就是幂等性的体现。
在上面这个 playbook 里我用到了when: ansible_os_family == "RedHat",这是一个条件判断。因为如果你后续把 Debian 系服务器也加入 web 组,用 yum 模块就没办法安装 nginx。这里可以配合ansible_os_family这个自动收集的变量,做到跨平台兼容。完善点做法是分别定义 yum 和 apt 的安装任务,这里只是展示条件判断的用法。
4. 实用进阶:变量、模板与角色
4.1 变量优先级和事实收集
当你开始管理几十上百台机器时,如果每台机器仅仅是 IP 不同,配置内容可以完全一致,那静态 playbook 还够用。但现实中不同角色机器配置不同、同角色内不同机器恢复不同,这时候就必须引入变量。
Inventory 里可以直接给组或主机定义变量:
[web:vars] http_port=8080Playbook 里也可以定义变量:
--- - hosts: web vars: http_port: 8080还有 vars_files,可以在单独的 YAML 文件里集中存放变量,比如按环境分文件:
vars_files: - vars/development.yml - vars/nginx.ymlAnsible 的变量优先级是一个非常经典的考点。默认情况下,变量可以从命令行、playbook、inventory、角色、系统 facts、环境变量等多个地方获得。它们之间存在优先级排序,规则比较复杂。我给新手一个简化但不误导的建议:尽量让同一种变量只在一个地方定义。如果必须覆盖,了解下面这个粗略先后顺序就够用:
- 命令行传参(
-e)优先级最高 - playbook 里 tasks 中定义的
vars优先级高于 inventory - inventory 里主机变量优先级高于组变量
- default 角色变量优先级最低
这样设计的好处是不容易踩坑。如果你非要在多处定义同名变量,运行结果可能会和你预期不一样,排查起来也费劲。
除了自定义变量,Ansible 还会自动收集目标机器的大量“事实”信息,叫 Facts。比如系统类型、发行版、IP 地址、CPU 架构、内存大小等。这些 facts 会在 playbook 内自动生成,以ansible_os_family、ansible_default_ipv4这样的方式引用。你可以用setup模块查看完整 facts:
ansible web -m setupfacts 非常有用,它能帮你写出更健壮的剧本。例如前面用到的ansible_os_family就是 facts 之一。不过收集 facts 会消耗额外时间,如果对性能敏感且不需要 facts,可以在 playbook 里加上gather_facts: no来关闭。
4.2 模板渲染(Jinja2)快速上手
配置管理里最刚需的技能就是模板渲染。你不再需要为每台机器手写一份 nginx 配置,而是写一个模板文件,用变量填充特殊内容。Ansible 的template模块依赖 Jinja2 模板引擎,这也是 Python 生态里非常成熟的模板系统。
举个例子,我想给每台 web 机器生成一个带有主机名和开放端口的 nginx 欢迎页配置。先在项目目录建立templates/nginx_welcome.conf.j2:
server { listen 80; server_name {{ ansible_hostname }}; location / { root /usr/share/nginx/html; index index.html; } }然后在 playbook 里添加任务:
- name: Deploy nginx welcome config template: src: templates/nginx_welcome.conf.j2 dest: /etc/nginx/conf.d/welcome.conf owner: root group: root mode: 0644 notify: reload nginx这里的notify: reload nginx是另一个知识:handler。handler 是只有在任务状态为changed时才会触发的特别任务。比如配置内容变了,才需要 reload nginx;如果配置没变,就不会触发 reload。这是避免无意义重启服务的关键手段。
对应 handler 需要在 playbook 里定义:
handlers: - name: reload nginx service: name: nginx state: reloadedJinja2 模板里的语法非常丰富,支持 if 判断、for 循环、过滤器等。举个例子,我想给不同环境配置不同的监听端口,可以结合变量:
server { listen {{ http_port }}; {% if environment == "prod" %} server_name {{ ansible_hostname }}.example.com; {% else %} server_name {{ ansible_hostname }}; {% endif %} }写模板时最需要注意的是变量不存在的情况。Jinja2 默认是 strict 模式关闭的,变量不存在会渲染成空字符串,这很难排查。建议在 playbook 中使用ansible.cfg里开启获取变量失败时的错误:
[defaults] error_on_undefined_vars = True4.3 角色与目录规范
当你把一个 playbook 越写越长,任务越来越多,就需要引入角色(Role)。角色本质是一套目录规范,把变量、任务、模板、文件、handler 分层存放,让整个自动化项目变得像标准化的“项目工程”。
一个标准角色目录结构如下:
roles/ common/ tasks/ main.yml handlers/ main.yml templates/ files/ vars/ main.yml defaults/ main.yml meta/ main.yml在 roles 下每个角色都有专门的职责。比如你有一个nginx角色,那么所有和 nginx 安装配置相关的任务都放在roles/nginx/tasks/main.yml中,所有模板都放在roles/nginx/templates/,变量放在roles/nginx/defaults和roles/nginx/vars。
使用角色时,playbook 就变得非常简洁:
--- - hosts: web roles: - common - nginx这样做的好处是清晰的复用。你可以在一个 playbook 中组合多个角色,也可以把常用角色抽离成单独仓库,供团队其他项目引用。角色里的defaults/main.yml用来定义默认值,优先级最低,适合被其他变量覆盖;vars/main.yml则是固定的、不希望被覆盖的重要变量。
刚开始接触角色时,很多同学容易把 defaults 和 vars 混用。我的建议是:所有需要被不同环境覆盖的值放进 defaults,所有固定值(如软件官方下载地址、固定参数字典)放进 vars。这样到后来你会省下很多调试时间的。
5. 常见问题与排查技巧实录
5.1 SSH 连接失败排查思路
Ansible 报错中最常见的就是 SSH 连接问题。典型报错是:
fatal: [192.168.10.10]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Permission denied (publickey,...)"}出现这个报错,我一般会按下面顺序排查:
第一,先用最原始的 ssh 命令测试手动连接:
ssh deploy@192.168.10.10如果手动连接也失败,那问题在 SSH 本身,不在 Ansible;如果手动连接成功,说明 Ansible 的连接参数或密钥使用有问题。
第二,确认 ssh 使用的是正确密钥。检查 inventory 中是否配置了ansible_ssh_private_key_file,或者~/.ssh/config里是否有对应主机段覆盖了默认密钥。
第三,检查是否用了 root 用户登录而服务器禁用了 root 登录。如果是这样,需要将 inventory 中的用户切换为有 sudo 权限的普通用户,并在 playbook 里加become: yes。
第四,检查端口。如果服务器 SSH 端口不是 22,要在 inventory 中用ansible_port明确指定。
还有一个经常被忽略的 host key 问题。新环境首次连接时,Ansible 会因为未验证 host key 而提示,然后直接失败。如果你非常确定目标机器是可信任的,可以在ansible.cfg中设置:
[defaults] host_key_checking = False但这句话在安全要求高的环境里要谨慎使用。更好的做法是使用ssh-keyscan提前将 target 机器的 host key 加入 known_hosts:
ssh-keyscan 192.168.10.10 > ~/.ssh/known_hosts5.2 JSON 解析错误:module result deserialization failed
这个报错在搜索热词里出现频率很高,我来讲讲。完整报错类似:
module result deserialization failed: no start of json char found ansibleAnsible 实现模块的方式,是先把一个 Python 脚本发送到目标机器上执行,该脚本会把执行结果以 JSON 格式输出到 stdout,控制端解析这个 JSON。如果目标机器输出的内容不是 JSON 开头,就出现了上述解析错误。
常见原因有三个。
第一个原因,目标机器上默认的 python 环境被“污染”了。比如/usr/bin/python指向的是一个被 conda 或 pyenv 特殊包装的版本,启动时会输出 banner 或额外字符串,把 JSON 输出挤到了非开头位置。查这个情况,可以在目标机上执行:
python -c "print('hello')"如果输出不干净,比如有 conda 环境提示、有Python 2.7.18之类,说明 python 环境有问题。解决方法是在 inventory 里明确指定 Ansible 使用哪个 python 解释器:
[web:vars] ansible_python_interpreter=/usr/bin/python3第二个原因是目标机器没有安装合适的 Python。Ansible 2.8 之后虽然很多模块支持免 python 也可以操作,但核心模块依然需要 Python 环境。如果系统极简,没有/usr/bin/python,Ansible 会自动查找其他 python。找不到时也会报类似错误。这时候在目标机器上安装python3并设置解释器路径,问题就解决了。
第三个原因是被管端 sudo 时输出被污染。有的系统配置了secure_path,导致非登录 shell 下 python 的路径不同。可以在 playbook 里设置:
- hosts: web become: yes vars: ansible_become_exe: sudo env PATH=$PATH:/usr/local/bin这样 sudo 执行时会保留原 PATH,避免 python 找不到或加载错误脚本。
5.3 幂等性陷阱与操作习惯
最后一个想认真聊的坑,是幂等性陷阱。Ansible 的理念是“声明目标状态”,但并不是所有模块天然幂等。最典型的就是command和shell模块。比如你用 shell 执行一条echo "hello" >> /tmp/hello.txt,第一次执行结果会追加内容,第二次执行还会继续追加。这不是 Ansible 出了问题,而是命令本身不是幂等的。
怎么改进?核心思路是让命令在执行前先判断当前状态,符合条件就不执行。例如shell模块有一个creates参数,只有当指定文件不存在时才执行命令:
- name: Download package only if not exists shell: wget -O /tmp/pkg.tar.gz http://example.com/pkg.tar.gz args: creates: /tmp/pkg.tar.gz另外尽量用专用模块代替裸命令。比如创建用户用user模块,下载文件用get_url模块,这些模块内部会做状态检查。
还有一个容易忽略的坑是文件权限。每次copy模块执行时,Ansible 都会对比文件内容和权限,这很好。但如果你只指定了owner却没指定group,Ansible 不会自动帮你更新 group,因为它认为 group 不是任务声明的目标状态。如果你希望 group 也一致,就要显式写出来。
最后,我强烈建议在 playbook 中开启日志记录。在ansible.cfg里配置:
[defaults] log_path = /var/log/ansible.log这个日志记录会在排障时救你命。毕竟自动化跑完之后,你很难靠回忆判断当时发生了什么。有了 log,我们能快速定位是哪一条任务、在哪台机器上、执行了什么命令、结果如何。
如果你正在从手动运维向自动化运维转型,Ansible 是我会推荐的第一套工具,因为你不必先成为编程高手,就能写出一份可复现、可维护的自动化剧本。我在多次团队落地和运维排障之后最深的感受是:衡量一套自动化方案好不好,并不在于第一次执行是否顺利,而在于它能不能被反复执行而依旧稳定、能不能让后来者通过读代码就理解系统的状态。这一点 Ansible 做得相当好。如果你刚开始接触,建议先在测试环境把整个流程跑通,再慢慢往生产环境推广。后面我还会继续写 Ansible 系列更深入的内容,比如大型 playbook 的组织结构、动态 inventory、甚至结合 CI/CD 的使用,希望这篇介绍能成为你入门的起点。