Ansible自动化运维是什么?多台Linux服务器批量管理教程

当服务器从三台变成三十台,运维的工作方式必须换。以前登录每台机器改配置、重启服务,一个人还能忙得过来;三十台机器时,同样的操作重复三十遍,不仅浪费时间,还容易漏掉一台、或者某台命令敲错。真正的痛点在于:你不知道三十台机器的配置是不是完全一致,出了问题也很难复现。Ansible 就是解决这类问题的工具,它让你用一份描述文件,把配置一次性推到所有机器,而且推完还能保证结果一致。

这篇文章讲清 Ansible 的核心概念:为什么它不需要在被管理机上装 Agent、inventory 清单怎么组织、ad-hoc 命令怎么用、playbook 怎么写、常用模块有哪些、roles 怎么组织代码,最后给一个多机批量部署的完整示例。贯穿全文的一个关键词是"幂等性"——同一个 playbook 跑一遍和跑十遍结果一样,这是自动化工具和普通脚本最本质的区别。

无 Agent 架构:为什么它这么轻

Ansible 不需要在被管理机安装任何常驻程序,它靠 SSH 连上去,把一段临时的 Python 脚本推过去执行,执行完就删掉。这就是它最吸引人的地方:只要目标机有 SSH 和 Python,什么都不用装就能管。

对比一下主流的配置管理工具,各自的定位差异很明显:

工具 架构 学习成本 幂等 适合场景
Ansible 无 Agent,SSH 推送 低(YAML) 是 中小规模,快速上手
Puppet C/S,需装 Agent 中高(自有 DSL) 是 大规模,强约束
Chef C/S,需装 Agent 高(Ruby) 是 开发主导的复杂环境
SaltStack C/S 或 SSH 中 是 大规模,秒级下发
Shell 脚本 无 最低 否 一次性任务,小规模

Ansible 的优势是"无 Agent + 幂等 + YAML 易读"。它的劣势在大规模场景会显现:SSH 逐台连接,几千台并发时对控制机压力大(虽然可以调 forks 参数),实时性不如 SaltStack 的常驻通道。所以几百台以内,Ansible 是性价比最高的选择。

安装很简单,控制机装上即可:

# RHEL、Rocky、CentOS
sudo dnf install -y epel-release
sudo dnf install -y ansible

# Debian、Ubuntu
sudo apt update
sudo apt install -y ansible

ansible --version

被管理机只需要有 SSH 服务和 Python(大多数发行版默认就有,极简系统可能需要 dnf install python3 或 apt install python3)。

inventory:管理哪些机器

inventory 是 Ansible 的机器清单,定义有哪些主机、怎么分组。最简单的写法是一个 INI 文件 /etc/ansible/hosts:

[web]
web-01 ansible_host=192.0.2.20
web-02 ansible_host=192.0.2.21

[db]
db-01 ansible_host=192.0.2.30

[all:vars]
ansible_user=ops
ansible_ssh_private_key_file=/home/ops/.ssh/id_rsa

[web:vars]
ansible_port=22

方括号里是组名,可以嵌套([web:children]),也可以用 [all:vars] 给所有主机设公共变量。生产环境建议用独立的 inventory 目录:

inventory/
├── production/
│   ├── hosts           # 清单
│   └── group_vars/
│       ├── all.yml     # 所有主机变量
│       └── web.yml     # web 组变量
└── staging/
    └── hosts

主机变量还可以单独放 host_vars/web-01.yml。这个结构清晰,配合 Git 管理非常方便。测试连通性用 ping 模块(这里的 ping 是 SSH 连通性检查,不是 ICMP):

ansible all -i inventory/production/hosts -m ping
ansible web -i inventory/production/hosts -m ping

返回 "ping": "pong" 就说明通。

风险提示: Ansible 的 SSH 私钥等于所有被管理机的钥匙,权限必须严格限制(chmod 600),不要放在共享目录或提交进 Git。控制机本身是高价值目标,要做好访问控制和审计。后面批量命令的文章会详细讲 SSH 密钥的安全管理。

ad-hoc 命令:临时任务利器

不需要写文件,一条命令就能对一批机器执行操作,这叫 ad-hoc。基本格式是 ansible <主机模式> -m <模块> -a <参数>。

# 查看所有机器内存
ansible all -i hosts -m shell -a "free -h"

# 查看磁盘
ansible web -i hosts -m shell -a "df -h /"

# 重启所有 web 服务(用 systemd 模块,幂等)
ansible web -i hosts -m systemd -a "name=nginx state=restarted"

# 复制文件到所有机器
ansible all -i hosts -m copy -a "src=/tmp/app.conf dest=/etc/app/app.conf mode=0644"

# 安装软件包
ansible all -i hosts -m package -a "name=htop state=present"

# 查服务状态
ansible db -i hosts -m shell -a "systemctl is-active mysql"

shell 和 command 模块的区别要分清:command 不经过 shell,不支持管道、重定向、变量展开;shell 会调 /bin/sh,支持这些但安全性稍低(有注入风险)。能用专用模块就别用 shell,比如装包用 package、改文件用 copy、管服务用 systemd,专用模块才有幂等性,shell 没有。

常用参数:-f 20 设置并发进程数(默认 5),机器多时可以提高;-b 提权到 root(become);-K 交互输入提权密码;-u 指定远程用户;-o 输出压缩成一行。比如:

ansible all -i hosts -m shell -a "uptime" -b -f 20 -o

ad-hoc 适合临时排查、简单变更。一旦任务超过两三个步骤,就该写成 playbook,因为 ad-hoc 不可复用、不可版本管理,用完即弃。

playbook:把操作写成文件

playbook 是 YAML 格式的任务文件,是 Ansible 的核心。一个最基础的结构:

---
- name: 部署 Web 服务          # play 的名称
  hosts: web                  # 目标主机组
  become: yes                 # 提权
  vars:                       # 变量
    app_port: 8080
    app_dir: /opt/myapp
  tasks:                      # 任务列表,从上往下执行
    - name: 安装依赖包
      package:
        name:
          - nginx
          - curl
        state: present

    - name: 创建应用目录
      file:
        path: "{{ app_dir }}"
        state: directory
        mode: '0755'

    - name: 下发配置文件
      template:
        src: app.conf.j2
        dest: "{{ app_dir }}/app.conf"
        mode: '0644'
      notify: 重启应用           # 文件变化才通知 handler

    - name: 复制程序文件
      copy:
        src: files/app
        dest: "{{ app_dir }}/app"
        mode: '0755'

    - name: 确保服务运行
      systemd:
        name: myapp
        state: started
        enabled: yes

  handlers:                   # 被 notify 触发的任务,只在变化时执行
    - name: 重启应用
      systemd:
        name: myapp
        state: restarted

几个关键概念:

任务(tasks)按顺序执行,每个任务调用一个模块,name 是给人看的描述,出问题时日志里会显示。

handler 是一种特殊任务,只被 notify 触发,而且一个 play 里不管通知几次,handler 只在最后执行一次。这是配置管理的精髓:只有配置文件真的变了才重启服务,没变就不重启,避免无谓的重启。

变量用 {{ }} 引用,可以定义在 play 的 vars、inventory 的 group_vars、命令行 -e 或寄存器。变量优先级有讲究,实战中记住一条:命令行 -e 最高,适合覆盖。

模板用 Jinja2 语法,app.conf.j2 可以这样写:

server {
    listen {{ app_port }};
    server_name {{ ansible_fqdn }};
    root {{ app_dir }}/public;
}

ansible_fqdn 是 Ansible 自动收集的事实变量(fact),不用自己定义就能用。执行前可以用 ansible web -m setup 查看某台机器收集到的所有 fact。

执行 playbook:

ansible-playbook -i inventory/production/hosts deploy.yml
# 检查语法
ansible-playbook --syntax-check deploy.yml
# 试运行,不改动任何东西
ansible-playbook -i hosts deploy.yml --check
# 指定变量
ansible-playbook -i hosts deploy.yml -e "app_port=9090"

--check 是写 playbook 时最该常用的参数,它模拟执行,告诉你"将会改什么",不真正动手。配合 --diff 还能看文件具体差异:

ansible-playbook -i hosts deploy.yml --check --diff

常用模块速查

模块 作用 常用参数 幂等
copy 复制文件 src、dest、mode、owner 是
template 渲染模板 src、dest、mode 是
file 管理文件/目录/链接 path、state、mode 是
package 通用包管理 name、state 是
yum/dnf/apt 指定包管理器 name、state 是
systemd 管理服务 name、state、enabled 是
service 通用服务管理 name、state、enabled 是
user 管理用户 name、state、groups 是
lineinfile 修改某一行 path、regexp、line 是
command 执行命令 cmd、creates 否
shell 执行 shell 命令 cmd 否
uri HTTP 请求 url、method 否
git 拉代码 repo、dest、version 是

用 package 而不是 yum/apt 的好处是跨发行版通用,Ansible 会自动选择。但有时需要发行版特有参数(比如 yum 的 enablerepo),就得用具体模块。

command 和 shell 没有幂等性,每次都会执行。如果确实要用,加 creates 或 removes 参数来获得一定幂等性:

- name: 初始化数据库(仅当标记文件不存在时)
  command: /opt/myapp/init_db.sh
  args:
    creates: /var/lib/myapp/.initialized

这样只有 .initialized 文件不存在时才执行,避免了重复初始化。

roles:把代码组织起来

playbook 一多,公共逻辑(装包、配时区、开防火墙)会到处重复。roles 就是把这些可复用的部分封装成标准结构:

roles/
└── nginx/
    ├── tasks/main.yml      # 任务入口
    ├── handlers/main.yml   # 处理器
    ├── templates/          # 模板文件
    ├── files/              # 静态文件
    ├── vars/main.yml       # 变量(优先级高)
    ├── defaults/main.yml   # 默认变量(优先级低,可被覆盖)
    └── meta/main.yml       # 依赖关系

在 playbook 里引用:

---
- name: 配置 Web 服务器
  hosts: web
  become: yes
  roles:
    - common        # 公共基础配置
    - nginx

- name: 配置数据库
  hosts: db
  become: yes
  roles:
    - common
    - role: mysql
      vars:
        mysql_port: 3306

defaults/main.yml 和 vars/main.yml 的区别常被问:defaults 优先级最低,适合放"希望被用户覆盖"的默认值;vars 优先级高,适合放"不希望被随意改"的固定值。写角色时把可配置项放 defaults。

role 也可以通过 Galaxy 下载社区现成的:

ansible-galaxy install geerlingguy.nginx
ansible-galaxy list

用别人的 role 能省时间,但要看清它做了什么、是否还在维护、是否引入意外变更,别盲目用。

批量部署完整示例

假设要把一个应用部署到十台 web 机,包含:装依赖、建用户、下发配置、部署程序、启动服务。用 playbook 写:

---
- name: 批量部署 Web 应用
  hosts: web
  become: yes
  serial: 2                    # 每次只操作 2 台,滚动更新
  vars:
    app_user: myapp
    app_dir: /opt/myapp
    app_port: 8080

  tasks:
    - name: 安装系统依赖
      package:
        name:
          - nginx
          - python3
        state: present

    - name: 创建应用用户
      user:
        name: "{{ app_user }}"
        shell: /sbin/nologin
        system: yes

    - name: 创建目录
      file:
        path: "{{ item }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_user }}"
        mode: '0755'
      loop:
        - "{{ app_dir }}"
        - "{{ app_dir }}/logs"

    - name: 下发配置
      template:
        src: templates/app.conf.j2
        dest: "{{ app_dir }}/app.conf"
        owner: "{{ app_user }}"
        mode: '0644'
      notify: restart app

    - name: 部署二进制
      copy:
        src: files/app
        dest: "{{ app_dir }}/app"
        owner: "{{ app_user }}"
        mode: '0755'
      notify: restart app

    - name: 启动服务
      systemd:
        name: myapp
        state: started
        enabled: yes

  handlers:
    - name: restart app
      systemd:
        name: myapp
        state: restarted

这里 serial: 2 让 Ansible 每次只处理两台机器,全部成功才继续下一批。这是发布时的正确姿势:万一新版有问题,只影响少量机器,能及时停下。配合 max_fail_percentage: 20 可以在失败超过两成时自动中止。

loop 是循环,{{ item }} 是当前项。老版本用 with_items,现在推荐 loop。

执行并观察结果:

ansible-playbook -i inventory/production/hosts deploy.yml --check --diff   # 先试运行
ansible-playbook -i inventory/production/hosts deploy.yml                  # 正式执行

输出里每个任务是 ok(无需变动)、changed(发生了变动)、skipped(跳过)还是 failed(失败)。幂等性的验证方式就是:第一次跑有一堆 changed,再跑一次应该几乎全是 ok。如果第二次还有大量 changed,说明某个任务不幂等,需要修。

常见问题

Ansible 连接报 "Permission denied" 或超时怎么办?

先手动用同样的用户和密钥 ssh 到目标机,确认能通。常见原因:密钥权限太开放(600 才对)、目标机 authorized_keys 没放对、SSH 端口不是默认 22、防火墙挡了、become 需要密码但没加 -K。可以加 -vvv 看详细调试信息。

为什么第二次跑 playbook 还是有一堆 changed?

一般是用了 command/shell 这类不幂等的模块,或者模板/文件的 mode、owner 没对齐导致每次都被重写。给 command 加 creates 参数,检查模板内容是否每次都变(比如里面嵌了时间戳),都能解决。幂等是自动化质量的核心指标,值得花时间修。

怎么只对部分主机执行?

用 --limit 参数,或主机模式匹配:

ansible-playbook -i hosts deploy.yml --limit "web-01"
ansible-playbook -i hosts deploy.yml --limit "web-01:web-02"
ansible all -i hosts -m shell -a "hostname" --limit "@retry"

遇到失败时,Ansible 会生成一个 .retry 文件,用 --limit @文件 可以只对失败的主机重试,非常实用。

生产环境怎么控制密钥和敏感变量?

敏感变量(数据库密码等)用 ansible-vault 加密:

ansible-vault encrypt group_vars/all/secrets.yml
ansible-playbook -i hosts deploy.yml --ask-vault-pass

密码文件本身不要提交进 Git。SSH 私钥也要严格权限管理,控制机的访问权限本身就是安全边界。

总结

  • Ansible 无 Agent,靠 SSH 加临时 Python 脚本工作,只要目标机有 SSH 和 Python 就能管,学习成本低。
  • inventory 定义机器和分组,配合 group_vars/host_vars 组织变量,建议纳入 Git 管理。
  • ad-hoc 用于临时任务,三步骤以上的操作就该写成幂等的 playbook,能用专用模块就别用 shell。
  • handler 是配置管理的精髓,只在文件真正变化时重启服务,避免无谓重启。
  • roles 把可复用逻辑封装成标准结构,defaults 放可覆盖的默认值,vars 放固定值。
  • 幂等性是衡量自动化质量的核心——第一次 changed 一堆,第二次应几乎全 ok。
  • 发布用 serial 做滚动更新,失败及时中止,--check --diff 先试运行再动手。