使用Ansible批量管理+更新产品环境服务器配置

文章目录

背景

我司生产环境的服务器,每年有2-3次完整的批量重建所有服务器用于升级OS、JDK、Tomcat等。在2次重建服务器间隔期间,难免会有一些其他改动需要应用,例如:OS的漏洞补丁,一些特定服务的配置新增、修改、更新等。这些改动如果都要应用在所有服务器上,面对几十甚至几百台服务器的情况下,不可能人工登录到每台服务器上去应用改变,所以需要

  1. 把要应用的改动写成脚本并提交到git库方便后续的跟踪、审查
  2. 批量ssh到服务器上执行该脚本

Ansible就是干这个事的。最终我们的git库存储的就是一系列Ansible脚本

开发环境搭建

在本地写ansible脚本时,我推荐安装Ansible Development Tools + VSCode插件一起开发。由于还需要Python环境,我推荐使用Poetry来管理虚拟环境进行隔离。以Linux为例

  1. 安装poetry并初始化项目

    shell 复制代码
    dnf install pipx
    pipx install poetry
    mkdir ansible-scripts && cd ansible-scripts
    git init
    poetry init
  2. 进入虚拟环境并安装Ansible Development Tools

    shell 复制代码
    eval $(poetry env activate)
    poetry add ansible-dev-tools
    adt --version
  3. 配置VSCode并配置ansible-lint检测ansible脚本中的不规范问题.先安装Ansible插件

    shell 复制代码
    mkdir .vscode && cd .vscode
    vim settings.json

    settings.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个网站,基本会分为几大步

  1. 部署数据库
  2. 部署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大步又分为很多具体细节的小步骤

  1. 对于部署数据库而言,要安装数据库,配置用户、权限、创建表结构、写入一些基本的网站要用的SQL数据等
  2. 对于部署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也有好处

相关推荐
用户03284722207011 小时前
如何搭建本地yum源(上)
运维
宋均浩2 天前
# GitHub Actions 实战:从零搭建 CI/CD 流水线的 5 个核心配置
ci/cd
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz3 天前
Maven依赖冲突
java·服务器·maven
Inhand陈工3 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智3 天前
ARP代理--工作原理
运维·网络·arp·arp代理
shushangyun_3 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
古城小栈4 天前
Unix 与 Linux 异同小叙
linux·服务器·unix