ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ansible Playbook实战:从文件分发到服务器自动化初始化

Ansible Playbook实战:从文件分发到服务器自动化初始化 说实话看这系列心得的标题就知道作者应该是在Linux运维这条路上一步一步踩过来的。前面20篇大概把命令、脚本、服务部署这些基础打了一遍到了Ansible这里正好是从“手动挨个操作”跨到“批量自动化管理”的关键一步。如果你也是运维或者正在转行做运维应该深有体会机器少的时候靠手敲没问题机器一多尤其是要同时改几十台配置、分发文件、重启服务的时候靠ssh一台台登录敲命令不光累还特别容易漏。所以这篇心得里的Playbook其实就是解决这个痛点的那个“开关”。先说说这篇文章适合谁看。已经会用Linux基本命令cd、ls、vim、systemctl这些但没接触过自动化配置工具的运维新人最合适另外就是写过一些shell脚本想进一步把部署流程标准化、可复用的同学。Playbook本身就是一套用YAML写成的“运维剧本”把你要在远程机器上做的所有操作像列清单一样写进去然后在控制端跑一下所有目标机器就会按剧本执行。这篇心得我尽量把从安装到写第一个可用的Playbook、再到实际分发文件授权限的过程中该避的坑都写出来。1. 环境准备与第一个Playbook先把工具箱搭好1.1 先搞懂Ansible的工作方式无Agent才是灵魂Ansible最让我觉得舒服的一点就是它不需要在被管理的每台机器上装Agent。这在当时对比了Puppet、SaltStack之后很快就决定了用它。Puppet和SaltStack虽然也很强但都得在被管理端装客户端光这一条几十台机器一轮装下来就足够让人头大了。Ansible走的是SSH协议控制端只要装一个ansible包被管理的机器只要有SSH服务、能用账号登录就够了。整个结构大概是这样的你有一台控制机可以是你的笔记本也可以是一台专门的跳板机上面装了Ansible然后在Ansible的清单Inventory文件里写上你要管理的机器IP和分组最后你写一个Playbook剧本定义“要在哪些机器上做什么事”。执行的时候控制机会通过SSH连上目标机器把要执行的模块推送过去执行完汇报结果。整个过程不需要被管理端提前安装任何额外软件这对新环境、裸机初始化这种场景特别友好。这里有个容易理解歪的点很多新手以为Ansible是装在被管理的目标机器上的其实不是。你可以在一台机器上装了Ansible去控制成百上千台机器。所以它的架构很轻轻到你只需要在控制端维护好SSH密钥和Playbook文件整个集群就在你手里了。1.2 安装Ansible的方式与版本选择安装Ansible的方法按不同的系统和习惯有好几种我按我实际用下来觉得靠谱的推荐一下。如果你的控制机是CentOS/RHEL系列最省事的是用EPEL源。命令很简单yum install epel-release -y yum install ansible -y装完后看下版本ansible --version这里要提醒一句EPEL源里的Ansible版本可能不是最新的但胜在稳定跟系统库的依赖关系处理得比较好适合生产环境不想折腾的场景。我之前在一台CentOS 7上直接装自带的是Ansible 2.9.x用到现在依然能满足绝大多数自动化需求。如果你用的是Ubuntu/Debian那更简单apt update apt install ansible -y如果你想要最新版本或者系统默认源里没有Ansible那走pip安装是更通用的路子。但要注意Python版本Ansible 2.9可以用Python 2.7但新版本比如5.x、7.x、9.x这些要求Python 3.8以上。所以先确认你的python3版本再用pip装python3 --version pip3 install ansible我个人建议如果是学习阶段用系统源装的版本就够了如果你要体验最新的模块功能和更快的执行速度再用pip装。别一上来就追新先把2.9用熟后面想升级随时可以升。1.3 编写主机清单要让Ansible知道管哪些机器装了Ansible之后第一件事不是急着写Playbook而是先把目标机器告诉它。这个清单文件默认在 /etc/ansible/hosts但我不太建议直接改系统文件而是在自己的项目目录里建一个用 -i 参数指定这样不同项目可以有不同的清单。举个例子我正在管理几台Web服务器和几台数据库服务器我可以这样写清单[web_servers] 192.168.1.10 192.168.1.11 192.168.1.12 [db_servers] 192.168.1.20 ansible_userroot 192.168.1.21 ansible_userroot最基础的写法就是IP地址一行一个用方括号分组。后面可以给某个主机单独指定连接用户比如上面db组里我就指定了用root登录。如果你的SSH端口不是默认22还可以在主机后面加 ansible_port2222 这种参数。写完之后先别急着写剧本先用Ansible的ping模块测试一下能不能连通。注意这个ping不是ICMP那个ping而是Ansible测试SSH连接和Python环境的模块ansible -i hosts all -m ping如果之前配置好了SSH免密登录这里应该会返回每台机器的pong。这一步通了下面写Playbook才有意义。1.4 配置SSH免密登录否则自动化无从谈起说到SSH免密这其实是Ansible自动化里非常前置的一步。Ansible走SSH连接如果每次都要密码虽然可以用 -k 参数提示输入密码或者配置ansible_ssh_pass变量但那样就失去自动化的意义了总不可能半夜定时任务跑的时候你起来输密码吧。所以我一般会在控制机上生成密钥对然后把公钥分发到所有目标机器上ssh-keygen -t rsa -b 4096 ssh-copy-id -i ~/.ssh/id_rsa.pub root192.168.1.10把每台机器都copy一遍之后ssh登录就不需要密码了。另外建议在ansible.cfg文件里把SSH密钥安全检查关掉避免第一次连接时卡在确认known_hosts的交互提示。最省心的配置是这样[defaults] host_key_checking False inventory ./hosts把host_key_checking关掉之后Ansible连接到新IP的时候就不会提示“是否确认密钥指纹”然后卡住等输入。如果你不喜欢也可以预先用ssh-keyscan把所有机器的指纹收集到known_hosts里效果一样。对学习来说直接关掉最省事。到这里环境就绪。接下来我们正式上手Playbook。2. 拆解Playbook核心语法别在缩进上栽跟头2.1 YAML语法Ansible的地基Playbook是用YAML写的所以想要写好PlaybookYAML的基本语法必须过关。YAML这东西刚接触的时候觉得简单写多了才发现它其实挺挑剔——尤其是缩进和空格的问题几乎是每个新手都会踩的坑。YAML的一个核心规则是用缩进表示层级关系而且不能使用Tab键必须用空格。很多人第一次写Playbook报错十个有九个是因为缩进混用了Tab和空格。另外冒号后面必须要有空格比如hosts: web_servers这个冒号后面是有一个空格的如果写成hosts:web_servers就是语法错误。还有短横线-也一样- name:这个横线后面有空格。举个最简单的例子- hosts: web_servers tasks: - name: ensure nginx is installed yum: name: nginx state: present顶层那个-表示这是一个play列表缩进两个空格说明 hosts 和 tasks 是同一个play的属性tasks下面再缩进说明 task 是 tasks 列表里的一个条目。这种规规矩矩的嵌套关系从缩进上一眼就能看出来。2.2 一个Play的骨架名字、目标、任务、变量一个最基本的Play由几个部分组成hosts目标机器、tasks要执行的任务列表、vars变量、handlers处理器、become提权。我一般习惯这样组织- hosts: web_servers # 在哪些机器上执行 become: yes # 是否需要切换root vars: http_port: 8080 # 定义变量 tasks: - name: install nginx yum: name: nginx state: present - name: start nginx service: name: nginx state: started enabled: yeshosts字段告诉Ansible去哪个分组执行become: yes等同于sudo提权比如你要装软件、改系统配置普通用户没权限就需要这个字段tasks就是执行清单从上到下依次执行vars用来定义变量后面可以通过{{ http_port }}来引用。这里特别想强调一下name这个字段的作用。很多新手觉得name随便起一下甚至不写也行但从实战角度我强烈建议每个task都写清晰的name。因为Ansible执行时会把每个task的name打印出来如果任务报错了你扫一眼name就知道是哪个环节出了问题。而且Playbook跑完之后输出结果每个task的状态ok还是changed对应哪个名字一目了然。花10秒钟取一个名字能省下你排查问题时的半小时。2.3 常用模块速览copy、file、yum、service、command模块是Ansible执行具体动作的“工具”同一个任务用不同的模块就有不同的效果。我总结一下日常用得最频繁的几个copy模块把文件从控制端复制到目标机器支持设置属主、属组、权限还支持备份。这个模块在“分发文件到所有机器”的场景里是绝对的主力后面专门演示。file模块管理文件属性创建目录、创建软链接、删除文件、修改权限等。它和copy的区别是file只管目标机器上的文件状态不负责从控制端传内容。yum/apt模块安装、卸载、更新软件包。比如yum: namenginx statepresent就是确保nginx装上了。service/systemd模块管理服务状态。statestarted启动、statestopped停止、staterestarted重启enabledyes设置开机自启。command/shell模块直接执行命令。两者区别是shell支持管道、重定向等高级特性command不行。能用command解决的就不用shell因为command更安全。debug模块打印变量值或调试信息写Playbook排查问题时特别好用。遇到不确定某个模块怎么用时直接用ansible-doc 模块名查看帮助文档这个命令会列出模块的所有参数和示例比上网查快多了。2.4 幂等性Playbook能重复跑而不出错的关键说到Playbook有一个概念必须理解幂等性。简单说就是一个Playbook执行多次和只执行一次最终效果是一样的。第一次跑的时候任务状态可能是changed有变化第二次跑同样的Playbook如果没有可做的改动任务状态就是ok不会对系统产生额外的副作用。举例来说copy模块如果发现目标文件内容已经和管理端源文件一致它就什么都不做返回ok真正有差异时它才会去覆盖返回changed。这就是为什么Ansible的模块设计得好的地方——它们会先检查当前状态再决定要不要动手而不是闷头使劲执行。理解了幂等性你在写Playbook时就会更注意尽量不要用command模块去执行那种每次都会产生变化的操作比如直接把一段文字echo追加到文件跑一次多一行而要尽量用专门的模块去描述“最终应该是什么状态”。比如追加内容用lineinfile模块就比echo加 安全得多因为它会先判断内容是否已存在。理解了幂等性你的自动化脚本才是真正可维护的。3. 把文件复制到所有节点并授权777最常用的文件分发场景3.1 场景一批机器要同步同一个脚本这里我说一个最典型的需求场景你有10台应用服务器需要统一放一个部署脚本到 /opt/scripts/ 目录下并且给脚本执行权限让所有节点上的用户都能运行它。在没有Ansible之前我的做法是这样循环ssh每台机器scp一次然后ssh进去chmod。大概是这样for ip in $(cat ip_list); do scp deploy.sh root$ip:/opt/scripts/ ssh root$ip chmod 777 /opt/scripts/deploy.sh done机器少的时候没啥问题但机器一多你还要考虑有些机器可能临时连不上、有些可能没有 /opt/scripts 目录整个命令行写下来又长又容易出错。用Playbook就是把这个过程结构化、可复查、可重复执行。3.2 编写Playbook的完整步骤先建立项目目录结构。我习惯这样组织mkdir -p ansible-project cd ansible-project touch deploy_script.yml mkdir files把要分发的脚本放在files目录下这样Playbook里引用相对路径就行了。假设我准备分发的脚本是 files/deploy.sh。然后在hosts清单里把所有目标机器的IP或主机名列好。我这里假设备一台测试机实际环境换成你的真实IP即可[app_servers] 192.168.1.30 192.168.1.31接着编写deploy_script.yml- hosts: app_servers become: yes tasks: - name: ensure target directory exists file: path: /opt/scripts state: directory mode: 0755 - name: copy deploy.sh to all nodes copy: src: files/deploy.sh dest: /opt/scripts/deploy.sh owner: root group: root mode: 0777 backup: yes这里两个task第一个确保目标目录存在用file模块把目录状态调整为存在、权限0755。为什么要单独建目录因为copy模块只管文件如果目标目录不存在它会直接报错。第二个task是核心分发动作copy模块的src是控制端本地相对路径dest是目标机器路径owner和group指定属主属组mode指定权限backup: yes表示如果目标文件已存在且内容不一致先把原文件备份一份再覆盖避免误操作后找不到原始版本。3.3 执行、验证、回滚写完后在项目目录执行ansible-playbook -i hosts deploy_script.yml执行过程中会看到每个task在每个主机上的状态如果一切正常输出的最后面会有一个类似 “ok3 changed1 unreachable0 failed0” 的汇总。到这里你的脚本就已经被复制到所有节点并且权限也设置好了。但要学会验证不能只看Ansible说成功就完事。我会用ad-hoc命令抽查一下ansible -i hosts app_servers -m command -a ls -l /opt/scripts/deploy.sh可以看到每台机器上文件的属主、属组、权限是否一致。这一步在批量分发后特别重要因为偶尔会有个别机器因为selinux或者路径原因导致文件没落地。如果发现分发的内容有问题想回滚到上一个版本因为我在copy之前设置了backup: yes目标机器上会有一个带时间戳的备份文件手动恢复或者再写一个Playbook从备份恢复都可以。日常使用中backup: yes建议打开占用不了多少空间却能在关键时刻救命。3.4 777权限到底该不该用说说我的看法既然提到了 mode: 0777我必须要多说一句因为这里其实藏着一个安全隐患。777权限意味着文件对所有人可读、可写、可执行。对普通用户来说这意味着任何能登录这台机器的人都可以随意修改这个脚本。如果脚本内容里包含敏感信息或者会被root身份定时执行那风险就非常大了。攻击者只需要把恶意内容写进这个脚本等定时任务一跑就拿到权限了。我在生产环境里一般给脚本的权限是0755或者0700。0755表示所有者可读写执行其他用户可读可执行但不能修改0700表示只有root能操作其他人啥都干不了。所以什么时候用777只有当这个文件确实需要被所有用户无差别修改时才值得用。如果你只是想让它能执行0755就够了。如果坚持要用777写法上有个细节要注意在YAML里mode: 0777 一定要用引号引起来。为什么因为YAML会把 0777 当成八进制数字解析不加引号在有些版本上会出现权限变成100777这类诡异情况。我当年就遇到过排查了半天才发现是引号的问题。所以记住mode字段的值统一加引号格式固定为 0755 这种四位数字。4. 综合实操用Playbook初始化一台新服务器4.1 需求定义从裸机到服务可用一键完成只分发文件还不够体现Playbook的威力我觉得最有感觉的场景是“新服务器初始化”。假设公司采购了一台Linux服务器交付到你手上时基本上就是一个只有SSH服务的“裸机”你需要做这些事情更新系统缓存、创建业务用户、安装Nginx、写入一个简单的页面、启动服务并设置开机自启。在以前这个过程大概要敲十几条命令耗时且无聊。现在用Playbook把整个流程固化成文件以后再来新机器改一下IP一键执行全部搞定。这种从“面向过程”到“面向状态”的转变就是Ansible真正改变工作效率的地方。4.2 完整Playbook拆解变量、handlers、notify的配合我直接贴一个我自己常用的初始化Playbook里面用到了变量、handlers、notify这几个重要概念- hosts: new_servers become: yes vars: app_user: devops nginx_port: 8080 tasks: - name: update apt cache apt: update_cache: yes cache_valid_time: 3600 - name: create app user user: name: {{ app_user }} state: present shell: /bin/bash groups: wheel append: yes - name: install nginx apt: name: nginx state: present - name: write custom nginx config template: src: templates/nginx.conf.j2 dest: /etc/nginx/nginx.conf notify: restart nginx - name: write custom index page copy: content: Welcome to {{ app_user }}s server dest: /var/www/html/index.html handlers: - name: restart nginx service: name: nginx state: restarted这段代码里vars定义了app_user和nginx_port两个变量后面用{{ app_user }}来引用。template模块根据模板文件生成目标配置文件而且这里有一个非常优雅的设计它搭配 notify 和 handlers 使用只有当nginx.conf文件内容发生变化时才会触发restart nginx这个handler如果配置没变就不会重启服务。这比传统脚本里“无论改没改都重启一遍”的方式要高好几个等级——不会因为重启造成短暂的服务中断。4.3 模板文件的作用配置从“改机器”变成“改模板”上面的Playbook里用到了template模块需要一个Jinja2模板文件 templates/nginx.conf.j2。这个模板的语法和普通nginx配置几乎一样只是可以把变量嵌进去比如server { listen {{ nginx_port }}; server_name _; root /var/www/html; index index.html; }为什么要用模板而不是直接把静态文件copy过去因为template可以在配置里插入变量、还能根据条件做判断。比如同一个Playbook测试环境用8080端口、生产环境用80端口只需要在vars里改一下nginx_port模板不用动。如果你的机器数量多、环境也多template的威力就非常大。另外在copy模块里其实也支持直接写content用来生成简单的文本文件比如上面那个index.html的例子如果不指定src而用contentAnsible就会直接把这段文字写成目标文件。这在写一些简单的占位页面、配置提示文件时非常方便不用额外建文件。4.4 执行与验证从ansible-playbook到curl执行这个初始化Playbookansible-playbook -i hosts init_server.yml执行过程中我会盯着几个关键点第一步update apt cache是否正确完成创建用户有没有报错模板有没有正常渲染最后handlers有没有被触发。如果一切正常末尾的汇总应该是没有failed。验证的时候我一般先确认服务状态ansible -i hosts new_servers -m command -a systemctl status nginx然后在浏览器或者用curl访问一下curl -I http://192.168.1.40:8080看到HTTP/1.1 200 OK就说明这台机器从裸机到服务可用的整个过程已经被一个Playbook搞定了。之后这台机器的状态被完整记录在代码里你随时可以重新执行、审核、复用。5. 常见问题与排查技巧实录把踩过的坑都填平5.1 执行失败高发原因速查表用了这么多年的Ansible我基本把新手会遇到的问题都见过了。下面这张表是我自己整理的常见问题定位指南几乎每一次排障都能从里面找到方向异常现象常见原因解决思路Host key verification failedSSH首次连接需要确认指纹交互被阻断关闭host_key_checking或预收集known_hostsPermission denied (publickey)SSH密钥未配置或用户名不对检查ssh-copy-id情况、检查ansible_user变量模块未找到或参数错误版本过于老旧导致模块名不一样用ansible-doc模块名查看当前版本支持的参数YAML语法报错缩进用了Tab、冒号后缺空格用ansible-playbook --syntax-check检查语法执行连接超时目标机防火墙或安全组拦截22端口检查systemctl status sshd、iptables/firewalld规则目标文件没变化但每次报changed模块选择不当导致不幂等优先用copy/template等声明式模块少用shell去改系统目标目录不存在导致copy报错忘建父目录先加file模块创建目录或直接用file的statedirectory表格里列的高频问题我基本都挨个踩过。最丢人的一次是把host_key_checking没关然后playbook跑了几十台机器到中间某台IP新机器时卡住等确认我还以为网络出问题了排查了大半天。后来学乖了所有新的控制机第一件事就是先看一眼ansible.cfg把该关的关掉。5.2 用 --check 预演和 -v 放大日志在真实执行任务之前我强烈建议先做一次“模拟演习”。Ansible提供了--check参数配合-C可以让playbook在“只报告、不修改”的模式下运行。它会模拟一遍任务的执行情况告诉你哪些task会发生变化、哪些会是ok。这跟面试做笔试、发版前先在测试环境跑一遍一样属于成本极低、收益极高的操作。ansible-playbook -i hosts deploy_script.yml --check如果模拟结果看着没问题再真正执行。如果执行过程中某个task失败或者你想看更详细的日志加上-v参数ansible-playbook -i hosts deploy_script.yml -v加一个v是看基本信息加两个v能看到连接过程加三个v会输出SSH协议层面的详细内容。实际排查问题的时候我一般先用-vvv看看到底卡在哪一步是网络的锅还是权限的锅。5.3 每次执行完都要看汇总数字每次执行结束Ansible会在屏幕打印一行类似这样的汇总PLAY RECAP ************************************************* 192.168.1.30 : ok4 changed1 unreachable0 failed0 192.168.1.31 : ok4 changed1 unreachable0 failed0这里的ok表示多少个任务成功changed表示多少个任务产生了实际变化unreachable表示连不上的机器failed表示执行失败。这四个数字是每次执行完最先要扫一眼的金标准。如果某个机器unreachable那说明SSH层面就有问题如果failed大于0那就必须去看是哪个task挂了。有一个细节想提醒如果同一台机器执行多次第一次changed比较大、第二次changed变成0这是好事说明你已经做到了幂等。但如果同一个playbook每次跑某些task永远都是changed那你就要警觉了——这个task可能是不幂等的每次执行都会对系统做一些永久性改动这种操作长期来看会破坏集群状态的一致性。5.4 大规模分发时注意速度和限流当初第一次给60多台机器同时跑一个Playbook结果主控节点直接卡成“假死”执行速度非常慢。后来才发现Ansible默认是并行执行所有主机的机器多了控制机需要同时开启几十个SSH连接资源被瞬间拉满。解决办法是用forks参数控制并发数。可以在ansible.cfg里设置[defaults] forks 20或者在执行命令行里加-f 20意思是同时最多20台机器并行。这样既不会压垮控制机也保留了并行效率。如果网络带宽本身就不宽还可以再把forks调低一些比如-f 10。这个参数很有用但很多人一开始不知道。另一个让大批量分发加快的办法直接用pssh、pdsh这类工具先并行把文件推到每个节点再用Ansible去确认状态。不过这种组合一般是在集群规模很大时才需要几十台机器以内默认forks调到20完全够用。反正一句话机器少无所谓机器多了务必控制并发度。5.5 排查问题三板斧ansible-doc、debug、逐步分离排障的时候我的顺序一般是这样的第一如果是对模块用法不清楚立刻执行ansible-doc 模块名看官方示例和参数说明。查文档比在搜索引擎搜零零散散的博客快得多而且不会遇到版本不匹配的问题。第二如果想看某一个变量在目标机器上的实际值用debug模块打印。比如在task里加一段- name: debug app_user variable debug: var: app_user执行的时候控制台上会直接打印出变量值。你对不上号就知道是变量定义错还是引用错了。第三如果多处报错先把复杂的Playbook简化留下最小复现步骤。比如只写一个task去测试网络只写一个task去测试权限逐段缩小范围。我排查过一个“文件总是分发失败”的问题最后把范围缩小到目标机磁盘已满跟Ansible本身一点关系都没有。这种排查方式虽然“原始”但条理清晰比瞎猜强一百倍。个人体会与后续进阶方向玩下来最大的体会是Ansible的Playbook本质上是在写“期望状态”而不是写“操作步骤”。你要做的不是告诉机器“先做A再做B如果C则D”而是描述“最后这台机器应该是什么样”。这种思维转变需要点时间适应一旦适应了你的运维工作会从“爱怎么做怎么做”变成“有据可依、可审计、可重放”。刚开始接触时我建议你先不要管什么角色、目录规范、ansible-galaxy这些高级东西就老老实实把单文件Playbook写好把copy、file、yum、service这几个模块用顺。等你发现单文件越来越长、同一个任务在多个Playbook里重复出现时再去研究roles角色。对大多数运维老哥来说用Ansible解决最实际的重复性工作比如批量分发文件、修改配置、安装软件就已经能省下很长一部分时间了。如果你有跟我相似的经历最近正好在折腾批量管理一堆Linux机器我强烈建议你从今天开始找一个简单的场景比如给一批机器分发hosts文件把第一个Playbook写起来。别等学会了再动手边用边学才是Ansible真正能落地的方式。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进