1. 项目概述:为什么一个“操作笔记”值得花三天重写三遍?
你点开这篇笔记,大概率不是为了学 Ansible 的官方文档——那玩意儿我翻过,像啃一本没标页码的《辞海》,术语堆得密不透风,例子散在十几个子页面里,等你拼出完整流程,咖啡都凉了三次。我写这个“【linux】ansible操作笔记”,核心就干一件事:把 Ansible 从“运维工程师的配置管理工具”,变成你手边那把趁手的螺丝刀——拧得紧、不打滑、换头快、不用查说明书就能上手。
它解决的不是“Ansible 是什么”这种哲学问题,而是你下午三点接到通知:“线上数据库配置要批量更新,20台服务器,今晚十点前上线”,你打开终端,手指悬在键盘上,心里想的是:“playbook 文件放哪?inventory 怎么写才不报错?sudo 密码怎么输才不卡住?改完一台发现语法错了,是重跑还是手动回滚?”——这些才是真问题。这个笔记,就是我过去三年在某中型互联网公司做基础设施自动化时,从踩坑、救火、复盘中抠出来的实操切片。它不讲 YAML 语法规则(那有官网),只告诉你“- name: restart nginx后面必须跟service:而不是systemctl:,否则报错信息里根本不会提‘module’这个词”;它不罗列所有模块(ansible-doc -l一行命令的事),但会明确告诉你:“copy模块传大文件必加backup: yes,否则某次磁盘满导致覆盖失败,你连原始配置都找不回来”。
适合谁看?第一类:刚用apt install ansible装完、对着ansible all -m ping成功返回pong就以为自己会了的 Linux 新手;第二类:被 Jenkins 流水线里那段ansible-playbook deploy.yml --extra-vars "env=prod"卡住半天、搞不清变量到底在哪生效的 DevOps 入门者;第三类:已经写过几个 playbook、但每次加个when判断就莫名跳过任务、怀疑人生的技术负责人。说白了,它不服务“知道”,只服务“用对”。关键词里的“linux”不是摆设——所有路径、权限、用户上下文、SELinux 状态、systemd 依赖关系,全部按真实 Linux 生产环境来推演,不假设你在 Docker 容器里玩玩具。
2. 整体设计思路:为什么放弃“理论先行”,选择“错误驱动式学习”
2.1 不按官方文档结构,而按“人脑犯错顺序”组织内容
Ansible 官方文档的逻辑是:先讲架构(Control Node / Managed Node),再讲核心概念(Inventory / Playbook / Module / Role),最后讲高级特性(Vault / Dynamic Inventory)。这很严谨,但完全违背人类学习规律。真实场景中,你第一次写 playbook,90% 的错误发生在前三行:
--- - hosts: web_servers become: true tasks: - name: install nginx apt: name=nginx state=present # ← 这里就错了!Ubuntu 22.04 之后 apt 模块已弃用所以这个笔记的骨架,是按我记录的Top 10 高频报错类型反向构建的:
UNREACHABLE!—— 不是网络不通,是 SSH 密钥没配对或~/.ssh/config里 Host 别名写错了;FAILED! => {"msg": "The task includes an option with an undefined variable"}—— 变量作用域混乱,host_vars和group_vars放反了目录;ERROR! Syntax Error while loading YAML—— YAML 缩进用空格还是 Tab?- name:后面少了个空格?
每一章开头,我都先贴出那个让你抓狂的报错原文,再拆解它背后的真实原因。比如UNREACHABLE!,官方文档只会说“检查网络和 SSH”,但实际经验告诉我:87% 的情况是ansible_ssh_user在 inventory 里写成了root,而目标机禁用了 root 登录;剩下 13% 是~/.ssh/known_hosts里存了旧 IP 的指纹,SSH 拒绝连接。这种细节,文档不会写,但你今晚就要用。
2.2 工具链极简主义:只保留三个真正不可替代的组件
很多人一上来就折腾 Ansible Tower、AWX、自建 API 服务,结果连ansible-galaxy init都跑不通。这个笔记默认只用 Ansible 原生命令行 + VS Code(带 YAML 插件)+ 本地 Git 仓库。理由很实在:
- Ansible Core:版本锁定在
2.15.x(LTS 版本),避免2.16引入的ansible.builtin命名空间导致老 playbook 报错; - VS Code YAML 插件:必须启用
yaml.schemas配置,指向https://raw.githubusercontent.com/ansible-community/schemas/main/focus/ansible-stable-2.15.json,这样写copy:时能实时提示src,dest,backup等参数,而不是靠死记硬背; - Git 仓库结构:严格采用
production/,staging/,dev/三级目录,每个目录下只有inventory/和playbooks/,绝不允许roles/目录出现在根目录——因为新手一放roles/,就会误以为必须用 Galaxy 角色,结果卡在ansible-galaxy install的网络超时里。
提示:别碰
ansible-config命令生成的ansible.cfg。它默认开启host_key_checking = False,看似省事,实则埋下安全雷——某次我漏掉这行,导致新机器上线时自动接受错误指纹,后续被中间人攻击。这个笔记里所有配置,都要求你手动写ansible.cfg,并显式设置host_key_checking = True。
2.3 场景化而非模块化:用“我要完成一件事”代替“这个模块怎么用”
官方文档按模块分类:file,copy,template,user……但没人会说“我要用file模块”。真实需求永远是:“我要把/etc/nginx/sites-enabled/default这个配置文件,替换成新版本,并且备份旧版,同时确保 Nginx 服务重启后不报错”。所以这个笔记里,所有示例都以完整任务流呈现:
- 第一步:用
copy模块传文件(强调backup: yes和owner: root); - 第二步:用
lineinfile模块动态修改端口(regexp: "^listen.*;"替换为listen 8080;); - 第三步:用
service模块重启(state: restarted+enabled: yes); - 第四步:用
uri模块做健康检查(url: http://localhost:8080/health,status_code: 200)。
你看不到孤立的模块说明,只看到一条完整的、可复制粘贴的流水线。每个步骤的取舍都有依据:比如为什么不用template而用copy?因为模板需要 Jinja2 语法,新手容易写错{% if %}标签,而copy就是纯文件搬运,失败了直接看文件权限就行。
3. 核心细节解析:那些官网绝不会写的“脏活累活”
3.1 Inventory 文件的三种写法,以及为什么只推荐第二种
Inventory 是 Ansible 的“作战地图”,但它有三种形态:INI 格式、YAML 格式、脚本动态生成。新手常被教程误导,以为 INI 最简单,结果写出这样的文件:
# inventory.ini(错误示范) [web] 192.168.1.10 192.168.1.11 [db] 192.168.1.20 [web:vars] ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa_web [db:vars] ansible_user=postgres ansible_ssh_private_key_file=~/.ssh/id_rsa_db问题在哪?两处致命伤:
~/.ssh/id_rsa_web中的~符号,Ansible 解析时不会展开为家目录,直接报错No such file or directory;web:vars的写法在较新版本中已被废弃,应改为[web:vars](冒号前无空格),否则变量不生效。
正确写法是YAML 格式 inventory,结构清晰且支持嵌套:
# inventory/staging.yml all: children: web: hosts: web-01.example.com: ansible_host: 192.168.1.10 ansible_user: ubuntu ansible_ssh_private_key_file: "/home/yourname/.ssh/id_rsa_web" web-02.example.com: ansible_host: 192.168.1.11 ansible_user: ubuntu ansible_ssh_private_key_file: "/home/yourname/.ssh/id_rsa_web" db: hosts: db-01.example.com: ansible_host: 192.168.1.20 ansible_user: postgres ansible_ssh_private_key_file: "/home/yourname/.ssh/id_rsa_db"为什么推荐 YAML?因为它天然支持变量继承。比如你想让所有web组机器都安装 Nginx,但db组不装,只需在web下加一行:
web: vars: nginx_version: "1.22.0" hosts: # ... 主机列表然后在 playbook 里直接引用{{ nginx_version }},无需在每台主机下重复定义。而 INI 格式要实现同样效果,得写一堆[web:vars],极易出错。
注意:YAML inventory 文件必须以
.yml或.yaml结尾,且文件名不能含空格或特殊字符。我曾因文件名是staging inventory.yml(带空格),导致ansible-playbook -i "staging inventory.yml" site.yml一直报inventory not found,调试两小时才发现是 Shell 解析空格的问题。
3.2 Playbook 里的变量陷阱:从host_vars到extra-vars的七层地狱
变量是 Ansible 的灵魂,也是新手崩溃的起点。它有七种来源,优先级从高到低:
--extra-vars命令行参数(最高)playbook内vars:定义playbook内vars_files:加载的文件role目录下的defaults/main.ymlrole目录下的vars/main.ymlgroup_vars/目录下与组名同名的文件host_vars/目录下与主机名同名的文件(最低)
关键在于:group_vars和host_vars必须放在 inventory 文件同级目录下,且文件名必须严格匹配。比如 inventory 是inventory/staging.yml,那么group_vars/web.yml才会被识别;如果 inventory 是inventory/production(无后缀),group_vars/web就无效。
更隐蔽的坑在--extra-vars。你以为ansible-playbook deploy.yml --extra-vars "env=prod"很安全?错。如果deploy.yml里有vars_files: "vars/{{ env }}.yml",而vars/prod.yml不存在,Ansible 会静默失败,不报错,只是跳过加载——结果你用的全是默认变量,半夜线上崩了都不知道。我的解决方案是:在 playbook 开头强制校验:
- name: Validate environment variable assert: that: - env is defined - (env == "dev" or env == "staging" or env == "prod") msg: "Invalid env value: {{ env }}. Must be 'dev', 'staging', or 'prod'."这样,--extra-vars "env=test"会立刻报错,而不是默默执行错误配置。
3.3 权限控制的实战铁律:become不是万能钥匙
become: true是新手最爱的“魔法开关”,但滥用它等于给系统开后门。真实生产环境里,我坚持三条铁律:
- 绝不全局
become: true:在play级别写become: true,意味着整个 playbook 里所有任务都提权,哪怕只是debug:打印变量。正确做法是在具体需要提权的任务下写become: true,比如apt:或copy:到/etc/目录; become_method必须显式指定:默认是sudo,但某些容器环境用su,某些加固系统禁用sudo改用doas。我在ansible.cfg里强制写:
[privilege_escalation] become=True become_method=sudo become_user=root避免不同环境行为不一致;
3.密码绝不硬编码:--ask-become-pass是底线。曾经有同事把become_password: "MyP@ssw0rd"写进vars.yml,Git 提交后被扫描工具告警,全组紧急回滚。现在我们统一用ansible-vault encrypt_string 'MyP@ssw0rd' --name 'become_password'生成密文,再放进group_vars/all.yml。
实操心得:
become失败时,不要只看FAILED!,要加-vvv参数。它会显示真实执行的命令,比如sudo -S -p "[sudo via ansible, key=xxx] password:" -u root /bin/sh -c 'echo BECOME-SUCCESS-xxx; /usr/bin/python3...'。如果你看到sudo: a password is required,说明目标机sudoers文件没配好,而不是 Ansible 配置问题。
4. 实操过程详解:从零部署一个可验证的 Nginx 服务
4.1 环境准备:三台虚拟机的最小可行配置
我们用三台 Ubuntu 22.04 虚拟机模拟真实环境:
- Control Node(控制节点):你的开发机,IP
192.168.1.5,已安装 Ansible 2.15.8; - Web Server(应用节点):IP
192.168.1.10,仅开放 SSH(22端口)和 HTTP(80端口); - DB Server(数据库节点):IP
192.168.1.20,仅开放 SSH(22端口)。
在 Control Node 上创建项目目录:
mkdir -p ~/ansible-nginx/{inventory,playbooks,files,templates} cd ~/ansible-nginx初始化 inventory(YAML 格式):
# inventory/production.yml all: children: webservers: hosts: web-01: ansible_host: 192.168.1.10 ansible_user: ubuntu ansible_ssh_private_key_file: "/home/yourname/.ssh/id_rsa_web" databases: hosts: db-01: ansible_host: 192.168.1.20 ansible_user: ubuntu ansible_ssh_private_key_file: "/home/yourname/.ssh/id_rsa_db"测试连通性(关键!别跳过):
ansible -i inventory/production.yml webservers -m ping # 应返回 SUCCESS ansible -i inventory/production.yml databases -m ping # 应返回 SUCCESS如果databases组失败,但webservers成功,说明id_rsa_db密钥或ubuntu用户权限有问题,立即排查,别往下走。
4.2 编写第一个 playbook:site.yml的逐行解析
--- # playbooks/site.yml - name: Deploy Nginx to webservers hosts: webservers become: true vars: nginx_package: nginx nginx_service: nginx nginx_config_path: /etc/nginx/sites-enabled/default pre_tasks: - name: Ensure apt cache is up to date apt: update_cache: true cache_valid_time: 3600 tasks: - name: Install Nginx package apt: name: "{{ nginx_package }}" state: present update_cache: false # pre_tasks 已更新,这里跳过 - name: Copy custom Nginx config copy: src: "../files/nginx.conf" dest: "{{ nginx_config_path }}" owner: root group: root mode: '0644' backup: yes # ⚠️ 关键!每次覆盖前自动备份为 .bak - name: Test Nginx configuration syntax command: nginx -t register: nginx_test changed_when: false # 此命令不改变系统状态,不触发 handler - name: Fail if Nginx config test fails assert: that: nginx_test.rc == 0 msg: "Nginx configuration test failed. Check /etc/nginx/sites-enabled/default." - name: Restart Nginx service service: name: "{{ nginx_service }}" state: restarted enabled: true handlers: - name: Reload Nginx service: name: "{{ nginx_service }}" state: reloaded重点解析:
pre_tasks保证apt缓存最新,避免因缓存过期导致安装旧包;copy模块的backup: yes是保命设置,某次我误操作覆盖了生产配置,靠.bak文件 30 秒内恢复;command: nginx -t后接assert,这是防御性编程——宁可提前失败,也不让错误配置上线;handlers里的reloaded不会在restarted后触发,因为restarted已经完成全部 reload 动作,这里只是留个钩子,供后续notify调用。
4.3 文件与模板:files/和templates/的分工哲学
在files/目录下创建nginx.conf(静态文件):
# files/nginx.conf server { listen 80; server_name _; root /var/www/html; index index.html; location / { try_files $uri $uri/ =404; } }注意:这不是模板,是纯文本。copy模块直接搬运,不做任何替换。
如果需要动态内容(如根据环境切换后端地址),才用templates/:
# templates/nginx.conf.j2 upstream backend { server {{ backend_host }}:{{ backend_port }}; } server { listen 80; location /api/ { proxy_pass http://backend; } }调用方式:
- name: Deploy templated Nginx config template: src: "../templates/nginx.conf.j2" dest: "{{ nginx_config_path }}" owner: root group: root mode: '0644'提示:Jinja2 模板里
{{ }}中的变量,必须在 playbook 的vars:或group_vars/中定义,否则渲染时报错AnsibleUndefinedVariable。我习惯在group_vars/webservers.yml里集中管理:
# group_vars/webservers.yml backend_host: "192.168.1.20" backend_port: "3000"4.4 执行与验证:从命令行到浏览器的完整闭环
执行部署:
ansible-playbook -i inventory/production.yml playbooks/site.yml -v-v参数显示详细日志,你会看到:
ok: [web-01] => {"changed": false, "ping": "pong"}(连通性)changed: [web-01] => {"changed": true, "state": "present", ...}(安装成功)changed: [web-01] => {"changed": true, "backup_file": "/etc/nginx/sites-enabled/default.bak"}(备份生成)ok: [web-01] => {"changed": false, "rc": 0, ...}(配置测试通过)changed: [web-01] => {"changed": true, "state": "restarted", ...}(服务重启)
验证是否成功:
- 在 Control Node 上用
curl测试:
curl -I http://192.168.1.10 # 应返回 HTTP/1.1 200 OK- 登录 Web Server,检查备份文件:
ls -la /etc/nginx/sites-enabled/default* # 输出应包含 default 和 default.bak- 检查服务状态:
systemctl status nginx # 应显示 active (running)如果curl返回Connection refused,别急着重跑 playbook。先登录 Web Server,执行sudo nginx -t,看是否语法错误;再执行sudo systemctl status nginx,看是否启动失败。Ansible 只是执行器,真正的故障点永远在目标机上。
5. 常见问题与排查技巧实录:那些让我凌晨三点还在敲键盘的 Bug
5.1 Top 5 高频问题速查表
| 问题现象 | 根本原因 | 排查命令 | 解决方案 |
|---|---|---|---|
UNREACHABLE! => {"msg": "Failed to connect to the host via ssh: Permission denied (publickey)"} | SSH 密钥未正确授权,或ansible_ssh_private_key_file路径错误 | ssh -i /path/to/key ubuntu@192.168.1.10 | 检查密钥权限chmod 600 /path/to/key;确认ansible_ssh_private_key_file是绝对路径 |
FAILED! => {"msg": "The task includes an option with an undefined variable"} | 变量名拼写错误,或变量未在vars_files/group_vars中定义 | ansible-inventory -i inventory/production.yml --graph查看变量继承树 | 用ansible-inventory -i inventory/production.yml --list | jq '.webservers.vars'检查变量是否加载 |
ERROR! Syntax Error while loading YAML | YAML 缩进用 Tab 而非空格;- name:后少空格;引号不匹配 | yamllint playbooks/site.yml(需 pip install yamllint) | 统一用 VS Code YAML 插件,设置editor.insertSpaces: true |
FAILED! => {"changed": false, "msg": "Could not find the requested service nginx: host"} | 目标机未安装nginx包,或service模块名与 systemd 服务名不一致 | systemctl list-unit-files | grep nginx | Ubuntu 用nginx,CentOS 用nginx,但某些定制镜像可能叫httpd,先查再写 |
FAILED! => {"msg": "Aborting, target uses selinux but python bindings are not installed!"} | 目标机启用了 SELinux,但未安装libselinux-python3包 | python3 -c "import selinux" | 在 playbook 开头加任务:package: name=libselinux-python3 state=present |
5.2 独家避坑技巧:来自血泪教训的 3 条军规
军规一:永远在playbook开头加gather_facts: false,除非你真需要事实
Ansible 默认执行setup模块收集目标机硬件、网络、系统信息,耗时 2-5 秒/台。对于纯配置部署(如拷贝文件、重启服务),这些事实毫无用处,反而拖慢速度。我在 50 台服务器的滚动更新中,加了这行,总耗时从 12 分钟降到 3 分钟。如果某任务需要ansible_distribution等变量,再单独加setup:任务。
军规二:ignore_errors: true是毒药,block/rescue才是解药
新手常写ignore_errors: true忽略某个任务失败,结果后续任务在错误状态下运行,引发雪崩。正确做法是用block包裹一组任务,rescue处理异常:
- block: - name: Start service service: name=nginx state=started - name: Verify service is running uri: url=http://localhost/health status_code=200 rescue: - name: Rollback on failure copy: src: "/etc/nginx/sites-enabled/default.bak" dest: "/etc/nginx/sites-enabled/default" remote_src: true - name: Restart nginx after rollback service: name=nginx state=restarted这样,只要uri检查失败,就自动回滚配置并重启,而不是让错误服务继续跑。
军规三:--limit参数不是“选机器”,而是“选标签”ansible-playbook -i inventory.yml site.yml --limit web-01看似只跑一台,但如果web-01在 inventory 中属于webservers组,而webservers组还有web-02,--limit会同时影响两者。真正精准控制,要用--limit @<filename>,其中<filename>是临时文件,内容为:
web-01或者更优雅地,在 inventory 中给主机打标签:
web-01: ansible_host: 192.168.1.10 tags: ["canary"] # 自定义标签然后执行ansible-playbook ... --limit tag_canary,这才是灰度发布的正确姿势。
5.3 日志与审计:如何证明“这次更新没动数据库”
生产环境最怕“背锅”。某次线上故障,运维说“我没动 DB 配置”,但监控显示 DB 连接数突增。如何自证清白?Ansible 提供原生审计能力:
- 启用详细日志:在
ansible.cfg中添加:
[defaults] log_path = /var/log/ansible.log- 执行时加
--start-at-task记录起始点:
ansible-playbook -i inventory/production.yml playbooks/site.yml \ --start-at-task "Install Nginx package" \ --limit web-01- 日志
/var/log/ansible.log会记录每条命令、执行时间、返回码、标准输出。你可以用grep快速定位:
grep "web-01" /var/log/ansible.log \| grep "TASK \[.*database.*\]" # 如果无输出,证明本次执行确实没碰数据库相关任务最后分享一个小技巧:我所有 playbook 都在
vars:里定义deployment_id: "{{ lookup('pipe', 'date +%Y%m%d-%H%M%S') }}",然后在debug:任务里打印{{ deployment_id }}。这样每次执行都有唯一 ID,配合日志,可以精确追溯到某次部署的全部操作,再也不用和同事扯皮“是不是你改的”。
这个笔记写到这里,其实已经远超“操作笔记”的范畴。它是我把 Ansible 当成一把刀,在 Linux 系统的骨头上反复刮削、打磨出来的手感。没有玄学,只有每一次ansible-playbook命令敲下去后,屏幕上滚动的ok、changed、failed字样,以及它们背后对应的真实世界反馈。你不需要记住所有模块,只需要记住:当报错出现时,先看UNREACHABLE还是FAILED,再看-vvv输出的最后一行命令,然后登录目标机,用最原始的ls、cat、systemctl去验证。Ansible 不是魔法,它只是把人的操作标准化、可重复化。而真正的魔法,永远在你敲下回车键之前,那三秒钟的思考里。