
聊到 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 都有现成模块第四执行是幂等优先大部分模块会先检查当前状态只有需要变更时才动手。这里给一个相对直观的对比表格方便你做选型参考维度AnsiblePuppetSaltStackShell 脚本是否需要 Agent不需要需要需要不需要配置语言YAMLPuppet DSLYAML/SLSShell幂等性强强强需自行实现学习曲线低高中低适用规模中小到大型超大型中到大型小型适合场景配置管理、部署、编排大规模配置一致性快速批量命令临时操作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_userroot ansible_port2200也可以定义共享变量比如所有 web 机器的用户都是 deploy[web:vars] ansible_userdeploy除了静态文件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, modefile管理文件、目录、符号链接的路径状态path, state, mode, srcuser管理本地用户账号name, uid, group, shell, stateyum / apt对应系统的包管理name, state, update_cacheservice / systemd管理系统服务name, state, enabled, daemon_reloadtemplate用 Jinja2 模板渲染配置并下发src, dest, owner, modeget_url从 URL 下载文件url, dest, checksumcommand / shell执行任意命令argv / cmd, chdir, creates2.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 权限等价于 sudotasks是任务列表每个任务都有一个名字和实际调用的模块。执行时 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_userdeploy用ansible_userdeploy而不是 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 deploy192.168.10.10 ssh-copy-id deploy192.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 -had-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_port8080Playbook 里也可以定义变量--- - hosts: web vars: http_port: 8080还有 vars_files可以在单独的 YAML 文件里集中存放变量比如按环境分文件vars_files: - vars/development.yml - vars/nginx.ymlAnsible 的变量优先级是一个非常经典的考点。默认情况下变量可以从命令行、playbook、inventory、角色、系统 facts、环境变量等多个地方获得。它们之间存在优先级排序规则比较复杂。我给新手一个简化但不误导的建议尽量让同一种变量只在一个地方定义。如果必须覆盖了解下面这个粗略先后顺序就够用命令行传参-e优先级最高playbook 里 tasks 中定义的vars优先级高于 inventoryinventory 里主机变量优先级高于组变量default 角色变量优先级最低这样设计的好处是不容易踩坑。如果你非要在多处定义同名变量运行结果可能会和你预期不一样排查起来也费劲。除了自定义变量Ansible 还会自动收集目标机器的大量“事实”信息叫 Facts。比如系统类型、发行版、IP 地址、CPU 架构、内存大小等。这些 facts 会在 playbook 内自动生成以ansible_os_family、ansible_default_ipv4这样的方式引用。你可以用setup模块查看完整 factsansible 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.j2server { 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 deploy192.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_hostsssh-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/pythonAnsible 会自动查找其他 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却没指定groupAnsible 不会自动帮你更新 group因为它认为 group 不是任务声明的目标状态。如果你希望 group 也一致就要显式写出来。最后我强烈建议在 playbook 中开启日志记录。在ansible.cfg里配置[defaults] log_path /var/log/ansible.log这个日志记录会在排障时救你命。毕竟自动化跑完之后你很难靠回忆判断当时发生了什么。有了 log我们能快速定位是哪一条任务、在哪台机器上、执行了什么命令、结果如何。如果你正在从手动运维向自动化运维转型Ansible 是我会推荐的第一套工具因为你不必先成为编程高手就能写出一份可复现、可维护的自动化剧本。我在多次团队落地和运维排障之后最深的感受是衡量一套自动化方案好不好并不在于第一次执行是否顺利而在于它能不能被反复执行而依旧稳定、能不能让后来者通过读代码就理解系统的状态。这一点 Ansible 做得相当好。如果你刚开始接触建议先在测试环境把整个流程跑通再慢慢往生产环境推广。后面我还会继续写 Ansible 系列更深入的内容比如大型 playbook 的组织结构、动态 inventory、甚至结合 CI/CD 的使用希望这篇介绍能成为你入门的起点。