Ansible 批量初始化 VPS:一条命令完成用户、密钥、防火墙基线(2026)

逐台 SSH 手工初始化 VPS 的方式,在第二台机器上就该淘汰——把用户、密钥、sshd 加固、UFW 基线、fail2ban 写进一个 Ansible playbook,一条命令两分钟内全部铺完,而且每一台的结果一模一样。 2026 年了,VPS 便宜到动辄一年囤三五台,可很多人的初始化流程还停留在"多开几个终端窗口挨个敲命令"。我上个月新开三台小机做集群测试,第一台手工配完用了二十多分钟,配第二台时就开始记不清哪一步漏了、哪台的 sudoers 忘了收紧。后来把整套流程写成 playbook,第三台从连上机器到 fail2ban 生效只花了两分钟,还顺手沉淀出一份可以永久复用的基线配置。全文围绕 Ubuntu 22.04,从装 Ansible、写 inventory,到完整 playbook、ansible-vault 管密码,以及三个真会踩的坑。

Ansible 批量初始化流程

手撕 SSH 逐台改,到底差在哪?

单台机器上,手工和 Ansible 没区别;机器一多,差距是结构性的。

维度 手工 SSH 逐台改 一条 playbook
三台机器耗时 20 分钟以上,越往后越容易漏 约 2 分钟
一致性 靠记忆,每台都可能差一点 幂等,跑一遍结果必然一致
新机器上线 重来一遍全部步骤 改一行 inventory 重跑
出错回滚 不知道改过什么 playbook 本身就是变更清单
审计 无 Git 里躺着每一次改动

最关键的是幂等性:同一个 playbook 跑十遍,结果和跑一遍完全相同。手工操作做不到这一点——你永远不确定"这台到底改没改过"。之前那套手工流程的细节,我写在SSH 安全加固五步里,本文做的事就是把那五步自动化。

Ansible 初始化步骤总览

环境怎么准备?

控制机(你自己的笔记本或其中一台 VPS)Ubuntu 22.04,apt 直装即可,仓库里的版本在 2.17 以上,足够用:

$ sudo apt update && sudo apt install -y ansible
$ ansible --version
ansible [core 2.17.x]

被控的三台 VPS 只需要一个条件:控制机能用 root 密码(或已配好的密钥)SSH 上去。Ansible 默认走 OpenSSH,目标机不需要装任何 agent,这点比 SaltStack、Puppet 轻得多。

inventory 写在 ~/ansible/inventory.ini:

[vps]
vps1 ansible_host=203.0.113.11
vps2 ansible_host=203.0.113.12
vps3 ansible_host=203.0.113.13

[vps:vars]
ansible_user=root

第一次连接怎么不被 host key 卡住?

新机器第一次连,Ansible 会和手工 SSH 一样弹 host key 确认,批量跑时直接卡死在交互上。两种处理:

# 方式一:先把三台的 host key 收进 known_hosts(推荐)
$ ssh-keyscan -t ed25519 203.0.113.11 203.0.113.12 203.0.113.13 >> ~/.ssh/known_hosts

# 方式二:临时关闭检查,仅首次初始化用,跑完关掉终端
$ export ANSIBLE_HOST_KEY_CHECKING=False

我一开始以为在 ansible.cfg 里全局关掉 host_key_checking 一劳永逸,后来发现这等于亲手放弃了中间人攻击检测—— Ansible 官方文档也只建议在 CI 这类一次性环境里关。日常保持检查开启,只在首次初始化时用环境变量临时关,才是正确姿势。

步骤 1:安装并测试连通

$ ansible vps -i inventory.ini -m ping
vps1 | SUCCESS => { "changed": false, "ping": "pong" }
vps2 | SUCCESS => { "changed": false, "ping": "pong" }
vps3 | SUCCESS => { "changed": false, "ping": "pong" }

三台全部 SUCCESS,控制面就通了。

playbook 里都写了什么?

完整 playbook 存成 init.yaml,干五件事:建 deploy 用户、写公钥、加固 sshd、配 UFW 基线、装 fail2ban。

- name: VPS 初始化基线
  hosts: vps
  become: true
  vars:
    ssh_port: 22022
    admin_key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"

  tasks:
    - name: 创建 deploy 用户
      ansible.builtin.user:
        name: deploy
        shell: /bin/bash
        groups: sudo
        password: "{{ deploy_password | password_hash('sha512') }}"

    - name: 免密 sudo
      ansible.builtin.copy:
        content: "deploy ALL=(ALL) NOPASSWD:ALL\n"
        dest: /etc/sudoers.d/deploy
        mode: "0440"
        validate: "visudo -cf %s"

    - name: 写入 authorized_key
      ansible.posix.authorized_key:
        user: deploy
        key: "{{ admin_key }}"

    - name: sshd 禁密码、改端口
      ansible.builtin.copy:
        content: |
          PasswordAuthentication no
          PermitRootLogin prohibit-password
          Port {{ ssh_port }}
        dest: /etc/ssh/sshd_config.d/99-hardening.conf
        mode: "0600"
      notify: restart sshd

validate: visudo -cf %s 是保命项:sudoers 写错一个字符,你可能永远拿不回 root,所以必须先本地验证语法再落盘。handler 部分:

  handlers:
    - name: restart sshd
      ansible.builtin.service:
        name: ssh
        state: restarted

步骤 2:用户与密钥

UFW 基线,注意 task 顺序——先放行新 SSH 端口,再启用 UFW:

    - name: UFW 放行新 SSH 端口与 Web 端口
      community.general.ufw:
        rule: allow
        port: "{{ item }}"
        proto: tcp
      loop: ["{{ ssh_port }}", "80", "443"]

    - name: UFW 默认拒绝入站
      community.general.ufw:
        direction: incoming
        policy: deny

    - name: 启用 UFW
      community.general.ufw:
        state: enabled

这里的端口放行和 UFW + Docker 那篇是同一个道理:本机防火墙只是两层防线之一,云安全组最好同步收紧。

步骤 3:防火墙基线

fail2ban 一段,Ubuntu 22.04 走 journald 后端:

    - name: 安装 fail2ban
      ansible.builtin.apt:
        name: fail2ban
        state: present

    - name: fail2ban 基线
      ansible.builtin.copy:
        content: |
          [DEFAULT]
          backend = systemd
          bantime = 1d
          findtime = 10m
          maxretry = 3
          [sshd]
          enabled = true
        dest: /etc/fail2ban/jail.local
      notify: restart fail2ban

跑起来就一条命令:

$ ansible-playbook -i inventory.ini init.yaml

输出里每台机器每个 task 一行 changed 或 ok。跑完立刻开一个新终端,验证能否用密钥从 22022 登入,确认通过再关旧会话——这一步和手工加固时的纪律完全一样。

步骤 4:fail2ban 与验证

密码这种敏感值怎么管?

deploy 用户的密码不该明文躺在 playbook 里。用 ansible-vault:

$ ansible-vault create secret.yaml
# 文件里写一行:deploy_password: 你的强密码

# playbook 里通过 vars_files 引入,运行时解密
$ ansible-playbook -i inventory.ini init.yaml --ask-vault-pass
# 或把 vault 口令存进文件,免交互
$ ansible-playbook -i inventory.ini init.yaml --vault-password-file ~/.vault_pass

secret.yaml 本身不进 Git,进仓库的是加密后的 vault 文件。这样 playbook 仓库可以放心同步到多台设备,密码只有持有 vault 口令的人能解开。

三个真会踩的坑

UFW 顺序把自己锁在外面。 我第一次跑的时候把 sshd 端口改到 22022、UFW 的放行列表却写成了 22/80/443——playbook 执行到 UFW enabled 那一步,旧端口规则还在、新端口没放行,后面的 task 全部连接超时,只能从 VPS 控制台进去救火。后来发现必须把"放行新端口"排在"改端口"之前,而且放行列表里写 {{ ssh_port }} 变量而不是写死 22,让两步永远自动对齐。

become 密码没准备。 用 root 直连时 become: true 不需要密码;但如果你把 ansible_user 设成了普通用户,become 会要 sudo 密码,批量跑到第一台就卡住。要么加 --ask-become-pass,要么给该用户配免密 sudo。我的做法是首次初始化用 root,之后所有日常操作都走 deploy 用户。

改完端口,fail2ban 可能还在盯 22。 改端口后跑一遍 fail2ban-client status sshd 确认它在正常计数;如果发现封禁动作没生效,检查 jail 的匹配端口是否需要跟着新端口调整。这类"一个改动牵出另一处遗漏"的问题,恰好是手工方式最容易漏、playbook 一次性写对后永久免疫的。

常见问题

Q:Ansible 和写 shell 脚本 ssh 循环,差别真有那么大吗?
有,核心差别是幂等性。shell 脚本第二次跑,useradd 会报用户已存在,sed 可能重复插入。Ansible 的每个模块天生幂等,重复执行安全;而且某个 task 失败会立刻停在那台机器上,不会像脚本一样"带病跑完全场"。

Q:三台要 2 分钟,五十台会慢吗?
默认 forks = 5,五十台会分十批串行。在 ansible.cfg 里把 forks 调到 20,加上 pipelining = True,五十台也是一分钟量级。SSH 连接建立是主要开销,还可以配 ControlPersist 复用连接。

Q:playbook 跑一半断了怎么办?
直接重跑。幂等性保证已完成的 task 第二次执行显示 ok 而不是再改一遍;也可以用 --start-at-task="任务名" 从断点继续。中断不产生半残状态,这是它比手工可靠的根本原因。

Q:只有一两台 VPS,还有必要上 Ansible 吗?
写一次 playbook 的成本大约一小时,换来的是下次重装系统或加新机器时两条命令搞定,还能顺便当配置文档用。机器会越攒越多,基线越早沉淀越省事——这就是"一次写对、长期复用"的运维效率。

如果重来一次,顺序会怎么排?

结论:先人肉走一遍基线,再把它写成 playbook——顺序反了会把「没验证过的流程」固化成「可重复执行的错误」。

我第一次写这份 playbook 时是直接抄模板,跑到第三台机器才发现 ufw 规则把自己 SSH 端口挡在外面,靠云厂商的 VNC 控制台救回来。重来一次的顺序是:挑一台最便宜的机器,手动做完用户、密钥、sshd、防火墙五步,每步把敲过的命令记进草稿;确认这台机器从零到可用没有卡点,再把草稿翻译成 tasks。上一篇 WireGuard 组网里的那三台机器,就是这样初始化的,一条命令八分钟全部就绪,之后加新 VPS 只改 inventory 一行。playbook 本身进 git 仓库,机器配置从此有版本、有回滚、有审计——这三样是手敲 SSH 永远给不了的。

常见问题

相关阅读

上一篇 WireGuard 自建内网组网:把几台 VPS 和家里设备串成一张私网(2026)