用 Ansible 批量初始化新服务器:账号、SSH 与安全基线一次到位

新服务器交付前有一串重复劳动:建账号、发 SSH 密钥、改 sshd 配置、装工具、设时区和内核参数。手工做,十台机器十个样;写成 Ansible 角色,一条命令跑完且可重复执行。本文给出一个可直接套用的初始化角色,以 Rocky Linux 9 为主,差异处注明 Ubuntu 做法。

前置条件

  • 控制机装好 ansible-core(2.14+),能 SSH 登录所有目标机;
  • 目标机已装好系统、规划好 IP 与主机名(示例 node-01~node-03);
  • 首次可用密码登录,分发密钥后再关密码登录。

第一步:组织 inventory

清单按环境和角色分组,分批执行就靠它(组变量设 ansible_user=deploy、ansible_become=true):

[init]
node-01 ansible_host=192.0.2.11
node-02 ansible_host=192.0.2.12
node-03 ansible_host=192.0.2.13

第二步:角色目录布局

初始化动作收进一个角色 server_init,任务按主题拆文件:

roles/server_init/
├── defaults/main.yml   # 变量
├── tasks/
│   ├── main.yml
│   ├── users.yml       # 用户
│   ├── ssh.yml         # ssh
│   ├── base.yml        # 基础
│   ├── sysctl.yml      # 参数
│   └── services.yml    # 服务
├── handlers/main.yml   # 重启
└── files/              # 文件

第三步:编写各部分任务

用户与 sudo

- name: 建运维组
  group:
    name: ops

- name: 建用户
  user:
    name: "{{ item.name }}"
    groups: ops
    shell: /bin/bash
  loop: "{{ init_users }}"

- name: sudoers 片段
  copy:
    src: ops_sudoers
    dest: /etc/sudoers.d/ops
    mode: "0440"
    validate: "visudo -cf %s"

片段务必带 validate,防语法错误锁死 sudo;形如 %ops ALL=(ALL) NOPASSWD: ALL,免密与否按规范定。

SSH 密钥与 sshd 加固

- name: 分发公钥
  ansible.posix.authorized_key:
    user: "{{ item.name }}"
    key: "{{ item.pubkey }}"
  loop: "{{ init_users }}"

- name: sshd 加固片段
  copy:
    dest: /etc/ssh/sshd_config.d/10-hardening.conf
    mode: "0600"
    content: |
      PermitRootLogin no
      PasswordAuthentication no
      MaxAuthTries 3
  notify: 重启 sshd

先确认主配置有 Include 行,drop-in 才生效。顺序务必先发密钥、验证能登录,再关密码登录,否则把自己锁在门外。

时区、时间同步与基础软件包

- name: 设置时区
  community.general.timezone:
    name: Asia/Shanghai

- name: 安装基础软件包(Rocky)
  dnf:
    name: [vim, curl, tmux, lsof, chrony]
  when: ansible_os_family == "RedHat"

Ubuntu 换 apt 模块并加 update_cache: true。chrony 另用 systemd 模块启用(服务名 Rocky 为 chronyd、Ubuntu 为 chrony)。

内核参数与 ulimit 基线

- name: 内核参数
  ansible.posix.sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
    sysctl_file: /etc/sysctl.d/90-baseline.conf
    reload: true
  loop:
    - { key: "net.ipv4.ip_forward", value: "0" }
    - { key: "vm.swappiness", value: "10" }
    - { key: "fs.file-max", value: "1000000" }

- name: ulimit 基线
  community.general.pam_limits:
    domain: "*"
    limit_type: "{{ item.0 }}"
    limit_item: "{{ item.1 }}"
    value: "65535"
  loop: "{{ ['soft', 'hard'] | product(['nofile', 'nproc']) | list }}"

limits 需重新登录才生效;systemd 服务要单独设 LimitNOFILE,不跟 limits 走。

关闭无用服务

- name: 停用无用服务
  systemd:
    name: "{{ item }}"
    enabled: false
    state: stopped
    masked: true
  loop: [bluetooth, cups, avahi-daemon]
  failed_when: false

清单按装机情况裁剪,masked 前确认无业务依赖;failed_when 防未装的服务中断整批。

第四步:幂等、handlers 与执行方式

幂等靠声明式模块:重复执行收敛到目标状态;不得不用 shell 时加 creates 或 changed_when,否则每次报 changed 并触发无谓重启。handlers 只放重启 sshd 一个任务,服务名用变量 sshd_service 按发行版取值,ssh.yml 改配置时 notify。

执行三步:干跑看差异、单台试点、分批全量(play 里写 serial: 2):

ansible-playbook -i inventory/hosts.ini init.yml --check --diff
ansible-playbook -i inventory/hosts.ini init.yml --limit node-01
ansible-playbook -i inventory/hosts.ini init.yml

验证清单

  • ansible init -a "timedatectl":时区为 Asia/Shanghai,时钟已同步;
  • 新账号密钥能登录,手动 ssh 以 root 登录应被拒;
  • sysctl vm.swappiness fs.file-max 与基线一致,重登后 ulimit -n 为 65535;
  • 重跑 playbook 应 changed=0,即幂等达标。

常见问题

  • 关掉密码登录后把自己锁死:最常见翻车。顺序永远是先发钥、新终端验证、再关密码;批量先 --limit 单台试点。
  • sshd 重启 handler 名称对不上:RHEL 系是 sshd、Ubuntu 是 ssh。服务名做成变量按 ansible_os_family 取值,别写死。
  • sudoers 校验失败中断:说明片段语法有问题,validate 挡下坏文件是好事;本地先跑 visudo -c 验证再分发。
  • --check 下某些任务报错:command/shell 类在 check 模式行为不一致,只读无害的加 check_mode: false;变量为空的模板报错在使用处加 default 兜底。

小结

价值在收敛:变量集中、任务按主题拆分、声明式模块保幂等、handlers 统一重启。节奏固定为「干跑 → 单台 → 分批 → 全量」,验证清单确认基线生效后,每台新机器都是一条命令的事。

发表回复

后才能评论