作为这套Ansible Playbook实战系列的第002篇我直接把“什么是Ansible”这类教科书章节略过上来就讲怎么把Playbook用在真实干活场景里。你手上只要有一台Linux服务器虚拟机里跑的CentOS、Ubuntu都行我就用20个能直接落地的例子把文件操作、软件部署、服务管理、配置模板这些自动化运维的日常动作全趟一遍。这20个例子不是随手凑的而是按一个新运维接手服务器的真实流程排的——从最简单的创建文件到用tags做定向执行再到用vault保护敏感信息。写完你会发现Playbook没那么玄乎它只是把你亲手敲过一遍又一遍的命令变成了能复用的“作业指导书”。1. 为什么是Ansible Playbook这套20例又该怎么用1.1 Playbook和Ad-hoc命令到底差在哪Ansible有两条路一条是ad-hoc命令直接在命令行执行单次操作比如ansible all -m shell -a uptime另一条是Playbook把一系列操作写成剧本。很多人学习时卡在同一个地方既然ad-hoc命令就能跑为什么还要多学一种写法因为生产环境的自动化从来不是单点操作而是带依赖关系的任务链先装软件、再改配置、再重启服务还要考虑哪一步失败了该怎么办、要不要回滚。这些逻辑用ad-hoc命令写会乱成一团只有Playbook能把它结构化地沉淀下来供团队反复使用。打个比方ad-hoc是叫外卖点一次送一次方便但每次都得重新下单Playbook是你家厨房里那本常备菜谱照着做出来的菜味道稳定还可以传给你徒弟。更关键的是Playbook是声明式写法——你声明“要让Nginx运行且开机自启”Ansible自己会判断当前机器是什么状态差什么补什么。同一份Playbook跑一百次结果和第一次完全一致不会越跑越乱这就是幂等性。幂等是Ansible最值钱的设计理念后面每个例子你都会感受到它。1.2 20个案例的设计路线这20例被我分成两大段。第一段是入门10例围绕文件、用户、软件包、服务、定时任务这些最常用模块属于“照着抄就能用”的部分第二段是进阶10例把template、handler、循环、条件、tags、roles、vault这些Playbook的特色能力放进去属于“需要想一想为什么”的部分。前10个例子解决的是“单个任务怎么做”后10个解决的是“一组任务怎么配合”。学习节奏上我建议不要跳。有些朋友觉得file模块创建目录太简单直接翻到roles章节结果看目录结构看到一半就放弃了。先承认“我会用mkdir”并不丢人Ansible的价值就在于把简单的mkdir变成可追踪、可回滚、可分发到100台机器的操作。耐心把这20个例子跑完你再看生产环境的Playbook仓库基本上不会两眼一抹黑。1.3 动手前必须弄懂的三个底层概念第一YAML语法。这是新手报错重灾区核心就三条缩进用两个空格绝对不要用Tab冒号后面必须跟一个空格列表项用短横线加空格开头。很多人的报错“mapping values are not allowed here”就是冒号后没加空格导致的。第二inventory清单。这是Ansible的“通讯录”告诉它你要操作哪些机器。最简单的hosts文件可以写成一行web1 ansible_host192.168.1.10。这里有几个命名细节容易踩坑ansible_host是IP地址ansible_user是登录用户ansible_ssh_private_key_file指定密钥。稍后实验环境里我会给一份完整示例。第三模块参数的FQCN写法。从Ansible 2.10开始官方推荐使用完全限定名称比如ansible.builtin.copy、ansible.builtin.service。网上大量老教程还在写copy:、service:这种短格式虽然目前还能跑但会让控制端输出一段弃用警告。建议你一开始就养成写全的习惯后面升级Ansible主版本时少掉很多头发。2. 环境准备半小时搭起Playbook实验场2.1 安装Ansible和第一条连通性验证控制机建议用Linux或macOSWindows做控制端比较折腾新手先别往那个方向使劲。安装方式我常用pip因为能指定版本pip3 install ansible。想用包管理器也可以Ubuntu/Debian下是apt install ansibleCentOS/RHEL需要先装EPEL源再yum install ansible。装完跑一下ansible --version能输出版本号就说明环境OK。接下来给控制机生成SSH密钥并拷贝到目标机ssh-keygen -t ed25519然后ssh-copy-id youruser目标机IP。这一步成功后Ansible才能免密连接目标机。它和你在终端里登录服务器是一个道理Ansible只是把SSH这个动作封装成自动化了。最小inventory我放在/opt/ansible-lab/hosts.ini里[demo] webserver ansible_host192.168.56.10 ansible_userroot [all:vars] ansible_python_interpreter/usr/bin/python3第一行是组名[demo]第二行是主机列表给机器起个逻辑名字webserveransible_host指向真实IPansible_user指定SSH登录用户。[all:vars]下面那行是全局变量指定Python解释器路径防止目标机同时有python2和python3时Ansible选错。写好之后跑第一条命令验证连通性ansible demo -m ping。注意这个ping模块不是网络ICMP探测它检测的是目标机能不能正常执行Ansible模块所需的最小Python环境。返回pong就说明通道打通了。2.2 我的建议目录结构很多教程一上来就铺开一套完整目录把新手唬住了。我自己做实验时习惯用极简结构跑通再逐步膨胀/opt/ansible-lab/ ├── ansible.cfg ├── hosts.ini ├── playbooks/ │ ├── 01_file.yml │ ├── 02_package.yml │ └── ... └── roles/ansible.cfg是控制端的本地配置文件。默认会依次读取/etc/ansible/ansible.cfg、用户目录下的.ansible.cfg、当前目录下的ansible.cfg优先级是当前目录最高。我的实验室配置长这样[defaults] inventory hosts.ini host_key_checking False retry_files_enabled False roles_path roles第一行指定默认inventory文件后面所有命令都不用带-i参数了。host_key_checking False是关闭SSH首次连接时的指纹确认不然第一次连新机器会卡在交互提示上。retry_files_enabled False是取消生成.retry文件个人实验室没必要留。如果你在公司环境用host_key_checking建议保持默认True那是安全边界的问题别学我图省事。2.3 第一次运行Playbook读懂输出新建一个playbooks/00_hello.yml内容是最简单的Hello World- name: 第一个Playbook hosts: demo tasks: - name: 打印一条消息 ansible.builtin.debug: msg: Hello Ansible运行命令是ansible-playbook playbooks/00_hello.yml。你会看到输出分几个层级PLAY [第一个Playbook]是一个Play的头部TASK [打印一条消息]是当前正在执行的任务任务下面会显示状态——ok表示任务正常完成且没有发生变更changed表示任务检测到差异并做了修改failed和unreachable分别是任务失败和目标机无法连接。刚开始练你要养成每次执行后扫一眼这些颜色的习惯绿色表示没变化黄色表示有变更红色就是要处理的错误。这里顺带讲一个命令ansible-playbook playbooks/00_hello.yml --check。这是模拟执行不真正改动目标机只告诉你“如果执行会变什么”。后面几乎所有生产变更我都会先跑一遍--check再跑真实执行这个习惯能救你无数次。3. 入门10例把最常用的模块练到顺手3.1 实例1-3文件与目录的创建、复制、删除例1创建文件并写入内容文件操作是自动化里出现频率最高的动作我第一个例子选择用copy模块而不是file模块是因为它兼顾两个需求写文件、写内容。- name: 创建测试文件 hosts: demo tasks: - name: 写入一个带内容的文件 ansible.builtin.copy: content: Hello Ansible\n dest: /tmp/hello.txt mode: 0644content参数直接把字符串内容写入目标文件dest指定目标路径mode给文件设权限。要注意content里想换行必须写\nYAML的引号会保留转义。这个任务第二次执行时状态是ok因为内容和权限都没变化这就是幂等。例2创建多级目录实际业务里经常一次需要建多级目录比如/data/apps/nginx/logs。不要用shell去拼mkdir -pfile模块自带递归能力- name: 创建应用目录 ansible.builtin.file: path: /data/apps/nginx/logs state: directory recurse: true owner: www group: www mode: 0755state: directory声明目标是目录recurse: true表示目录不存在时逐级创建。owner和group把目录归属给www用户这一步在生产环境特别常见很多软件部署跑不起来就是目录权限不对。例3清理不再需要的文件或目录删除操作同样不用shell声明式写起来更安全- name: 删除过期临时目录 ansible.builtin.file: path: /tmp/old_app_cache state: absentstate: absent表示“这个东西不该存在”不管它是文件还是目录Ansible都会帮你清理掉。注意这和命令rm -rf有个微妙的心理差别命令是你主动发出的破坏动作声明式是“目标状态是移除它”习惯后你的操作会更克制。3.2 实例4-6软件包、服务与远程命令例4安装软件包package模块是通用包管理器接口你在Playbook里不用关心目标机是apt还是yum- name: 安装nginx ansible.builtin.package: name: nginx state: presentstate: present表示“确保已安装”如果已经装了任务返回ok不会重复安装。想升级到最新版就写state: latest但生产环境慎用因为“最新”是个不可控状态哪天源里更新了个不兼容版本你的服务就跟着遭殃了。例5启动服务并设置开机自启装完软件下一步就是起服务- name: 启动nginx并设置开机自启 ansible.builtin.service: name: nginx state: started enabled: truestate: started保证服务处于运行中enabled: true保证开机自动启动。如果你希望服务配置变更后自动重启先别急着在这里写state: restarted那是handler的活后面例14我会详细说。这里你只需要记住声明式写法下Nginx已经是停止状态Ansible会自动把它启动不会像命令那样报“already running”就退出。例6command和shell到底用哪个远程执行命令是绕不开的场景但很多新手分不清command和shell的边界- name: 查看内核版本 ansible.builtin.command: cmd: uname -rcommand模块不经过shell所以不支持|、、这些shell语法也没有环境变量展开。如果你要执行带管道或重定向的命令只能用shell模块- name: 统计当前在线用户数 ansible.builtin.shell: cmd: who | wc -l我的建议是能不用shell就不用。command模块返回的结构化结果stdout、stderr、rc更容易被Ansible解析和判断而shell的输出是一坨文本后续处理麻烦。只有确实需要管道、正则、重定向时才切换到shell。3.3 实例7-10用户、定时任务与配置注入例7创建部署用户并加入sudo组业务跑起来不能全用root通常要建一个专用用户- name: 创建部署用户 ansible.builtin.user: name: deploy groups: [sudo, docker] append: true shell: /bin/bash create_home: true这里最容易犯的错是漏写append: true。如果用户已存在不写append的话Ansible会用groups列表覆盖用户现有的附属组把人家原本在的组全清掉了。这个参数在官方文档里描述得很轻描淡写实际坑过无数人。密码设置可以用password参数配合ansible-vault加密这个我放到例19。例8添加计划任务服务器上的日志清理、临时文件删除这类活用cron模块管理再合适不过- name: 每天凌晨清理超过7天的日志 ansible.builtin.cron: name: clean old logs minute: 30 hour: 2 job: find /var/log -name *.log -mtime 7 -delete只指定minute和hour时其他字段默认是*所以这个任务等价于每天2点30分执行。name字段很重要Ansible靠它唯一标识一个cron条目。以后你想改这个任务的执行时间继续用同样的name就行Ansible会自动更新原有条目而不是在crontab里堆一行新的。例9确保sshd配置项的值正确改配置文件有一个非常实用的模块叫lineinfile。它按行扫描目标文件找到匹配正则的那一行替换成你指定的内容- name: 禁用密码登录 ansible.builtin.lineinfile: path: /etc/ssh/sshd_config regexp: ^#?PasswordAuthentication(\s\w)?$ line: PasswordAuthentication no validate: sshd -t %sregexp里我故意写了#?用来匹配可能被注释的行validate参数会在保存前执行sshd -t检查语法这是改ssh配置的救命稻草——防止你把sshd_config改坏了把自己锁在门外。改系统关键配置的Playbook几乎都应该配置validate。例10批量分发同一份配置文件几十台机器需要同一份Nginx配置时用copy模块分发- name: 分发nginx主配置 ansible.builtin.copy: src: files/nginx.conf dest: /etc/nginx/nginx.conf owner: root group: root mode: 0644 backup: truesrc是控制机上的文件路径相对于Playbook所在目录解析。backup: true会在目标文件已存在且内容不同时先备份一份带时间戳的文件再覆盖。这个参数在配错配置想回滚时特别有用。文件分发的另一个场景是“内容相同但每台机器有细微差异”那种情况不能用copy要交给模板渲染也就是例13。3.4 跑完这10个例子你需要记住的四个习惯第一个习惯是跑之前先--check。第二个习惯是准备清理环境很多任务会创建文件、装包、改配置测试机上可以反复跑但要注意把资源回收别跑一次留一堆积木。第三个习惯是看输出里的changed字段——真正理解了“changed意味着这台机器从旧状态切换到了新状态”你才算理解Ansible的声明式思维。第四个习惯是别贪多把上面每个例子都稍作变化跑一遍比如把例4的nginx换成别的包把例8的清理路径换成自己的目录动手改过一遍才算真正吸收。4. 进阶10例从“能跑”到“用得漂亮”4.1 实例11-13变量、facts与模板渲染例11用facts读取系统信息Ansible每次执行Play时会自动在目标机上采集一堆系统信息叫facts包括操作系统类型、内存大小、IP地址等。这些信息被放到变量里可以直接用- name: 打印目标机基础信息 hosts: demo tasks: - name: 输出系统信息 ansible.builtin.debug: msg: 主机名: {{ ansible_hostname }}, 系统: {{ ansible_os_family }}, 内存: {{ ansible_memtotal_mb }} MB{{ }}是Jinja2模板语法Ansible执行到这一行时会把它替换成实际值。facts是后续很多条件判断的基础比如例16里根据操作系统执行不同命令就是基于ansible_os_family。例12变量的定义与引用变量除了facts这种系统自动生成的更多时候是我们自己定义的。定义方式有很多种我推荐在Playbook顶部写一个vars块简单直观- name: 使用变量部署应用 hosts: demo vars: app_name: myapp app_port: 8080 app_root: /opt/{{ app_name }} tasks: - name: 创建应用目录 ansible.builtin.file: path: {{ app_root }} state: directory注意一个细节变量里可以引用其他变量app_root中用了{{ app_name }}这是Ansible变量解析支持嵌套的体现。文件目录这种含变量的值在YAML里最好用双引号包起来避免特殊字符被解析出歧义。例13用Jinja2模板生成差异化配置前面例10讲到copy模块做文件分发但它有个局限所有目标机拿到的文件内容完全一样。真实场景往往是“同一个配置框架不同机器IP不同、内存参数不同”。这时候要上template模块- name: 用模板生成redis配置 ansible.builtin.template: src: redis.conf.j2 dest: /etc/redis/redis.conf owner: redis group: redis mode: 0644在Playbook同级目录放一个templates/redis.conf.j2文件内容里写Jinja2占位符bind {{ ansible_default_ipv4.address }} maxmemory {{ redis_maxmemory | default(256mb) }}模板文件后缀.j2是约定俗成Ansible会通过后缀识别这是模板。default(256mb)是Jinja2过滤器当变量redis_maxmemory未定义时使用默认值。模板渲染是Ansible各类模块中最能提效的一个学会了它你就能把“部署一套环境”抽象成“填几个参数”批量复制环境的能力会上一个台阶。4.2 实例14-16handler、循环与条件例14让服务在配置变更后自动重启改配置文件后必须重启服务这个动作如果直接写在tasks里会导致每次都重启哪怕配置根本没变。Ansible的handler就是专门解决这个痛点的它只在被通知时执行而且只执行一次。- name: 更新Nginx配置 hosts: demo tasks: - name: 渲染nginx配置 ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf notify: restart nginx handlers: - name: restart nginx ansible.builtin.service: name: nginx state: restarted注意结构handlers和tasks是同一层级的。当template任务发生changed时会触发notify指向的handler“restart nginx”如果配置没变化handler不会执行。这就是为什么Ansible可以在不改动配置的机器上做到“零重启”对生产环境的稳定性至关重要。例15用loop批量处理相似任务创建5个用户、拷贝多个文件、安装一堆依赖包这些重复动作用一个任务加loop就够- name: 批量创建开发用户 ansible.builtin.user: name: {{ item }} state: present groups: dev loop: - alice - bob - carolloop遍历列表每个列表项赋值给内置变量item。写惯了代码的人会觉得这不是理所当然吗但很多运维就是从学会loop开始思维从“一条命令管一台”升级到“一段逻辑管一片”。如果你想遍历字典还能这样写loop: - name: alice uid: 1001 - name: bob uid: 1002然后引用{{ item.name }}和{{ item.uid }}。这是循环变复杂时的标准打开方式。例16用when做条件判断同一套Playbook跑在不同操作系统上执行的命令往往不一样。when条件判断就是为此而生的- name: 按系统安装包 hosts: demo tasks: - name: 安装curlDebian系 ansible.builtin.apt: name: curl state: present when: ansible_os_family Debian - name: 安装curlRedHat系 ansible.builtin.yum: name: curl state: present when: ansible_os_family RedHatwhen里的表达式可以非常丰富比较运算、逻辑运算、in判断都能用。要注意when的值是在目标机上求值的它可以使用facts变量和你在Play中定义的所有变量。判断条件是字符串时别忘记加引号否则YAML会把Debian当成布尔值解析报错的时候很让人摸不着头脑。4.3 实例17-20tags、roles、vault与检查模式例17用tags挑选任务执行一套完整的部署Playbook通常包含安装软件、修改配置、启动服务、健康检查等多个阶段。日常运维里可能只想单独重跑“修改配置”这一步不想重新装一遍软件。tags就是为这个场景设计的- name: 部署Nginx全流程 hosts: demo tasks: - name: 安装Nginx ansible.builtin.package: name: nginx state: present tags: install - name: 渲染配置文件 ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf tags: config - name: 启动服务 ansible.builtin.service: name: nginx state: started tags: service执行时只跑配置文件相关任务命令是ansible-playbook deploy_nginx.yml --tags config反过来想跳过某些任务用--skip-tags install。注意tags: always有特殊含义带这个标签的任务无论你指定什么tags都会执行。如果有一段公共任务比如“加载变量”“检查主机连通性”给它标记always比较合适。例18用roles组织可复用的任务集合当你需要把“部署Nginx”这套Playbook复用到另一个项目时直接复制粘贴不够优雅Ansible给出的工程化方案是角色roles。一个角色把tasks、handlers、templates、vars分组放进固定目录结构roles/ └── nginx/ ├── tasks/ │ └── main.yml ├── handlers/ │ └── main.yml ├── templates/ │ └── nginx.conf.j2 └── vars/ └── main.yml在Playbook中引用角色只需一行- name: 应用nginx角色 hosts: demo roles: - nginxAnsible会自动加载roles/nginx/tasks/main.yml里的任务列表。角色的意义不只是整理目录更是定义了清晰的契约任务怎么写、模板放哪里、变量作用域怎么隔离。团队协作时几个运维各负责一个角色的维护互不干扰整体质量比一个人维护一大堆Playbook高出不少。例19用vault加密敏感变量Playbook里经常出现密码、密钥、token直接明文写在文件里代码仓库一泄露所有密码跟着完蛋。Ansible自带的vault提供了对称加密方案ansible-vault create secrets.yml这个命令会打开编辑器在文件里写入变量保存后文件内容是完全加密的。在Playbook中引用它- name: 使用加密变量 hosts: demo tasks: - name: 引入vault文件 ansible.builtin.include_vars: file: secrets.yml - name: 创建数据库用户 ansible.builtin.mysql_user: name: {{ db_user }} password: {{ db_password }}执行时需要提供vault密码ansible-playbook site.yml --ask-vault-pass。如果怕交互输入麻烦可以把vault密码放到一个单独文件里并用--vault-password-file指定但那个文件本身的权限要设成600。vault是任何正式团队都要用的特性别图省事。例20语法检查与模拟演练最后一个例子不是具体模块而是一套工作习惯。每次写完Playbook先检查语法ansible-playbook site.yml --syntax-check这个命令只做本地语法检查不会连目标机。语法通过后再做一次模拟执行ansible-playbook site.yml --check --diff--check进入“干跑模式”Ansible不会真正修改目标机但会调用模块的判断逻辑告诉你“这里会变”。加上--diff还会展示文件级别的差异预览。需要注意command和shell模块在check模式下不会真正执行也不会给出可靠的变化判断官方称之为“不检查安全”的模块。对这种模块可以显式加check_mode: no让它们哪怕在check模式下也实际执行但你要清楚自己在干什么。5. 实战中踩过的坑和排查思路5.1 破解高频报错module result deserialization failed这个报错在Ansible老用户群里出现频率极高完整错误长这样module result deserialization failed: no start of json char found ansible。第一次遇到时我也一脸懵后来逐步排查才摸清它的本质。先解释一下Ansible的执行原理控制机把模块脚本和参数打包后传到目标机目标机的Python解释器执行这段脚本然后将结果以JSON格式输出到stdout控制机接收后再反序列化。之所以叫“deserialization”就是因为控制机在等一个合法的JSON字符串结果拿到了一堆不是JSON的内容。最常见的触发原因有四类第一目标机的shell配置文件里有输出语句。比如用户根目录下的.bashrc里写了echo WelcomeSSH登录时这些内容会被打印到stdout污染Ansible模块的结果流。排查方法很简单手动SSH登录目标机看登录过程是否有额外输出。如果有把这些echo从shell配置里删掉。第二sudo交互提示。如果Ansible通过become去执行sudo而sudo配置要求输入密码或配置了requiretty会产生额外的交互输出。解决方向是调整sudoers配置给ansible用户设置NOPASSWD或取消requiretty限制。第三Python解释器路径不对。Ansible在目标机上执行模块时必须找到可用的Python如果你在inventory里指定了ansible_python_interpreter但那个路径下根本没有Python或者Python版本太老导致模块执行中途崩掉输出的信息就不是JSON。检查方法在hosts.ini里显式指定/usr/bin/python3然后重新执行。第四分区磁盘满。Ansible执行模块前会在目标机的临时目录默认~/.ansible/tmp写脚本磁盘满了脚本可能写一半执行结果自然无法反序列化。用df -h检查根分区空间清出空间后报错通常就消失了。遇到这个报错我推荐的排查顺序是先手动SSH看有没有登录横幅再看sudo能否免密执行接着确认python3路径和版本最后检查磁盘空间。绝大多数情况前三步就能定位。还有一个隐藏调试技巧在ansible.cfg里临时加ANSIBLE_KEEP_REMOTE_FILES1Ansible会把目标机上生成的模块脚本保留下来不清理进入目标机同名路径直接打开脚本看内容能发现很多奇怪的问题。5.2 其他常见问题速查表除了上面那个反序列化报错日常Playbook写得多了还会遇到一批高频问题我整理成了速查表问题表现常见原因处理办法fatal: [host]: UNREACHABLE!SSHD未启动、防火墙拦截、密钥未分发检查目标机SSH服务状态、防火墙规则、ssh-copy-id重新分发sudo: a password is required当前用户无法免密sudo给用户配置NOPASSWD或用ansible_become_password传递密码找不到模块或模块弃用警告用的短模块名且版本较新改用ansible.builtin.前缀的FQCN写法变量未定义变量名拼错或引用作用域超出范围用debug打印变量确认变量定义层级必要时用default过滤器兜底YAML缩进报错Tab和空格混用、冒号后缺空格在编辑器里开启显示空格统一全空格缩进任务总是显示changed模块本身不幂等或参数里有时间戳尽量不用shell/command文件操作加mode、owner固定权限执行被卡在交互提示上某些模块执行时需要交互确认检查目标机SSH配置和sudo配置确保非交互执行这表看起来简单但每一条背后都是我实实在在熬夜排查过的案例。比如“任务总是changed”这个现象你如果放任不管任何依赖判断changed的handler都会每次触发导致每跑一次Playbook服务就重启一次线上事故就是这样来的。5.3 新手最容易被忽略的三个细节第一个细节是become的作用域。很多人在Play头部写了become: true以为所有任务都会以root执行但如果在某个task里覆盖了become: false那个task就会回到普通用户权限报错让你查到崩溃。排查技巧是看报错前最近的become设置别想当然。第二个细节是cron模块的name字段。Ansible识别cron条目的主键就是name如果你不小心改了名字旧条目不会自动删除会在crontab里留下一个僵尸任务。规范化命名格式对后续维护帮助很大我习惯用项目名_动作_目标这样的三段式。第三个细节是--check的局限性。前面例20提过command和shell模块不参与检查这是新手最容易误判的地方你以为--check跑完绿色就可以放心了结果生产执行时那几个shell命令带来了实际变更。正确做法是对shell类任务单独审一遍或者用check_mode: no让它在干跑模式下也真实执行、真实返回结果自己心里有数。6. 几句来自实战的话这20个例子我陪隔三差五跑一遍每次跑都有新的领悟因为Ansible的坑往往是“你觉得自己会了才会真正踩到”的那种。我个人的建议是别在练习环境里一次性跑完全部20个就以为结束了。你可以从明天起选一台自己的工作机或虚拟机把每天重复做两次以上的操作写成Playbook哪怕一开始只是清理临时文件、安装开发依赖、同步配置几天之后你自然会发现原来那些繁琐的部署步骤是可以沉淀成“可执行文档”的。我也特别推荐一个练习方法把例17的tags和例20的check结合起来用。给每个任务都打上合理标签养成“改配置文件只跑config标签、重服务只跑service标签”的习惯这会让你的Playbook在真正生产环境中更有生命力。最后如果你在跑这20个例子的过程中卡在某一步停下来想一想我的目标状态是什么Ansible有没有这个模块模块参数是不是写全了。想清楚这三件事大部分问题都能自行解决。