从手工登录到一条命令:Ansible 批量运维的落地笔记

前因:五台机器,半小时

Ansible 批量运维实战:让十台机器一条命令搞定

我们早期有个服务集群,五台应用服务器,每次发版都要逐个登录:

ssh app1
docker restart myapp
tail -f /var/log/app.log
# 等三十秒,确认起来
exit

ssh app2
...

五台下来,加上网络延迟、确认时间,一趟完整的发版要四十分钟。更麻烦的是验证环节:每台机器都要手动查进程、看端口、检查依赖服务状态。有一回 app3 的容器没起来,健康检查接口返回 503,我没有逐台看日志,直接在群里说「发布完了」,结果用户端故障持续了十分钟才发现。

这类问题有个共性:操作在多个机器上并行发生,但人在串行确认。批量任务天然适合自动化工具,而 Ansible 是我最常用的选择之一。

为什么是 Ansible,而不是 PXE 或 Ansible Tower

自动化部署的工具链很长:PXE 裸机安装适合大规模集群初始化,SaltStack 需要部署 agent,Puppet 依赖证书体系,Kubernetes Operator 适合容器化场景。但如果是五台到几百台之间的物理机或虚拟机,需要管理操作系统配置、分发文件、执行命令、管理包安装,Ansible 是最轻量的方案——因为它不需要在目标机器上装任何东西。

核心机制是 SSH:主控机(control node)通过 SSH 连接目标机,把 Python 脚本传输过去执行,返回结果后清理临时文件。整个过程对目标机无侵入,不需要开机自启服务,不需要防火墙例外端口(除了默认的 22)。

代价也有:SSH 连接开销在机器数量多时会放大,大批量(上千台)并行执行时主控机会成为瓶颈。这时更适合 SaltStack 或专门的配置管理平台。

核心概念:Inventory、Playbook、Module

Inventory:机器清单

/etc/ansible/hosts 是默认位置,也可以指定其他路径:

[app_servers]
app1 ansible_host=192.168.1.11
app2 ansible_host=192.168.1.12
app3 ansible_host=192.168.1.13

[db_servers]
db1 ansible_host=192.168.1.21

[prod:children]
app_servers
db_servers

变量可以在 inventory 里定义,也可以在运行时传入:

ansible all -m ping -e "env=prod version=2.3.1"

常见坑:ansible_host 和 ansible_ssh_host 是等价的,但混用时容易写错。ansible_user 和 ansible_ssh_user 同样,建议统一用 ansible_user。

Playbook:声明式执行流程

YAML 格式,描述「应该是什么状态」,而不是「执行什么命令」:

- hosts: app_servers
  become: yes
  tasks:
    - name: Ensure Java 17 is installed
      apt:
        name: openjdk-17-jdk
        state: present
    
    - name: Deploy application jar
      copy:
        src: deploy/{{ version }}.jar
        dest: /opt/app/app.jar
        owner: appuser
        group: appuser
        mode: '0644'
      
    - name: Restart service
      systemd:
        name: myapp
        state: restarted
        enabled: yes

关键字段说明:

  • become: yes:使用 sudo 提权,不需要切换到 root 用户登录
  • state: present/absent/restarted/reloaded:声明式状态,模块会自行判断当前状态并执行必要的变更
  • owner/group/mode:文件权限管理,避免手动 chmod 忘记改权限导致启动失败

Module:可复用的原子操作

Ansible 内置了几百个模块,常用分类:

类别典型模块用途
系统apt/yum/dnf包管理
文件copy/template/file文件分发、模板渲染
服务systemd/service服务管理
命令command/shell执行任意命令
用户user/group账户管理
数据库mysql_db/postgresql_db数据库操作

command 模块不经过 shell,安全但功能受限;shell 模块可以执行管道和重定向,但要警惕注入风险。

批量执行:一次命令搞定多台

基础命令格式:

# 测试连通性
ansible all -m ping

# 在 app_servers 组执行
ansible app_servers -m shell -a "systemctl status myapp"

# 带提权执行
ansible app_servers -m shell -a "apt update" -b

# 异步执行,不等待结果
ansible app_servers -m shell -a "long-running-task.sh" --async 3600 --poll 0

参数说明:

  • -m:指定模块名
  • -a:模块参数
  • -b:等同于 --become,使用 sudo
  • --forks:并行度,默认 5,机器多时可以调高
  • --limit:限制执行范围,如 --limit app1,app2

并行度的取舍:--forks 越大,执行越快,但主控机会同时打开更多 SSH 连接。如果目标机有连接数限制(如 MaxStartups),需要相应调整。生产环境通常设置在 20-50 之间。

真实案例:批量部署 Spring Boot 服务

下面是我实际使用过的 playbook,做了脱敏处理:

---
- name: Deploy backend service
  hosts: app_servers
  become: yes
  vars:
    app_name: user-service
    app_port: 8080
    deploy_dir: /opt/apps/{{ app_name }}
    jar_version: "{{ lookup('env', 'DEPLOY_VERSION') }}"
  
  pre_tasks:
    - name: Create deploy directory
      file:
        path: "{{ deploy_dir }}"
        state: directory
        owner: appuser
        group: appuser
    
    - name: Backup current jar
      command: "cp {{ deploy_dir }}/{{ app_name }}.jar {{ deploy_dir }}/{{ app_name }}.jar.bak.{{ ansible_date_time.iso8601 }}"
      args:
        creates: "{{ deploy_dir }}/{{ app_name }}.jar.bak.{{ ansible_date_time.iso8601 }}"
  
  tasks:
    - name: Upload new jar
      copy:
        src: "../build/libs/{{ app_name }}-{{ jar_version }}.jar"
        dest: "{{ deploy_dir }}/{{ app_name }}.jar"
        owner: appuser
        group: appuser
        mode: '0644'
      
    - name: Restart service
      systemd:
        name: "{{ app_name }}"
        state: restarted
        daemon_reload: yes
      
    - name: Wait for service to be ready
      uri:
        url: "http://localhost:{{ app_port }}/actuator/health"
        status_code: 200
      retries: 12
      delay: 5
      register: health_check
      until: health_check.status == 200
  
  post_tasks:
    - name: Clean old backups
      file:
        path: "{{ deploy_dir }}/{{ app_name }}.jar.bak.*"
        state: absent
      args:
        age: 7d
        recurse: yes

这个 playbook 有几个设计点:

  1. 备份带时间戳:creates 参数确保同时间戳不会重复备份,避免磁盘被旧备份撑满
  2. 健康检查重试:retries: 12 + delay: 5 意味着最多等一分钟,避免服务还没启动完就报告失败
  3. 清理旧备份:age: 7d 只保留最近七天的备份,防止历史堆积

一个常见的误解:template 模块只用于渲染配置文件,但实际上 copy 模块也可以渲染 Jinja2 模板,区别在于 template 始终走模板引擎,copy 只在文件是模板时才渲染。对于简单的环境变量替换,用 template 更直观。

问题排查:Ansible 的报错往往不够直观

SSH 连接失败

UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh.",
    "unreachable": true
}

排查顺序:

  1. 手动 ssh target_host 确认可达
  2. 检查 /etc/ansible/hosts 里的 ansible_host 是否正确
  3. 确认 SSH 端口不是默认的 22,需要在 inventory 里指定 ansible_port
  4. 检查私钥路径,ansible_ssh_private_key_file 或 ~/.ssh/id_rsa 是否存在

权限不足

chmod: changing permissions of '/opt/app': Operation not permitted

确认 playbook 里有 become: yes,且执行用户有 sudo 权限。有时需要在 inventory 里指定 ansible_become_pass,或在主控机配置免密 sudo。

模块找不到

ERROR! couldn't resolve module/module_uri 'apt'

目标机没有安装 Python,或者 Python 版本过旧(< 2.6 或 >= 3.9)。Ansible 要求目标机有 Python,对于不包含 Python 的设备(如路由器),需要使用 raw 模块通过 SSH 直接执行命令。

并发导致的文件竞争

ERROR! Attempting to reconstruct a split diff failed because the original source file wasn't found

多个 playbook 同时修改同一文件时会冲突。解决方案:

  1. 降低 --forks 数量
  2. 使用 serial 关键字让 playbook 分批执行
  3. 改用锁定机制(delegate_to 到一台机器上统一管理)

适用边界:什么时候不该用 Ansible

Ansible 擅长的是配置管理和应用部署,不适用于:

  1. 实时配置推送:Ansible 是拉模式,需要主动触发。如果需要配置变更实时同步,不适合
  2. 大规模动态环境:容器、Serverless 等场景下机器生命周期很短,Ansible 的静态 inventory 跟不上变化
  3. 复杂的条件逻辑:虽然支持 when 条件和循环,但复杂业务逻辑还是应该在应用代码里处理
  4. 长连接场景:Ansible 每个任务都是短连接,不适合需要保持长连接的监控或日志收集

对于这些场景,Kubernetes Operator、Consul、或专用的配置中心更合适。

总结

Ansible 的核心价值在于:把重复的人工操作变成可复现、可审计、可回滚的流程。从五台机器到五百台,playbook 几乎不需要改动,只需要调整 inventory 和并发度。

真正的难点不在于工具本身,而在于:

  1. 幂等性设计:每次执行结果应该一致,不会因为重复执行产生副作用
  2. 错误处理:一条命令失败时,后续操作是继续还是中止,需要在 playbook 里明确
  3. 版本管理:playbook 和 inventory 应该和代码一起进入版本控制,避免「这台机器是谁改的」类问题

工具不会解决所有问题,但能消除机械劳动中的犯错空间。对于批量运维而言,这个价值已经足够。

posted @ 2026-10-29 06:00  框架师  阅读(9)  评论(0)    收藏  举报