用 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 统一重启。节奏固定为「干跑 → 单台 → 分批 → 全量」,验证清单确认基线生效后,每台新机器都是一条命令的事。







