文章目录背景开发环境搭建创建Inventory文件Play vs Task vs Playbook实战简单更新文件命令解释动态传入服务器名字在远程主机上以root用户执行命令使用copy模块复制文件到远程检查脚本运行命令并发执行命令在配置文件中新增配置命令解释运行命令更新配置并重启相关服务命令解释重启服务运行命令升级Linux内核修复CVE漏洞命令解释静态变量动态变量版本比较使用block进一步组织task中的步骤条件判断获取脚本task的输出并打印打印日志运行命令工程规范示例工程备注背景我司生产环境的服务器每年有2-3次完整的批量重建所有服务器用于升级OS、JDK、Tomcat等。在2次重建服务器间隔期间难免会有一些其他改动需要应用例如OS的漏洞补丁一些特定服务的配置新增、修改、更新等。这些改动如果都要应用在所有服务器上面对几十甚至几百台服务器的情况下不可能人工登录到每台服务器上去应用改变所以需要把要应用的改动写成脚本并提交到git库方便后续的跟踪、审查批量ssh到服务器上执行该脚本Ansible就是干这个事的。最终我们的git库存储的就是一系列Ansible脚本开发环境搭建在本地写ansible脚本时我推荐安装Ansible Development Tools VSCode插件一起开发。由于还需要Python环境我推荐使用Poetry来管理虚拟环境进行隔离。以Linux为例安装poetry并初始化项目dnfinstallpipx pipxinstallpoetrymkdiransible-scriptscdansible-scriptsgitinit poetry init进入虚拟环境并安装Ansible Development Toolseval$(poetryenvactivate)poetryaddansible-dev-tools adt--version配置VSCode并配置ansible-lint检测ansible脚本中的不规范问题.先安装Ansible插件mkdir.vscodecd.vscodevimsettings.jsonsettings.json内容如下{ansible.python.interpreterPath:/home/${USER}/.cache/pypoetry/virtualenvs/ansible-scripts-fiLmypOw-py3.14/bin/python,ansible.ansible.path:/home/${USER}/.cache/pypoetry/virtualenvs/ansible-scripts-fiLmypOw-py3.14/bin/ansible,ansible.validation.lint.path:/home/${USER}/.cache/pypoetry/virtualenvs/ansible-scripts-fiLmypOw-py3.14/bin/ansible-lint,ansible.ansibleNavigator.path:/home/${USER}/.cache/pypoetry/virtualenvs/ansible-scripts-fiLmypOw-py3.14/bin/ansible-navigator,files.associations:{*.yaml:ansible},[ansible]:{editor.defaultFormatter:redhat.ansible,editor.formatOnSave:true,editor.tabSize:2},yaml.validate:true,yaml.format.enable:true,[yaml]:{editor.defaultFormatter:redhat.vscode-yaml,editor.formatOnSave:true,editor.insertSpaces:true,editor.tabSize:2},ansible.validation.lint.enabled:true,ansible.validation.enabled:trueansible.lightspeed.enabled:false,chat.disableAIFeatures:true}把USER换成自己的用户即可。我这里还禁用了VSCode的AI Chat功能创建Inventory文件在实际生产环境的几百台服务器中有的可能是web应用服务器有的可能是数据库服务器还有的可能是Redis服务器等等。每种类型的服务器的数量从几台到几十台不等所以第1步我们先把这些分好类的服务器写到/etc/ansible/hosts文件中这些在逻辑上被分为一组的节点在Ansible中叫做Inventory这里以应用服务器为例[webservers] app00 app01 app02 ... app30上述写法可以进一步简写成[webservers] app[00:30]官方把这种简写称为ranges of hosts如何验证上述简写最终被ansible识别的服务器列表是我们所期望的呢? 使用简单的ansible命令加上–list-hosts参数即可ansible webservers --list-hosts输出如下hosts(31)app00 app01... app30对于Inventory的分组更复杂的例子见官方文档Play vs Task vs Playbook官方文档中关于它们的定义简单理解为当要做个事时例如部署1个网站基本会分为几大步部署数据库部署web程序这里的每1大步就叫做paly它们结合起来的完整工作流就叫做play-book----name:Deploy Databasehosts:dbtasks:-name:Install MySQL...-name:Deploy Webhosts:webtasks:-name:Install Nginx...所以Paly就是对哪些主机hosts执行什么任务tasks这2大步又分为很多具体细节的小步骤对于部署数据库而言要安装数据库配置用户、权限、创建表结构、写入一些基本的网站要用的SQL数据等对于部署web程序而言要安装nginx配置nginx部署代码等而这些小步骤就叫做play中的taskPlaybook │ ├─ Play(db) │ ├─ Task │ ├─ Task │ └─ Task │ └─ Play(web) ├─ Task ├─ Task └─ Task所以Task就是在目标主机上执行的一步操作实战简单更新文件最简单的例子是把1个新的配置文件复制到远程同时保证新文件的所属组合权限正确并备忘老文件play脚本如下----name:deploy jmx exporter configurationhosts:{{ target_host | default(webservers) }}become:yestasks:-name:copy jmx_exporter.yaml to target positionansible.builtin.copy:src:./jmx_exporter.yamldest:/etc/otelcol/jmx_exporter.yamlowner:rootgroup:rootmode:0644backup:yes命令解释动态传入服务器名字这里的hosts我们没有写死采取动态变量传入为什么呢因为在实际场景中对于配置的改动我们会采取渐进式应用不会一下子全都应用。所以每次执行脚本时远程服务器的名字都不是固定的定义1个名为target_host变量然后再执行命令时通过–extra-vars 参数传递--extra-varstarget_hostapp00,app01,app02假如我每次需要执行10台服务器那传递变量岂不是要从app00一直写到app10既然Inventory文件中的服务器列表可以简写那这里是不是也可以简写官方提供了切片的匹配模式即可以指定某个分组的开始下标和结束下标索引位置来指定服务器名字上述命令就可以简写成--extra-varstarget_hostapp[00:10]这样就可以本次只更新app00到app10 这11台服务器如果想更新appp15到25则--extra-varstarget_hostapp[15:25]注意 这里切片取的是某个分组服务器数组列表的索引而不是服务器名字。如果你的webservers分组没有app00,而是从app01开始的那么在更新app01-05时上述传递参数时不要写成app[01:05]这会取成app02-06正确写法是app[00:04]在远程主机上以root用户执行命令上述文件位置只有root用户才可以访问所以需要我们当前用户ssh到远程后切换到root用户这一动作通过使用become和become_user来实现。become是启用提升权限功能become_user默认是root当然前提是当前用户在远程服务器上属于root用户组在执行命令时需要加入-k和-K选项, 小写的k是询问当前用户ssh到远程服务器密码大写的K是询问在远程服务器上执行sudo输入的密码它们一般是相同的所以在执行命令时提示输入密码后再次提示输入becom密码则直接回车即可-k-K使用copy模块复制文件到远程使用copy模块的参数说明见官方文档刚开始接触ansible时实现一个需求的时候不知道官方哪些模块支持可以在Ansible 文档 Collection Index页面查找对于大部分简单需求都可以在Ansible Builtin模块中找到同时在task中使用模块时尽量都是以模块的全路径名即Fully Qualified Collection Name FQCN这一点官方在最佳实践中也建议这么做。对于内置的模块例如copy模块虽然可以不用加ansible.builtin前路径也可以被正常识别但是最好加上For builtin modules and plugins, use the ansible.builtin collection name as a prefix, for example, ansible.builtin.copy.检查脚本写完脚本后先使用ansible-lint检查一下当前脚本是否满足规范ansilbe-lint copy_jmx_config.yml其他更多检查见文档运行命令并发执行命令对于多台服务器ansible是并发执行play脚本的默认值是5. 也就说是如果传入的target_host是00-14那么ansible会分为3次循环执行完毕如果想1次执行完毕即可在传入命令时使用–forks参数--forks15也可以在ansible的配置文件中修改有了上述知识下面的命令应该就能看懂了以当前用户执行ansible-play命令回车后会提示输入 ssh到远程服务器的密码(-k选项)然后会提示输入 切换为root用户的sudo的密码(-K选项)直接默认回车和-k密码保持一致然后脚本开始执行一次并发直接对11台服务器同时执行上述逻辑ansible-playbook /home/tomcat/ansible/eng-15996/copy_jmx_config.yml--forks11--extra-varstarget_hostapp[00:10]-k-K在配置文件中新增配置现在需求是要为Tomcat的server.xml中新增一个datasource这个需求可以使用blockinfile模块实现这是针对文本块的操作----name:Add PostgreSQL Resource to Tomcat server.xmlhosts:{{ target_host | default(webservers) }}become:truebecome_user:tomcattasks:-name:Insert PostgreSQL Resource into GlobalNamingResourcesansible.builtin.blockinfile:path:/usr/local/tomcat/conf/server.xmlbackup:yesinsertbefore:/GlobalNamingResourcesmarker:!-- {mark} ANSIBLE MANAGED JDBC RESOURCE --block:|Resource namejdbc/prod_postgres usernameuser_dml passwordtest authContainer driverClassNameorg.postgresql.Driver logAbandonedtrue maxActive20 maxIdle5 minIdle2 maxWait10000 suspectTimeout60 removeAbandonedtrue removeAbandonedTimeout120 abandonWhenPercentageFull10 testOnBorrowtrue typejavax.sql.DataSource factoryorg.apache.tomcat.jdbc.pool.DataSourceFactory urljdbc:postgresql://test-pg-1.us-west-2.rds.amazonaws.com/prod?currentSchemaprod_app validationInterval60000 validationQueryselect 1 validationQueryTimeout3 jdbcInterceptorsorg.apache.tomcat.jdbc.pool.interceptor.ConnectionState;org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer;org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer defaultAutoCommittrue/命令解释写入位置是server.xml的/GlobalNamingResources标签之前写入内容block是一个多行内容使用 | 标记| 属于 yaml 多行字符串语法运行命令ansible-playbook /home/tomcat/ansible/adm-3888/add_tomcat_resource.yml--forks10--extra-varstarget_serverapp[00:09]-k-K更新配置并重启相关服务有时候仅仅修改配置还不够我们需要在修改配置后使用systemd重启相关服务使其重新加载新的配置----name:Add local probehosts:{{ target_host | default(webservers) }}become:truehandlers:-name:restart custom_metricsansible.builtin.systemd:name:custom_metricsstate:restarted-name:restart otelcolansible.builtin.systemd:name:otelcolstate:restartedtasks:-name:Update metrics.pyansible.builtin.copy:src:./metrics.pydest:/opt/common/python/custom_metrics/metrics.pyowner:rootgroup:rootmode:0644backup:truenotify:restart custom_metrics-name:Update custom_metrics.pyansible.builtin.copy:src:./custom_metrics.pydest:/opt/common/python/custom_metrics/custom_metrics.pyowner:rootgroup:rootmode:0755backup:truenotify:restart custom_metrics-name:Update otelcol to add jsp metrics filteransible.builtin.lineinfile:path:/etc/otelcol/config.yamlinsertafter:^\s-\stop_processes_memory_usageline: - jsp_.*state:presentbackup:truenotify:restart otelcol命令解释对于文件的改动除了之前copy模块外这次还需要在配置文件中的某一行后写入1行新配置对于文本中某一行的改动不使用blockinfile而是使用lineinfile重启服务通过Handler来实现先创建handler并命名然后在task中使用notify来通知handler执行重启服务的任务运行命令ansible-playbook /home/tomcat/ansible/adm-5252/add_local_probe.yml--forks7--extra-varstarget_hostapp[03:09]-K-k升级Linux内核修复CVE漏洞对于最近的Linux内核漏洞CVE-2026-31431及其后续漏洞官方出了patch之后需要及时应用到生产环境中。我们使用的是Rocky8漏洞修复的内核版本为4.18.0-553.124----name:Kernel Update and System Cleanup for CVE-2026-31431hosts:{{ target_host | default(all) }}become:yesvars:# CVE-2026-31431 is fixed in kernel 4.18.0-553.123 or higherfixed_kernel_version:4.18.0-553.124tasks:-name:Get current kernel version# Get the kernel version from ansible_factsset_fact:current_kernel:{{ ansible_facts[ansible_kernel] }}-name:Display current kernel versionansible.builtin.debug:msg:Current kernel version is {{ current_kernel }}-name:Check if kernel is affected by CVE-2026-31431# Compare current version with the fixed versionset_fact:is_affected:{{ current_kernel is version(fixed_kernel_version, ) }}-name:Perform updates if the system is affectedblock:-name:Update all packages except temurin-17-jdk# dnf -y --excludetemurin-17-jdk updateansible.builtin.dnf:name:*state:latestexclude:temurin-17-jdkupdate_cache:yes-name:Remove setroubleshoot packages# dnf -y remove setroubleshoot*ansible.builtin.dnf:name:setroubleshoot*state:absent-name:Run dnf autoremove# dnf -y autoremoveansible.builtin.dnf:autoremove:yes-name:Identify the latest kernel installed on disk# Query RPM to see the newest kernel version that will load after rebootansible.builtin.shell:rpm -q kernel --queryformat %{VERSION}-%{RELEASE}.%{ARCH}\n | tail -n 1register:installed_kernelchanged_when:false-name:Final status report and reboot requirementansible.builtin.debug:msg:--------------------------------------------------PATCHING SUMMARY:-1. Currently Active Kernel: {{ current_kernel }}-2. Newly Installed Kernel: {{ installed_kernel.stdout }}--------------------------------------------------ACTION REQUIRED:-reboot the server-------------------------------------------------when:is_affected-name:Notify if no action is requiredansible.builtin.debug:msg:System kernel is already at or above the secure version. No patching needed.when:not is_affected命令解释静态变量使用vars定义静态变量fixed_kernel_version代表内核版本为此版本的服务器不需要升级内核动态变量我们即使重新创建服务器也没有特意跟踪个每台服务器的内核版本所以每一台服务器的具体内核版本是未知的需要在脚本中动态获取。对于这种系统级的信息ansible也默认提供了工具叫facts它的结构包含的内容非常多具体信息可参考facts文档。也可以在本机执行ansible localhost-msetup查看当前机器的ansible facts的所有信息。既然ansible提供了这些信息直接根据它的语法去拿即可拿到之后通过set_fact把需要的信息设置到变量current_kernel中版本比较对于这种条件比较ansible同样提供了内置工具。不得不说ansible还是太全面了.工具是verstion_test具体用法可语法课参考文档链接使用block进一步组织task中的步骤如果内核需要更新那么需要执行4个子任务这种task中的task可以通过block搭配条件判断来实现即block中的task要么都执行要么都不执行条件判断使用最基本的when来进行判断获取脚本task的输出并打印通过register来获得当前task的输出每个模块结束之后都会返回一个结构化的数据里面包含了很多项。这些数据可以随着register注入到声明的变量中。stdout也包含在内所以后续可以直接调用installed_kernel.stdout。 所有数据项见Return Values文档打印日志普通的打印日志使用内置的debug模块运行命令ansible-playbook /home/tomcat/ansible/adm-5240/patch_kernel.yml --extra-varstarget_serverapp[03:05]-K-k工程规范对于更复杂的企业级项目来说ansible官方也有对于工程目录的建议. 一些更通用的建议见Tips文档示例工程更详细的一个示例见源码仓库。 这个工程是我使用 ClaudeCode Agent KIMI 大模型 Superpowers做的。99%工作都由AI完成。我做了一些需求的澄清、确认和最终工程部分结果的验证按照文档说明即可启动使用。可放心食用备注Ansible的东西非常多在平时使用时如果不清楚的地方可以直接问AI或者干脆让AI来完成Ansible脚本的书写。我现在一般交给AI来写Ansible脚本了只不过从个人学习的角度来看还是要熟悉一下没有AI的日子是如何学习新东西的这对以后更好的使用AI也有好处