用 Ansible 批量管理多台 VPS 实战:inventory 分组、幂等剧本、变量模板与 roles 复用全流程

为什么一个站长迟早要用上 Ansible

刚开始做站的时候,只有一台 VPS,装 Nginx、配 PHP、开防火墙,全凭 SSH 手敲,改完记住就行。等到手里有三五台机器——一台跑主站,一台做数据库和备份,一台放对象存储或者测试环境——事情就变了。每次系统升级、每次改一个 Nginx 参数、每次新建一个站点目录,你都要一台一台登录过去重复同样的动作。重复到第三台的时候人就开始偷懒,偷懒就会出现"这台装了新版、那台还是旧版"的漂移;再过半年,你甚至记不清哪台机器上改过什么,只能靠 `history` 去猜。

配置管理工具就是来解决这个问题的。市面上有 Puppet、Chef、SaltStack,但对个人站长来说,Ansible 几乎是唯一理性的选择:它不需要在每台被管机器上装 agent,只要目标机器能 SSH 登录、有 Python,Ansible 就能干活。它靠的是"推"(push)模式——你在本地(或者任意一台装了 Ansible 的机器上)写好剧本,一条命令推到所有服务器上执行。对只有几台机器、不想维护额外服务的人来说,这是最低的心智负担。

这篇文章面向真在多机环境里折腾过的站长,从零讲一遍:怎么装、怎么写 inventory、playbook 的幂等到底是什么意思、变量和模板怎么用、roles 怎么复用,以及我在实际踩过的几个坑。看完你应该能给自己的几台机器写出一份可重复执行的运维剧本,再也不用逐台敲命令。

Ansible 的安装与"无 agent"到底意味着什么

Ansible 只需要装在一台机器上——通常是你本地电脑,或者一台专门的跳板机。它通过 SSH 连到目标机,把一小段 Python 脚本传过去执行,执行完就删掉。所以目标机上唯一的要求是:能 SSH 登录,并且有 Python(现代 Linux 发行版自带)。这就是它跟 Puppet/Chef 最大的区别,后者需要每台机器常驻一个 agent 进程、定期向 master 拉配置。

# Debian/Ubuntu
apt update && apt install -y ansible

# 用 pip 装最新版(发行版自带版本常常偏旧)
python3 -m pip install --user ansible

# 验证
ansible --version

"无 agent"带来的直接好处是:一台全新的、刚买来的裸 VPS,只要你能用密码 SSH 进去,Ansible 立刻就能接管它,不需要先手动部署任何东西。缺点也很明确:因为每次都是现连现跑,Ansible 的性能不如 agent 模式,几百台机器的规模下会比较慢——但这对个人站长完全不是问题,我们撑死十几台。

第一步:写好 inventory,把机器编成组

Ansible 的所有操作都基于一个叫 inventory(清单)的东西,它就是"我要管哪些机器"的名单。最简形式是一个 INI 文件:

# /etc/ansible/hosts 或项目目录下的 inventory.ini
[web]
web1 ansible_host=203.0.113.10
web2 ansible_host=203.0.113.11

[db]
db1 ansible_host=203.0.113.20

[backup]
bk1 ansible_host=203.0.113.30

[all:vars]
ansible_user=root
ansible_port=22
ansible_python_interpreter=/usr/bin/python3

这里定义了三组:web、db、backup。`[all:vars]` 是给所有机器设置的公共变量——登录用户、SSH 端口、Python 解释器路径。分组的意义在于,你可以只对 web 组执行"Nginx 重载",只对 db 组执行"MySQL 备份",而不用一台台指定。

一个比密码更安全的做法是配置 SSH 密钥免密登录(Ansible 才能全自动跑,不用每次输密码):

# 本地生成密钥(如果还没有)
ssh-keygen -t ed25519 -C "ansible"

# 把公钥推到每台机器
ssh-copy-id -p 22 root@203.0.113.10

# 测试连通性
ansible all -i inventory.ini -m ping

`ansible all -m ping` 这条命令会连到所有机器,返回绿色的 pong 就说明 SSH 通、Python 在、Ansible 能控。这是每次改完 inventory 后第一件该做的事,比直接跑剧本发现问题要快得多。

幂等:配置管理最核心的一个词

很多人第一次用 Ansible 会下意识写这种剧本:每一行都是"执行某条 shell 命令"。那样做其实和写一个 SSH 批量脚本没区别,也丢掉了 Ansible 最大的价值——幂等(idempotence)。

幂等的意思是:同一个剧本,跑一次和跑一百次,结果是一样的。第一次跑,它把没装的软件装上、把不对的配置改对;第二次、第三次跑,它检查发现一切已经符合期望,就什么都不做,报告"ok"而不是"changed"。这样你可以在任何时候安心地重跑剧本,不用担心中途重启服务、不用担心中断后重新执行会重复操作。

# 反例:非幂等的写法,每次都真的去执行命令
- name: 安装 nginx
  shell: apt install -y nginx

# 正例:用模块,Ansible 自己判断是否需要安装
- name: 确保 nginx 已安装
  apt:
    name: nginx
    state: present

`apt` 模块会先查包在不在,在就不动。`state: present` 表示"应该存在"。如果写成 `state: latest`,则会升级到最新——对生产环境要谨慎,因为它每次跑都可能触发升级导致服务重启。个人站一般用 `present` 更稳。

写第一个真正的 Playbook

Playbook 是一个 YAML 文件,描述"对哪些机器、按什么顺序、做哪些事"。下面这份剧本做的事是:给 web 组装好 Nginx、把它设成开机自启、确保配置目录存在:

---
- name: 配置 web 服务器
  hosts: web
  become: yes            # 用 sudo 提权
  tasks:
    - name: 安装 nginx
      apt:
        name: nginx
        state: present
        update_cache: yes

    - name: 确保 nginx 开机自启并正在运行
      service:
        name: nginx
        state: started
        enabled: yes

    - name: 确保站点根目录存在
      file:
        path: /var/www/mysite
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'

执行方式:

ansible-playbook -i inventory.ini web.yml

几个关键点:`become: yes` 让你以 root 权限执行(等价于 sudo);`state: started` 和 `enabled: yes` 分别管"当前正在运行"和"开机时自启",两者要分开写;`mode: '0755'` 一定要加引号,否则 YAML 会把它当成八进制数字算错——这是新手最常见的坑之一。

用变量和模板消灭重复

真正的多机环境里,每台机器的配置往往只差几个值:域名不同、端口不同、数据库地址不同。把这些差异抽成变量,剧本就能复用。变量可以写在 inventory 里,也可以单独放在 `group_vars/` 和 `host_vars/` 目录下——Ansible 会自动读取 `group_vars/web.yml`(给 web 组)和 `host_vars/web1.yml`(给单台机器),这是管理多机差异最清晰的方式。

# group_vars/web.yml
server_name: www.mysite.com
doc_root: /var/www/mysite
php_socket: /run/php/php8.2-fpm.sock

然后是模板。Ansible 用 Jinja2 模板,文件以 `.j2` 结尾,里面的 `{{ }}` 会在推送时被替换成变量值。Nginx 站点配置是个绝佳例子:

# templates/site.conf.j2
server {
    listen 80;
    server_name {{ server_name }};
    root {{ doc_root }};

    index index.php index.html;

    location ~ \.php$ {
        fastcgi_pass unix:{{ php_socket }};
        include fastcgi_params;
    }
}

推送模板用 `template` 模块,它和 `copy` 的区别是:`template` 会先渲染变量,`copy` 只是原样复制文件。

    - name: 部署 Nginx 站点配置
      template:
        src: templates/site.conf.j2
        dest: /etc/nginx/sites-available/mysite.conf
        mode: '0644'
      notify: reload nginx   # 只有文件变化时才触发 handler

这里的 `notify` 配合 handler 是 Ansible 很优雅的一个设计:handler 只在被 notify 的任务真的发生改变(changed)时才运行。也就是说,如果配置文件内容没变,Nginx 不会被无谓地重载——这又是幂等思想的一次体现。

# 剧本末尾定义 handler
  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

用 Roles 把剧本组织成可复用的模块

当剧本越写越长,把所有任务堆在一个 YAML 里会难以维护。Roles 让你按"职责"把配置拆开,每个 role 是一个目录,有固定的结构:

roles/
  nginx/
    tasks/main.yml      # 任务
    handlers/main.yml   # 处理器
    templates/          # 模板
    files/              # 静态文件
    vars/main.yml       # 变量
    defaults/main.yml   # 默认变量(优先级最低,可被覆盖)

引用一个 role 非常简单:

---
- name: 全站配置
  hosts: all
  become: yes
  roles:
    - common      # 基础:时区、常用工具、SSH 加固
    - nginx
    - php

Roles 的好处是:你写一次"nginx"角色,以后换新机器、开新站,直接复用,不用重抄。社区还有大量现成的 role(比如 `geerlingguy.nginx`、`geerlingguy.mysql`),通过 `ansible-galaxy` 就能拉下来直接用,这又是个人站长省时间的一条捷径。

真实场景复盘:一次配置漂移是怎么被揪出来的

说个我自己的经历。有一次站点在半夜报 502,上去一查是 PHP-FPM 挂了。手动重启后我顺手翻了一下其他几台机器,发现同样的 `pm.max_children` 参数,主站是 20,测试机是 5——明明记得是一起改的。原因很快就清楚了:上个月我改这个参数时,用的是"手动 SSH 逐台改"的老办法,改到测试机的时候正好被别的事情打断,命令敲了一半就忘了。这就是典型的配置漂移(configuration drift)——机器的实际状态和你脑子里的预期状态悄悄分叉了。

把这条参数写进 Ansible 剧本之后,这个问题从根上消失了。剧本文件本身就是"期望状态"的唯一真源,任何机器只要跑一遍剧本就会被拉回这个状态;想知道哪台不一致,加 `--check` 参数干跑一遍即可(只报告会改什么,不实际执行):

ansible-playbook -i inventory.ini site.yml --check --diff

`--check` 会告诉你"这台机器上有 3 个任务需要 changed",`--diff` 还会打印具体哪个文件哪一行会变。我后来把这个命令加进了每周的巡检脚本,输出的天数量立刻变成邮件里的一个数字——从"凭记忆觉得一致"变成"可测量的一致",这是运维心态上的一次跃迁。

几个我踩过的坑,提前告诉你

坑一:YAML 对缩进极其敏感。 用空格不用 Tab,同一层级必须对齐。一个错位的缩进不会报语法错误,而是让任务跑到错误的层级下,行为诡异。建议编辑器装 YAML 插件实时校验。

坑二:`shell` 和 `command` 模块不幂等。 它们总是执行、总是报告 changed。能用专用模块(apt、service、file、template、copy、lineinfile)就绝不要用 shell。确实非用不可的时候,加 `creates:` 或 `changed_when:` 来告诉 Ansible 什么时候算真的变了。

坑三:`ansible_python_interpreter` 在旧系统上会咬人。 现代发行版默认 Python3,但一些老 VPS 上还是 Python2 的路径,连接时会报 `python not found`。在 inventory 的 `[all:vars]` 里显式指定 `/usr/bin/python3` 最保险。

坑四:sudo 需要密码时剧本会卡住。 如果目标机 root 不能直接登录、必须 `sudo` 且有密码,要加 `--ask-become-pass`(`-K`)参数,或在剧本里配 `ansible_become_password`。生产上更推荐给运维账号配好免密 sudo。

坑五:别一上来就全站推。 第一次跑剧本,先 `--check` 干跑,再拿一台机器单独验证(`-l web1 --limit`),确认无误再放开到全组。Ansible 的 `--limit` 是你最好的保险绳:

# 先在单台机器上试
ansible-playbook -i inventory.ini site.yml --limit web1

小结

对个人站长来说,Ansible 的价值不在于"高大上的自动化运维",而在于把"我对每台机器的期望状态"从脑子里、从聊天记录里、从零散的 shell 历史里,搬进一份看得见、可版本管理、可重复执行的剧本文件。它不需要额外服务,一台机器装上就能管住其他所有机器,学习曲线对一个熟悉 Linux 的站长来说也就一两天。

如果你的机器已经超过两台,或者你已经经历过"明明改过却记不清哪台没改"的困惑,那就值得花一个下午把最常用的那几件事——装软件、改配置、重启服务——写成第一份 playbook。之后每加一台新机器,你要做的只是往 inventory 里加一行,然后跑一遍剧本,它就和其它机器完全一致。这种"新机器即插即用、老机器永远对齐"的确定性,是手动运维永远给不了的。

Last modification:October 8th, 2026 at 01:26 pm

Leave a Comment