文章目录
背景
我司生产环境的服务器,每年有2-3次完整的批量重建所有服务器用于升级OS、JDK、Tomcat等。在2次重建服务器间隔期间,难免会有一些其他改动需要应用,例如:OS的漏洞补丁,一些特定服务的配置新增、修改、更新等。这些改动如果都要应用在所有服务器上,面对几十甚至几百台服务器的情况下,不可能人工登录到每台服务器上去应用改变,所以需要
- 把要应用的改动写成脚本并提交到git库方便后续的跟踪、审查
- 批量ssh到服务器上执行该脚本
Ansible就是干这个事的。最终我们的git库存储的就是一系列Ansible脚本
开发环境搭建
在本地写ansible脚本时,我推荐安装Ansible Development Tools + VSCode插件一起开发。由于还需要Python环境,我推荐使用Poetry来管理虚拟环境进行隔离。以Linux为例
-
安装poetry并初始化项目
shelldnf install pipx pipx install poetry mkdir ansible-scripts && cd ansible-scripts git init poetry init -
进入虚拟环境并安装Ansible Development Tools
shelleval $(poetry env activate) poetry add ansible-dev-tools adt --version -
配置VSCode并配置ansible-lint检测ansible脚本中的不规范问题.先安装Ansible插件
shellmkdir .vscode && cd .vscode vim settings.jsonsettings.json内容如下
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": true "ansible.lightspeed.enabled": false, "chat.disableAIFeatures": true }把USER换成自己的用户即可。我这里还禁用了VSCode的AI Chat功能
创建Inventory文件
在实际生产环境的几百台服务器中,有的可能是web应用服务器,有的可能是数据库服务器,还有的可能是Redis服务器等等。每种类型的服务器的数量从几台到几十台不等,所以第1步我们先把这些分好类的服务器写到 /etc/ansible/hosts 文件中,这些在逻辑上被分为一组的节点在Ansible中叫做Inventory
这里以应用服务器为例
ini
[webservers]
app00
app01
app02
...
app30
上述写法可以进一步简写成
ini
[webservers]
app[00:30]
官方把这种简写称为ranges of hosts
如何验证上述简写最终被ansible识别的服务器列表是我们所期望的呢? 使用简单的ansible命令加上--list-hosts参数即可
shell
ansible webservers --list-hosts
输出如下
shell
hosts(31)
app00
app01
...
app30
对于Inventory的分组更复杂的例子,见官方文档
Play vs Task vs Playbook
官方文档中关于它们的定义
简单理解为,当要做个事时,例如部署1个网站,基本会分为几大步
- 部署数据库
- 部署web程序
这里的每1大步就叫做paly,它们结合起来的完整工作流就叫做play-book
yaml
---
- name: Deploy Database
hosts: db
tasks:
- name: Install MySQL
...
- name: Deploy Web
hosts: web
tasks:
- name: Install Nginx
...
所以Paly就是:对哪些主机(hosts)执行什么任务(tasks)
这2大步又分为很多具体细节的小步骤
- 对于部署数据库而言,要安装数据库,配置用户、权限、创建表结构、写入一些基本的网站要用的SQL数据等
- 对于部署web程序而言,要安装nginx,配置nginx,部署代码等
而这些小步骤,就叫做play中的task
yaml
Playbook
│
├─ Play(db)
│ ├─ Task
│ ├─ Task
│ └─ Task
│
└─ Play(web)
├─ Task
├─ Task
└─ Task
所以Task就是在目标主机上执行的一步操作
实战
简单更新文件
最简单的例子是把1个新的配置文件复制到远程,同时保证新文件的所属组合权限正确并备忘老文件
play脚本如下
yaml
---
- name: deploy jmx exporter configuration
hosts: "{{ target_host | default('webservers') }}"
become: yes
tasks:
- name: copy jmx_exporter.yaml to target position
ansible.builtin.copy:
src: ./jmx_exporter.yaml
dest: /etc/otelcol/jmx_exporter.yaml
owner: root
group: root
mode: '0644'
backup: yes
命令解释
动态传入服务器名字
这里的hosts我们没有写死,采取动态变量传入,为什么呢?因为在实际场景中,对于配置的改动,我们会采取渐进式应用,不会一下子全都应用。所以每次执行脚本时,远程服务器的名字都不是固定的
定义1个名为target_host变量,然后再执行命令时通过--extra-vars 参数传递
shell
--extra-vars "target_host=app00,app01,app02"
假如我每次需要执行10台服务器,那传递变量岂不是要从app00一直写到app10,既然Inventory文件中的服务器列表可以简写,那这里是不是也可以简写?
官方提供了切片的匹配模式,即可以指定某个分组的开始下标和结束下标索引位置来指定服务器名字,上述命令就可以简写成
shell
--extra-vars "target_host=app[00:10]"
这样就可以本次只更新app00到app10 这11台服务器,如果想更新appp15到25,则
shell
--extra-vars "target_host=app[15:25]"
注意: 这里切片取的是某个分组服务器数组列表的索引而不是服务器名字。如果你的webservers分组没有app00,而是从app01开始的,那么在更新app01-05时,上述传递参数时,不要写成app01:05,这会取成app02-06,正确写法是app00:04
在远程主机上以root用户执行命令
上述文件位置只有root用户才可以访问,所以需要我们当前用户ssh到远程后切换到root用户,这一动作通过使用become和become_user来实现。become是启用提升权限功能,become_user默认是root
当然前提是当前用户在远程服务器上属于root用户组
在执行命令时需要加入-k和-K选项, 小写的k是询问当前用户ssh到远程服务器密码,大写的K是询问在远程服务器上执行sudo输入的密码,它们一般是相同的,所以在执行命令时,提示输入密码后再次提示输入becom密码,则直接回车即可
shell
-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检查一下当前脚本是否满足规范
shell
ansilbe-lint copy_jmx_config.yml
其他更多检查见文档
运行命令
并发执行命令
对于多台服务器,ansible是并发执行play脚本的,默认值是5. 也就说是,如果传入的target_host是00-14,那么ansible会分为3次循环执行完毕,如果想1次执行完毕,即可在传入命令时使用--forks参数
shell
--forks 15
也可以在ansible的配置文件中修改
有了上述知识,下面的命令应该就能看懂了:
以当前用户执行ansible-play命令,回车后,会提示输入 ssh到远程服务器的密码(-k选项),然后会提示输入 切换为root用户的sudo的密码(-K选项),直接默认回车和-k密码保持一致,然后脚本开始执行,一次并发直接对11台服务器同时执行上述逻辑
shell
ansible-playbook /home/tomcat/ansible/eng-15996/copy_jmx_config.yml --forks 11 --extra-vars "target_host=app[00:10]" -k -K
在配置文件中新增配置
现在需求是要为Tomcat的server.xml中新增一个datasource,这个需求可以使用blockinfile模块实现,这是针对文本块的操作
yaml
---
- name: Add PostgreSQL Resource to Tomcat server.xml
hosts: "{{ target_host | default('webservers') }}"
become: true
become_user: tomcat
tasks:
- name: Insert PostgreSQL Resource into GlobalNamingResources
ansible.builtin.blockinfile:
path: /usr/local/tomcat/conf/server.xml
backup: yes
insertbefore: "</GlobalNamingResources>"
marker: "<!-- {mark} ANSIBLE MANAGED JDBC RESOURCE -->"
block: |
<Resource name="jdbc/prod_postgres" username="user_dml" password="test"
auth="Container"
driverClassName="org.postgresql.Driver"
logAbandoned="true"
maxActive="20"
maxIdle="5"
minIdle="2"
maxWait="10000"
suspectTimeout="60"
removeAbandoned="true"
removeAbandonedTimeout="120"
abandonWhenPercentageFull="10"
testOnBorrow="true"
type="javax.sql.DataSource"
factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
url="jdbc:postgresql://test-pg-1.us-west-2.rds.amazonaws.com/prod?currentSchema=prod_app"
validationInterval="60000"
validationQuery="select 1"
validationQueryTimeout="3"
jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.ConnectionState;org.apache.tomcat.jdbc.pool.interceptor.StatementFinalizer;org.apache.tomcat.jdbc.pool.interceptor.ResetAbandonedTimer"
defaultAutoCommit="true"/>
命令解释
写入位置是server.xml的</GlobalNamingResources>标签之前,写入内容block是一个多行内容,使用 | 标记,| 属于 yaml 多行字符串语法
运行命令
shell
ansible-playbook /home/tomcat/ansible/adm-3888/add_tomcat_resource.yml --forks 10 --extra-vars "target_server=app[00:09]" -k -K
更新配置并重启相关服务
有时候仅仅修改配置还不够,我们需要在修改配置后使用systemd重启相关服务使其重新加载新的配置
yaml
---
- name: Add local probe
hosts: "{{ target_host | default('webservers') }}"
become: true
handlers:
- name: restart custom_metrics
ansible.builtin.systemd:
name: custom_metrics
state: restarted
- name: restart otelcol
ansible.builtin.systemd:
name: otelcol
state: restarted
tasks:
- name: Update metrics.py
ansible.builtin.copy:
src: ./metrics.py
dest: /opt/common/python/custom_metrics/metrics.py
owner: root
group: root
mode: '0644'
backup: true
notify: restart custom_metrics
- name: Update custom_metrics.py
ansible.builtin.copy:
src: ./custom_metrics.py
dest: /opt/common/python/custom_metrics/custom_metrics.py
owner: root
group: root
mode: '0755'
backup: true
notify: restart custom_metrics
- name: Update otelcol to add jsp metrics filter
ansible.builtin.lineinfile:
path: /etc/otelcol/config.yaml
insertafter: '^\s+-\s+top_processes_memory_usage'
line: ' - jsp_.*'
state: present
backup: true
notify: restart otelcol
命令解释
对于文件的改动,除了之前copy模块外,这次还需要在配置文件中的某一行后写入1行新配置,对于文本中某一行的改动不使用blockinfile,而是使用lineinfile
重启服务
通过Handler来实现,先创建handler并命名,然后在task中使用notify来通知handler执行重启服务的任务
运行命令
shell
ansible-playbook /home/tomcat/ansible/adm-5252/add_local_probe.yml --forks 7 --extra-vars "target_host=app[03:09]" -K -k
升级Linux内核修复CVE漏洞
对于最近的Linux内核漏洞CVE-2026-31431及其后续漏洞,官方出了patch之后,需要及时应用到生产环境中。我们使用的是Rocky8,漏洞修复的内核版本为4.18.0-553.124
yaml
---
- name: Kernel Update and System Cleanup for CVE-2026-31431
hosts: "{{ target_host | default('all') }}"
become: yes
vars:
# CVE-2026-31431 is fixed in kernel 4.18.0-553.123 or higher
fixed_kernel_version: "4.18.0-553.124"
tasks:
- name: Get current kernel version
# Get the kernel version from ansible_facts
set_fact:
current_kernel: "{{ ansible_facts['ansible_kernel'] }}"
- name: Display current kernel version
ansible.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 version
set_fact:
is_affected: "{{ current_kernel is version(fixed_kernel_version, '<') }}"
- name: Perform updates if the system is affected
block:
- name: Update all packages except temurin-17-jdk
# dnf -y --exclude=temurin-17-jdk update
ansible.builtin.dnf:
name: "*"
state: latest
exclude: temurin-17-jdk
update_cache: yes
- name: Remove setroubleshoot packages
# dnf -y remove setroubleshoot*
ansible.builtin.dnf:
name: "setroubleshoot*"
state: absent
- name: Run dnf autoremove
# dnf -y autoremove
ansible.builtin.dnf:
autoremove: yes
- name: Identify the latest kernel installed on disk
# Query RPM to see the newest kernel version that will load after reboot
ansible.builtin.shell: "rpm -q kernel --queryformat '%{VERSION}-%{RELEASE}.%{ARCH}\n' | tail -n 1"
register: installed_kernel
changed_when: false
- name: Final status report and reboot requirement
ansible.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 required
ansible.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文档。也可以在本机执行
shell
ansible localhost -m setup
查看当前机器的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模块
运行命令
shell
ansible-playbook /home/tomcat/ansible/adm-5240/patch_kernel.yml --extra-vars "target_server=app[03:05]" -K -k
工程规范
对于更复杂的企业级项目来说,ansible官方也有对于工程目录的建议. 一些更通用的建议见Tips文档
示例工程
更详细的一个示例见源码仓库。 这个工程是我使用 ClaudeCode Agent + KIMI 大模型 + Superpowers做的。99%工作都由AI完成。
我做了一些需求的澄清、确认和最终工程部分结果的验证,按照文档说明即可启动,使用。可放心食用
备注
Ansible的东西非常多,在平时使用时,如果不清楚的地方可以直接问AI,或者干脆让AI来完成Ansible脚本的书写。我现在一般交给AI来写Ansible脚本了,只不过从个人学习的角度来看,还是要熟悉一下没有AI的日子是如何学习新东西的,这对以后更好的使用AI也有好处