Linux 基础体系 · 第 52/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Ansible Linux 自动化:Inventory、Playbook、幂等、Secret 和滚动变更

Ansible 是一种以控制节点为中心的自动化工具。控制节点读取 Inventory,根据 Playbook 描述目标状态,通过 SSH 连接 Linux 主机并调用模块完成变更。大多数场景不需要在被管理主机上运行常驻 Agent;被管理主机通常只需要 SSH、Python 和执行目标模块所需的系统工具。

本文围绕五个核心问题展开:

  1. Inventory 如何把主机、分组和变量组织成可执行对象;
  2. Playbook 如何描述任务、条件、处理器和权限;
  3. 幂等性是什么,模块如何判断“已经完成”;
  4. Secret 如何进入自动化流程而不被日志、差异输出和文件权限泄露;
  5. 滚动变更如何控制并发、验证健康状态,并处理失败和恢复。

示例面向现代主流 Linux 发行版,命令假设控制节点已安装 ansible-core,被管理主机可通过 SSH 登录。Debian/Ubuntu 与 RHEL/Rocky/AlmaLinux 在包名、服务名、配置路径和 SELinux 行为上可能不同,示例会明确指出这些边界。


一、Ansible 的执行模型:控制节点、Inventory、模块和状态

一次典型执行可以抽象为:

控制节点
  │
  ├─ 读取 ansible.cfg
  ├─ 解析 Inventory
  ├─ 读取 Playbook、变量和 Vault
  ├─ 为每个主机选择任务
  ├─ 通过 SSH 连接被管理主机
  ├─ 上传并执行模块
  └─ 根据模块返回值决定 changed、ok 或 failed
       │
       ▼
被管理 Linux 主机
  ├─ 检查当前状态
  ├─ 执行必要变更
  └─ 返回结构化结果

Ansible 任务的核心不是“执行一条命令”,而是向模块提供目标状态。例如:

- name: 确保 nginx 已安装
  ansible.builtin.package:
    name: nginx
    state: present

这里的目标状态是“包 nginx 存在”。模块会先查询当前状态:

  • 如果包已安装,返回 ok,通常不会改变系统;
  • 如果包不存在,安装后返回 changed
  • 如果包管理器失败,返回 failed

相比之下:

- name: 直接安装 nginx
  ansible.builtin.shell: apt-get install -y nginx

只表达了“执行这个命令”,没有可靠地表达当前状态。它可能重复执行、产生不同副作用,也无法跨发行版工作。shellcommand 并非不能使用,但应当只在没有合适模块,或者必须调用专用 CLI 时使用,并额外定义判断条件。

1. 控制节点的最小安装

在 Debian/Ubuntu 控制节点上:

python3 -m pip install --user ansible-core
ansible --version

在生产环境中,应使用虚拟环境或发行版软件包固定版本,避免控制节点升级后改变模块行为:

python3 -m venv .venv
. .venv/bin/activate
python -m pip install 'ansible-core>=2.15,<2.18'
ansible --version

ansible-core 提供执行引擎和内置模块;某些第三方模块属于 Ansible Collection,需要通过 ansible-galaxy collection install 单独安装。本文使用的模块均以 ansible.builtin 开头,表示来自核心内置集合。

2. SSH 是自动化的传输层

Ansible 默认使用 SSH。先不要急于运行 Playbook,应先验证人工 SSH:

ssh -o BatchMode=yes web-01 'uname -a && id'

BatchMode=yes 会禁止交互式密码询问,适合验证自动化是否真的具备无交互登录条件。

建议在 ~/.ssh/config 中为自动化定义独立身份:

Host web-*
    User ansible
    IdentityFile ~/.ssh/ansible_ed25519
    IdentitiesOnly yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

如果目标网络必须经过跳板机,可以使用:

Host web-*
    ProxyJump bastion.example.com

连接复用可以减少大量主机执行时的 SSH 建连开销:

Host *
    ControlMaster auto
    ControlPersist 60s
    ControlPath ~/.ssh/control-%C

但控制套接字包含可复用的认证连接,目录权限必须严格,例如:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/config ~/.ssh/ansible_ed25519

不要通过关闭主机密钥检查来“解决”首次连接问题。应预先审计并写入 known_hosts

ssh-keyscan -H web-01.example.com >> ~/.ssh/known_hosts

ssh-keyscan 只负责获取公钥,不能证明该公钥可信;生产环境应通过云平台、主机控制台或带外渠道核对指纹。


二、Inventory:主机、分组与变量作用域

Inventory 是 Ansible 的目标主机集合,同时可以定义主机分组和连接变量。它不是单纯的 IP 地址列表,因为 Playbook 通常面向“角色组”编写,例如 webserversdbservers,而不是面向某个固定主机名。

1. 一个可运行的 YAML Inventory

创建 inventory/production.yml

all:
  vars:
    ansible_user: ansible
    ansible_python_interpreter: /usr/bin/python3

  children:
    webservers:
      hosts:
        web-01:
          ansible_host: 192.0.2.11
        web-02:
          ansible_host: 192.0.2.12

    dbservers:
      hosts:
        db-01:
          ansible_host: 192.0.2.21

这里有三个不同概念:

  • web-01 是 Inventory 中的逻辑主机名;
  • ansible_host 是实际连接地址;
  • webservers 是主机组,Playbook 可以用它作为目标。

检查解析结果:

ansible-inventory \
  -i inventory/production.yml \
  --graph

可能输出:

@all:
  |--@ungrouped:
  |--@webservers:
  |  |--web-01
  |  |--web-02
  |--@dbservers:
     |--db-01

查看 Ansible 合并后的主机变量:

ansible-inventory \
  -i inventory/production.yml \
  --host web-01

测试连通性:

ansible \
  -i inventory/production.yml \
  webservers \
  -m ansible.builtin.ping

ping 模块不是 ICMP Ping。它会通过 SSH 执行模块,验证登录、Python 和模块执行路径。成功通常类似:

web-01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

2. 分组是批量运维的边界

可以用组表达拓扑关系:

all:
  children:
    production:
      children:
        webservers:
        dbservers:

于是以下命令只选生产环境的 Web 节点:

ansible-playbook \
  -i inventory/production.yml \
  site.yml \
  --limit 'production:&webservers'

常见选择表达式包括:

  • webservers:选择该组;
  • web-01:选择单个主机;
  • webservers:dbservers:并集;
  • webservers:&production:交集;
  • webservers:!web-02:排除主机。

--limit 是生产执行的重要安全边界。即使 Playbook 的 hosts 写成 webservers,也可以用 --limit web-01 把第一次变更缩小到一台机器。

3. 变量与事实

变量可以来自 Inventory、group_varshost_vars、Playbook、命令行和 Vault。变量最终会按照 Ansible 的变量优先级合并,具体优先级较复杂,不能简单理解为“后面文件一定覆盖前面文件”。

一种常见目录:

.
├── ansible.cfg
├── inventory/
│   └── production.yml
├── group_vars/
│   └── webservers.yml
├── host_vars/
│   └── web-01.yml
└── site.yml

group_vars/webservers.yml

http_port: 8080
health_path: /health

主机事实由 gather_facts: true 收集,例如:

ansible_facts['os_family']
ansible_facts['distribution']
ansible_facts['distribution_major_version']
ansible_facts['service_mgr']

事实不是静态配置,而是执行时从主机采集的当前状态。因此它可能受到权限、网络、Python、事实缓存和发行版差异影响。


三、Playbook:把目标状态组织成可执行流程

Playbook 是 YAML 文件,包含一个或多个 Play。每个 Play 至少定义目标主机和任务:

- name: 配置 Web 服务器
  hosts: webservers
  become: true
  gather_facts: true

  tasks:
    - name: 确保 nginx 已安装
      ansible.builtin.package:
        name: nginx
        state: present
  • hosts 决定目标集合;
  • become: true 表示需要通过 sudo 等机制提升权限;
  • gather_facts 控制是否收集系统事实;
  • tasks 按顺序执行;
  • 每个任务通常调用一个模块。

1. 权限模型

Inventory 中的 ansible_user 是 SSH 登录用户,become_user 是提权后的目标用户。它们不是一回事:

- name: 以 ansible 用户登录并以 root 执行
  hosts: webservers
  become: true
  become_user: root
  tasks:
    - name: 写入 root 文件
      ansible.builtin.copy:
        content: "managed by ansible\n"
        dest: /etc/wr-managed
        owner: root
        group: root
        mode: '0644'

执行前提是 ansible 用户具备免密 sudo 或可由 Ansible 提供 sudo 密码:

ansible-playbook \
  -i inventory/production.yml \
  site.yml \
  --ask-become-pass

不要为了“方便”给自动化账号无限制的 NOPASSWD: ALL,除非已经接受该账号被接管后等同于 root 的风险。更严格的做法是限制可执行命令、限制来源网络,并把 SSH 私钥放在专用控制节点。

2. 条件与发行版差异

ansible.builtin.package 可以抽象大多数安装动作,但包名本身仍可能不同。可以按事实选择变量:

- name: 根据发行版选择服务包
  ansible.builtin.set_fact:
    web_package: >-
      {{
        'apache2'
        if ansible_facts['os_family'] == 'Debian'
        else 'httpd'
      }}

- name: 安装 Web 服务
  ansible.builtin.package:
    name: "{{ web_package }}"
    state: present

这里的条件只处理 Debian 系和 Red Hat 系的一般情况。SUSE、Arch、容器镜像或企业定制系统可能需要单独适配。服务名也不应默认等于包名:

- name: 确保服务启动并设置开机启动
  ansible.builtin.service:
    name: "{{ 'apache2' if ansible_facts['os_family'] == 'Debian' else 'httpd' }}"
    state: started
    enabled: true

如果使用 systemd 模块,应确认目标系统确实由 systemd 管理;容器、chroot 或精简系统可能没有 systemd。


四、幂等性:不是“运行一次”,而是重复收敛到同一状态

幂等性在自动化中表示:当系统已经处于目标状态时,再次执行同一任务不会产生新的有效变化。

形式化地,设:

  • SS 是系统当前状态;
  • TT 是目标状态;
  • FF 是一次自动化执行;
  • F(S)F(S) 是执行后的状态。

理想的幂等任务满足:

F(F(S))=F(S)F(F(S)) = F(S)

如果第一次执行把服务从未安装变为已安装:

S0FS1S_0 \xrightarrow{F} S_1

S1S_1 已满足目标状态,则第二次执行应为:

S1FS1S_1 \xrightarrow{F} S_1

Ansible 输出中的 changed 描述本次任务是否认为系统发生了变化,ok 表示无需变化。它不是严格的数据库事务证明,也不代表整个 Playbook 没有副作用。

1. 一个真正幂等的任务

- name: 确保配置目录存在
  ansible.builtin.file:
    path: /etc/wr-app
    state: directory
    owner: root
    group: root
    mode: '0755'

执行过程是:

  1. 目录不存在:创建目录,返回 changed
  2. 目录存在但权限错误:修正权限,返回 changed
  3. 目录存在且属性正确:不动作,返回 ok

2. 配置文件与 Handler

配置文件变化后通常需要 reload 或 restart。不能每次 Playbook 执行都重启服务:

- name: 写入 nginx 配置
  ansible.builtin.template:
    src: templates/wr-health.conf.j2
    dest: /etc/nginx/conf.d/wr-health.conf
    owner: root
    group: root
    mode: '0644'
  notify: reload nginx

handlers:
  - name: reload nginx
    ansible.builtin.service:
      name: nginx
      state: reloaded

notify 的关键语义是:

  • 只有任务返回 changed 时才通知 Handler;
  • 同一个 Handler 在一个 Play 中通常只执行一次;
  • Handler 默认在该 Play 的任务阶段结束时执行;
  • 如果此前任务失败,默认后续 Handler 不会执行。

因此配置文件“没有变化”时不会无谓 reload,配置变化时才触发服务动作。

3. 反例:每次都产生副作用

- name: 反例:每次都追加一行
  ansible.builtin.shell: echo 'feature=true' >> /etc/wr-app/app.conf

第一次执行后文件包含一行,第二次执行后包含两行。令文件状态为 SnS_n,每次任务都追加内容,则:

Sn+1=Sn+lineS_{n+1} = S_n + \text{line}

显然:

F(F(S))F(S)F(F(S)) \ne F(S)

更合适的是:

- name: 确保配置项存在且唯一
  ansible.builtin.lineinfile:
    path: /etc/wr-app/app.conf
    regexp: '^feature='
    line: 'feature=true'
    create: true
    owner: root
    group: root
    mode: '0644'

如果文件本质上是由模板完整生成的,优先使用 template,因为逐行修改容易与人工修改、排序和重复项产生冲突。

4. command 的幂等性补偿

有些操作只能调用命令,例如导入应用数据。此时应使用 createsremoves 或明确的 changed_when

- name: 仅在标记文件不存在时初始化
  ansible.builtin.command:
    cmd: /usr/local/bin/myapp-init
    creates: /var/lib/myapp/.initialized

creates 的意义是:标记文件存在时跳过命令。它只是一个条件保护,不会自动证明初始化内容正确。

以下写法要谨慎:

- name: 反例:强行把结果标记为未变化
  ansible.builtin.command: /usr/local/bin/myapp-migrate
  changed_when: false

这会隐藏真实变化,导致 Handler 不触发,也让审计输出失真。changed_when 应基于可靠的命令输出或状态检查,而不是为了让结果“好看”。


五、一个可执行的基础 Playbook

以下示例在目标机上安装 nginx,并部署一个健康检查端点。目标系统需要支持 systemd,且 nginx 主配置会加载 /etc/nginx/conf.d/*.conf;这是多数 Debian、Ubuntu、RHEL 系发行版的常见布局,但不是所有发行版的规范保证。

创建 templates/wr-health.conf.j2

server {
    listen {{ http_port }};
    server_name _;

    location = {{ health_path }} {
        default_type text/plain;
        return 200 "ok\n";
    }
}

创建 site.yml

- name: 配置 Web 节点
  hosts: webservers
  become: true
  gather_facts: true

  vars:
    http_port: 8080
    health_path: /health

  pre_tasks:
    - name: 检查操作系统
      ansible.builtin.assert:
        that:
          - ansible_facts['os_family'] in ['Debian', 'RedHat']
        fail_msg: >-
          此示例只验证 Debian 系和 Red Hat 系,当前系统为
          {{ ansible_facts['distribution'] }}

  tasks:
    - name: 安装 nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: 确保 nginx 已启动并开机启动
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

    - name: 部署健康检查配置
      ansible.builtin.template:
        src: templates/wr-health.conf.j2
        dest: /etc/nginx/conf.d/wr-health.conf
        owner: root
        group: root
        mode: '0644'
      notify: reload nginx

  handlers:
    - name: reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

执行前先做语法检查和模拟执行:

ansible-playbook \
  -i inventory/production.yml \
  site.yml \
  --syntax-check

ansible-playbook \
  -i inventory/production.yml \
  site.yml \
  --check

--check 会尽量预测变化,但不是完整模拟器:

  • 某些模块不支持 check mode;
  • commandshell 的真实副作用通常无法可靠预测;
  • 依赖前序任务产生的文件、服务或事实时,模拟结果可能与真实执行不同;
  • 外部 API、数据库迁移和应用健康状态不能仅靠 check mode 验证。

第一次真实执行限定一台主机:

ansible-playbook \
  -i inventory/production.yml \
  site.yml \
  --limit web-01

验证:

curl --fail http://192.0.2.11:8080/health

预期输出:

ok

如果 nginx reload 失败,常见原因包括:

  • 配置路径在该发行版中不同;
  • http_port 已被其他进程占用;
  • 模板生成的 nginx 语法错误;
  • SELinux 或文件上下文阻止访问;
  • 服务实际名称不是 nginx
  • 被管理主机没有 systemd。

诊断命令应在目标机上执行:

sudo nginx -t
sudo systemctl status nginx --no-pager
sudo journalctl -u nginx -n 100 --no-pager
ss -ltnp | grep ':8080'

注意:示例中的 Handler 先执行 state: reloaded,并不自动保证 nginx 配置测试通过。生产 Playbook 应在变更前后加入发行版适配的配置验证,并设计失败恢复路径;不能把“模块返回成功”误认为“应用已经健康”。


六、Secret:加密存储不等于全链路保密

Secret 是不应公开的敏感数据,例如密码、API Token、私钥、数据库凭据和会话签名密钥。Secret 管理需要区分三个阶段:

  1. 静态存储:仓库中的文件是否明文;
  2. 执行传递:变量、命令行、日志和控制节点进程是否泄露;
  3. 目标落盘:被管理主机上的文件权限、备份、日志和进程环境是否安全。

1. 使用 Ansible Vault 加密变量文件

创建加密文件:

ansible-vault create group_vars/webservers/vault.yml

输入内容:

vault_app_database_password: "change-me-in-a-real-secret-manager"
vault_app_api_token: "token-value"

查看或编辑:

ansible-vault view group_vars/webservers/vault.yml
ansible-vault edit group_vars/webservers/vault.yml

目录可以是:

group_vars/
└── webservers/
    ├── vars.yml
    └── vault.yml

vault.yml 只保存加密后的内容,不能因为文件名包含 vault 就自动获得保护。提交前应确认仓库中没有明文副本、编辑器临时文件和 shell 历史记录。

执行时输入 Vault 密码:

ansible-playbook \
  -i inventory/production.yml \
  site.yml \
  --ask-vault-pass

也可以使用密码文件,但该文件必须严格控制权限:

chmod 600 ~/.ansible-vault-password
ansible-playbook \
  -i inventory/production.yml \
  site.yml \
  --vault-password-file ~/.ansible-vault-password

Vault 解决的是“仓库静态文件加密”问题,不解决:

  • Vault 密码文件被窃取;
  • 控制节点内存被读取;
  • Secret 被打印到任务输出;
  • Secret 被写入世界可读文件;
  • 应用把 Secret 记录到日志;
  • 目标机备份系统复制了明文配置。

生产环境常把 Vault 与外部 Secret Manager、短期凭据和轮换机制结合使用。外部系统的认证本身仍需要保护。

2. 避免日志泄露

如果任务参数中包含 Secret,应使用 no_log: true

- name: 写入应用环境文件
  ansible.builtin.template:
    src: templates/app.env.j2
    dest: /etc/wr-app/app.env
    owner: root
    group: root
    mode: '0600'
  no_log: true
  notify: restart myapp

模板 templates/app.env.j2

DATABASE_PASSWORD={{ vault_app_database_password }}
API_TOKEN={{ vault_app_api_token }}

对应 Playbook:

- name: 部署应用 Secret
  hosts: webservers
  become: true
  vars_files:
    - group_vars/webservers/vault.yml

  tasks:
    - name: 创建应用配置目录
      ansible.builtin.file:
        path: /etc/wr-app
        state: directory
        owner: root
        group: root
        mode: '0750'

    - name: 写入应用环境文件
      ansible.builtin.template:
        src: templates/app.env.j2
        dest: /etc/wr-app/app.env
        owner: root
        group: root
        mode: '0600'
      no_log: true

no_log 会隐藏任务结果中的参数和错误细节,因此不能对所有任务无差别启用。它适合保护确实包含 Secret 的任务;同时应通过任务名称、外部监控和应用日志保留足够的诊断能力。

--diff 也可能泄露文件内容。处理 Secret 的执行不应随意使用:

ansible-playbook site.yml --diff

如果必须查看普通配置差异,应确保 Secret 任务设置 no_log: true,并明确审计终端、CI 日志和制品保存范围。

3. Secret 文件权限与进程环境

以下权限具有不同风险:

stat -c '%U %G %a %n' /etc/wr-app/app.env

期望类似:

root root 600 /etc/wr-app/app.env

把 Secret 放入命令行参数通常不安全,因为可能出现在进程列表中:

# 不推荐
myapp --api-token 'secret-value'

环境变量比命令行参数好一些,但可能被调试接口、崩溃转储或子进程继承。更稳妥的方式取决于应用能力,例如受限权限文件、systemd credentials、短期令牌或本地 Secret Agent。


七、滚动变更:把一次大范围动作分解成可验证批次

滚动变更是指不同时修改全部节点,而是按批次处理节点,并在批次之间观察健康状态。它的目标不是“绝对不宕机”,而是限制同时暴露的故障范围。

设有 NN 台节点,每批最多 BB 台,则理想批次数为:

K=NBK = \left\lceil \frac{N}{B} \right\rceil

但批大小不能只由数量决定,还取决于服务冗余。例如有 4 台节点、负载均衡器要求至少 3 台健康时,最多只能同时摘除 1 台;此时 B=1B=1,而不是简单使用 50%。

1. serial 控制批次

- name: 滚动发布 Web 配置
  hosts: webservers
  serial: 1
  become: true

  tasks:
    - name: 部署配置
      ansible.builtin.template:
        src: templates/wr-health.conf.j2
        dest: /etc/nginx/conf.d/wr-health.conf
        owner: root
        group: root
        mode: '0644'
      notify: reload nginx

    - name: 从控制节点检查当前节点健康
      ansible.builtin.uri:
        url: "http://{{ ansible_host | default(inventory_hostname) }}:8080/health"
        status_code: 200
        return_content: true
      delegate_to: localhost
      become: false

  handlers:
    - name: reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

处理顺序是:

  1. 选出第一批一台主机;
  2. 在该主机执行任务;
  3. 执行已触发的 Handler;
  4. 执行健康检查;
  5. 第一批完成后,才处理下一批。

delegate_to: localhost 表示健康检查在控制节点执行,而不是在被管理主机上执行。这样更接近真实客户端或负载均衡器访问路径,但控制节点必须能访问目标机的业务端口。如果业务端口只对内网开放,应改为委托给监控节点、跳板机或使用负载均衡器 API。

可以采用逐步放大的批次:

serial:
  - 1
  - 2
  - "50%"

具体百分比和列表语法依赖所使用的 Ansible 版本,应在目标版本上通过 ansible-playbook --syntax-check 验证。初次上线时使用固定的 serial: 1 更容易推理和审计。

2. 滚动变更不等于自动回滚

如果前两批已经成功,第三批失败,Ansible 默认不会把前两批自动恢复。原因是 Ansible 不是跨主机事务系统:

batch 1: 成功,节点状态已改变
batch 2: 成功,节点状态已改变
batch 3: 失败
batch 4: 不再继续

这时需要明确恢复策略:

  • 使用版本化配置和上一版本制品;
  • 在任务中保存备份;
  • 通过 block/rescue 对当前主机恢复;
  • 通过独立回滚 Playbook 处理已经成功的主机;
  • 由负载均衡器暂时摘除异常节点;
  • 通过应用自身的版本回滚机制恢复。

一个局部恢复示例:

- name: 部署并验证 nginx 配置
  block:
    - name: 复制配置并保留备份
      ansible.builtin.template:
        src: templates/wr-health.conf.j2
        dest: /etc/nginx/conf.d/wr-health.conf
        owner: root
        group: root
        mode: '0644'
        backup: true
      register: nginx_config

    - name: 测试 nginx 配置
      ansible.builtin.command: nginx -t
      changed_when: false

    - name: 重载 nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

  rescue:
    - name: 输出失败信息
      ansible.builtin.debug:
        msg: "nginx 配置验证或重载失败,当前主机需要恢复上一版本"

    - name: 让当前主机任务失败
      ansible.builtin.fail:
        msg: "nginx 变更失败,请根据 backup 文件执行恢复"

这里的 backup: true 只会生成备份文件,不会自动选择正确版本并恢复;rescue 也不会回滚其他主机。生产实现应把备份文件名记录下来,或者更进一步使用版本化发布目录和符号链接:

/etc/wr-app/releases/2025-03-08/
/etc/wr-app/releases/2025-03-09/
/etc/wr-app/current -> /etc/wr-app/releases/2025-03-09/

切换符号链接和恢复上一版本比直接覆盖单个文件更容易审计,但应用是否支持热加载、文件句柄是否需要重启,仍需单独验证。

3. 失败主机、未触发 Handler 和强制 Handler

默认情况下,某个任务失败后,该主机后续任务停止;已经触发但尚未运行的 Handler 也可能因 Play 失败而不执行。对于“配置已写入、服务必须重载”的场景,可以考虑:

- hosts: webservers
  force_handlers: true
  tasks:
    - name: 写入配置
      ansible.builtin.template:
        src: app.conf.j2
        dest: /etc/myapp/app.conf
      notify: restart myapp

    - name: 其他校验
      ansible.builtin.assert:
        that:
          - ansible_facts['distribution'] is defined

  handlers:
    - name: restart myapp
      ansible.builtin.service:
        name: myapp
        state: restarted

force_handlers 不是无条件安全选项。它可能让一个后续校验失败的主机仍然重启服务,因此必须确认 Handler 本身不会扩大故障,并在服务动作前加入必要的配置验证。


八、变更前后的验证与故障诊断

一个可审计的执行流程至少应分为四层。

1. 语法和目标检查

ansible-playbook -i inventory/production.yml site.yml --syntax-check
ansible-inventory -i inventory/production.yml --graph
ansible -i inventory/production.yml webservers -m ansible.builtin.ping

这些命令只验证不同层次:

  • --syntax-check 验证 YAML 和 Playbook 结构;
  • --graph 验证目标分组;
  • ping 验证 SSH、Python 和模块执行;
  • 它们都不能证明业务配置正确。

2. 单主机模拟和真实执行

ansible-playbook \
  -i inventory/production.yml \
  site.yml \
  --limit web-01 \
  --check

ansible-playbook \
  -i inventory/production.yml \
  site.yml \
  --limit web-01

第一次真实执行后,立即检查:

ansible \
  -i inventory/production.yml \
  web-01 \
  -m ansible.builtin.shell \
  -a 'systemctl is-active nginx && curl --fail http://127.0.0.1:8080/health' \
  -b

这里使用 shell 是因为命令包含 shell 的 &&。如果只执行一个不含重定向和管道的程序,应优先使用 command

3. 识别失败类型

常见错误可按路径分类:

SSH 层失败

UNREACHABLE! => Permission denied

检查:

ssh -vvv web-01
ssh-add -l

可能是用户、私钥、跳板机、主机密钥或网络 ACL 问题。

提权层失败

Missing sudo password

检查:

ansible -i inventory/production.yml web-01 -m command -a id -b -K

-K 请求 become 密码。自动化环境如果依赖交互输入,通常需要重新设计凭据分发方式。

模块层失败

MODULE FAILURE

检查目标机 Python 路径、解释器版本、临时目录权限和目标命令:

ansible \
  -i inventory/production.yml \
  web-01 \
  -m ansible.builtin.setup \
  -a 'filter=ansible_python*'

应用层失败

Ansible 可能返回 changed,但应用随后启动失败。例如配置文件写入成功,systemd 服务却因语法错误退出。因此必须把 nginx -t、端口监听、HTTP 状态码、依赖连接和关键业务指标纳入验证。


九、常见误解与真实边界

“Playbook 成功”不等于“业务成功”

Ansible 的成功主要表示模块完成了它能观察到的动作。service: state=started 只能说明服务启动动作没有返回错误,不能保证:

  • 端口可从客户端访问;
  • DNS、TLS 和负载均衡正常;
  • 应用能连接数据库;
  • 所有请求都返回正确结果;
  • 性能和容量满足要求。

业务健康检查必须使用与真实流量相近的路径,并明确超时、重试和失败阈值。

“幂等”不等于“无风险”

即使任务是幂等的,第一次执行仍可能造成高风险变化:

  • 修改防火墙规则导致 SSH 断开;
  • 重启内核或服务导致连接中断;
  • 删除旧配置造成不可逆数据损失;
  • 数据库迁移成功但应用旧版本无法运行。

幂等只约束重复执行后的状态收敛,不提供事务、备份或自动回滚。

“Vault 加密”不等于“Secret 不会泄露”

Secret 可能从以下位置泄露:

命令行参数
Shell history
CI 日志
--diff 输出
任务失败信息
目标机配置文件
备份文件
应用日志
进程环境
崩溃转储

因此 Secret 的治理必须覆盖生成、传递、落盘、使用、轮换和撤销,而不是只把一个 YAML 文件加密。

“serial: 1”不等于高可用

即使一次只变更一台主机,也可能因为:

  • 负载均衡器没有摘除节点;
  • 健康检查只检查本机 127.0.0.1
  • 服务实际依赖共享数据库;
  • 发布内容破坏了所有节点共用的外部依赖;
  • 连接复用或缓存使旧状态继续服务;

导致整体业务不可用。滚动变更必须和流量治理、健康检查、容量余量及恢复方案一起设计。


十、推荐的生产执行顺序

针对一个已有编程经验的工程团队,可以把一次 Linux 自动化变更约束为如下状态机:

stateDiagram-v2
    [*] --> Draft
    Draft --> SyntaxChecked: 编写 Inventory/Playbook
    SyntaxChecked --> ConnectivityChecked: syntax-check + inventory graph
    ConnectivityChecked --> Simulated: ping + check mode
    Simulated --> Canary: --limit 单主机
    Canary --> Rolling: 健康检查通过
    Canary --> Recovery: 验证失败
    Rolling --> Rolling: 批次健康检查通过
    Rolling --> Completed: 所有批次完成
    Rolling --> Recovery: 某批次失败
    Recovery --> Completed: 恢复或回滚验证通过
    Recovery --> [*]: 无法恢复,进入应急流程
    Completed --> [*]

每个状态都有不同的证明目标:

  • Draft:目标主机、变量、权限和回滚路径已经写入代码;
  • SyntaxChecked:YAML 和 Playbook 结构可解析;
  • ConnectivityChecked:SSH、Python、sudo 和主机密钥可用;
  • Simulated:check mode 没有发现明显异常,但不替代真实验证;
  • Canary:一台真实主机完成变更并通过应用健康检查;
  • Rolling:按批次扩大范围,任何失败都停止继续扩大;
  • Recovery:恢复动作本身也必须验证,而不是只执行命令;
  • Completed:所有节点达到目标状态,并保留执行记录和版本信息。

Ansible 的价值在于把状态、权限、目标范围和变更顺序显式化。真正可靠的 Linux 自动化,则还需要 SSH 密钥治理、最小权限、Secret 生命周期、应用级验证、容量余量和可执行的恢复方案共同成立。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。