手动SSH登录100台服务器改配置?我试过,改到第30台的时候已经不知道哪些改过了。这篇文章分享我用Ansible实现批量管理的完整过程。
背景:运维的噩梦
去年公司扩张,服务器从10台涨到100+台。以前的运维方式:
bash
# 登录服务器1
ssh root@192.168.1.1
vim /etc/nginx/nginx.conf
systemctl restart nginx
# 登录服务器2
ssh root@192.168.1.2
vim /etc/nginx/nginx.conf
systemctl restart nginx
# 重复100次...
问题来了:
- 改到一半忘了改到哪了
- 有几台配置漏了没发现
- 凌晨3点被叫起来改配置,困得要死还要一台台登录
后来用了Ansible,同样的操作:
bash
ansible all -m shell -a "systemctl restart nginx"
# 1秒搞定100台
效率提升不是一点半点。
一、Ansible是什么?
简单说:批量在多台服务器上执行命令的工具。
和其他工具对比:
| 工具 | 需要Agent | 学习成本 | 适合场景 |
|---|---|---|---|
| Ansible | ❌ 不需要 | 低 | 中小规模,快速上手 |
| SaltStack | ✅ 需要 | 中 | 大规模,实时性要求高 |
| Puppet | ✅ 需要 | 高 | 大规模,配置复杂 |
| Chef | ✅ 需要 | 高 | 大规模,Ruby生态 |
为什么选Ansible:
- 不用在目标服务器装Agent(只要有SSH就行)
- YAML语法,看一眼就懂
- 模块丰富,常见操作都有现成的
二、5分钟快速上手
2.1 安装Ansible
bash
# CentOS/RHEL
yum install -y epel-release
yum install -y ansible
# Ubuntu/Debian
apt update
apt install -y ansible
# macOS
brew install ansible
# 验证安装
ansible --version
# ansible 2.9.27
2.2 配置主机清单
创建 /etc/ansible/hosts:
ini
# 定义主机组
[webservers]
192.168.1.10
192.168.1.11
192.168.1.12
[dbservers]
192.168.1.20
192.168.1.21
[all:vars]
ansible_user=root
ansible_ssh_private_key_file=/root/.ssh/id_rsa
2.3 配置SSH免密登录
bash
# 生成密钥(如果没有)
ssh-keygen -t rsa -b 4096 -N ""
# 批量分发公钥(第一次需要输密码)
for ip in 192.168.1.{10..12} 192.168.1.{20..21}; do
ssh-copy-id root@$ip
done
2.4 测试连通性
bash
# 测试所有主机
ansible all -m ping
# 输出
192.168.1.10 | SUCCESS => {
"ping": "pong"
}
192.168.1.11 | SUCCESS => {
"ping": "pong"
}
...
看到SUCCESS就说明配置成功了。
三、常用命令速查
3.1 Ad-hoc命令(临时执行)
bash
# 查看所有服务器的磁盘使用
ansible all -m shell -a "df -h"
# 查看内存使用
ansible all -m shell -a "free -m"
# 重启nginx
ansible webservers -m service -a "name=nginx state=restarted"
# 复制文件到所有服务器
ansible all -m copy -a "src=/tmp/test.txt dest=/tmp/test.txt"
# 安装软件包
ansible all -m yum -a "name=htop state=present"
# 创建用户
ansible all -m user -a "name=deploy state=present"
3.2 常用模块
| 模块 | 用途 | 示例 |
|---|---|---|
| ping | 测试连通 | ansible all -m ping |
| shell | 执行命令 | ansible all -m shell -a "uptime" |
| copy | 复制文件 | ansible all -m copy -a "src=a dest=b" |
| file | 文件操作 | ansible all -m file -a "path=/tmp/test state=directory" |
| yum/apt | 包管理 | ansible all -m yum -a "name=nginx state=present" |
| service | 服务管理 | ansible all -m service -a "name=nginx state=started" |
| user | 用户管理 | ansible all -m user -a "name=www state=present" |
| cron | 定时任务 | ansible all -m cron -a "name='backup' job='/backup.sh' hour=2" |
四、Playbook:把操作写成剧本
Ad-hoc命令适合临时操作,但复杂任务还是要写Playbook。
4.1 第一个Playbook
创建 nginx-install.yml:
yaml
---
- name: 安装并配置Nginx
hosts: webservers
become: yes # 使用sudo
tasks:
- name: 安装nginx
yum:
name: nginx
state: present
- name: 复制配置文件
copy:
src: ./nginx.conf
dest: /etc/nginx/nginx.conf
backup: yes # 备份原文件
notify: restart nginx # 配置变更时触发handler
- name: 确保nginx运行
service:
name: nginx
state: started
enabled: yes
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
执行:
bash
ansible-playbook nginx-install.yml
# 输出
PLAY [安装并配置Nginx] ****************************
TASK [安装nginx] *********************************
changed: [192.168.1.10]
changed: [192.168.1.11]
changed: [192.168.1.12]
TASK [复制配置文件] *******************************
changed: [192.168.1.10]
changed: [192.168.1.11]
changed: [192.168.1.12]
TASK [确保nginx运行] *****************************
changed: [192.168.1.10]
changed: [192.168.1.11]
changed: [192.168.1.12]
PLAY RECAP ***************************************
192.168.1.10 : ok=3 changed=3 failed=0
192.168.1.11 : ok=3 changed=3 failed=0
192.168.1.12 : ok=3 changed=3 failed=0
3台服务器,一条命令全搞定。
4.2 带变量的Playbook
yaml
---
- name: 部署应用
hosts: webservers
vars:
app_version: "1.2.3"
app_port: 8080
app_user: "www"
tasks:
- name: 创建应用用户
user:
name: "{{ app_user }}"
state: present
- name: 下载应用包
get_url:
url: "https://releases.example.com/app-{{ app_version }}.tar.gz"
dest: "/tmp/app-{{ app_version }}.tar.gz"
- name: 解压应用
unarchive:
src: "/tmp/app-{{ app_version }}.tar.gz"
dest: "/opt/app"
remote_src: yes
- name: 生成配置文件
template:
src: ./app.conf.j2
dest: /opt/app/config/app.conf
配置模板 app.conf.j2:
jinja2
# 应用配置
server.port={{ app_port }}
server.host=0.0.0.0
app.version={{ app_version }}
app.env={{ ansible_hostname }}
4.3 条件判断和循环
yaml
---
- name: 条件和循环示例
hosts: all
tasks:
# 条件判断
- name: 只在CentOS上执行
yum:
name: epel-release
state: present
when: ansible_distribution == "CentOS"
- name: 只在Ubuntu上执行
apt:
name: software-properties-common
state: present
when: ansible_distribution == "Ubuntu"
# 循环安装多个包
- name: 安装常用工具
yum:
name: "{{ item }}"
state: present
loop:
- vim
- htop
- wget
- curl
- net-tools
when: ansible_distribution == "CentOS"
五、实战:批量初始化100台服务器
这是我实际用的服务器初始化Playbook:
5.1 目录结构
csharp
ansible-init/
├── inventory/
│ ├── hosts # 主机清单
│ └── group_vars/
│ └── all.yml # 全局变量
├── roles/
│ ├── common/ # 基础配置
│ ├── security/ # 安全加固
│ ├── monitor/ # 监控Agent
│ └── docker/ # Docker安装
├── site.yml # 主Playbook
└── ansible.cfg # Ansible配置
5.2 主机清单
inventory/hosts:
ini
[webservers]
web[01:20].example.com # web01到web20
[dbservers]
db[01:05].example.com
[cacheservers]
cache[01:10].example.com
[all:vars]
ansible_user=root
ansible_port=22
5.3 全局变量
inventory/group_vars/all.yml:
yaml
---
# 时区
timezone: "Asia/Shanghai"
# NTP服务器
ntp_servers:
- ntp.aliyun.com
- ntp1.aliyun.com
# 运维用户
ops_users:
- name: deploy
ssh_key: "ssh-rsa AAAAB3..."
- name: monitor
ssh_key: "ssh-rsa AAAAB3..."
# 安全配置
ssh_port: 22
ssh_permit_root: "no"
ssh_password_auth: "no"
# 监控配置
node_exporter_version: "1.6.1"
5.4 Common Role
roles/common/tasks/main.yml:
yaml
---
- name: 设置主机名
hostname:
name: "{{ inventory_hostname }}"
- name: 配置时区
timezone:
name: "{{ timezone }}"
- name: 安装基础软件包
yum:
name:
- vim
- wget
- curl
- htop
- iotop
- net-tools
- telnet
- lsof
- tree
- rsync
- chrony
state: present
- name: 配置NTP
template:
src: chrony.conf.j2
dest: /etc/chrony.conf
notify: restart chrony
- name: 启动chrony服务
service:
name: chronyd
state: started
enabled: yes
- name: 配置内核参数
sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
state: present
reload: yes
loop:
- { name: 'net.ipv4.tcp_tw_reuse', value: '1' }
- { name: 'net.ipv4.tcp_fin_timeout', value: '30' }
- { name: 'net.core.somaxconn', value: '65535' }
- { name: 'vm.swappiness', value: '10' }
- name: 配置文件描述符限制
pam_limits:
domain: '*'
limit_type: "{{ item.type }}"
limit_item: nofile
value: "{{ item.value }}"
loop:
- { type: 'soft', value: '65535' }
- { type: 'hard', value: '65535' }
5.5 Security Role
roles/security/tasks/main.yml:
yaml
---
- name: 创建运维用户
user:
name: "{{ item.name }}"
shell: /bin/bash
groups: wheel
append: yes
loop: "{{ ops_users }}"
- name: 配置SSH公钥
authorized_key:
user: "{{ item.name }}"
key: "{{ item.ssh_key }}"
state: present
loop: "{{ ops_users }}"
- name: 配置sudo免密
lineinfile:
path: /etc/sudoers.d/ops
line: "{{ item.name }} ALL=(ALL) NOPASSWD: ALL"
create: yes
mode: '0440'
loop: "{{ ops_users }}"
- name: 加固SSH配置
lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin {{ ssh_permit_root }}' }
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication {{ ssh_password_auth }}' }
- { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' }
- { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 60' }
notify: restart sshd
- name: 配置防火墙
firewalld:
port: "{{ item }}"
permanent: yes
state: enabled
loop:
- "{{ ssh_port }}/tcp"
- "80/tcp"
- "443/tcp"
notify: reload firewalld
5.6 主Playbook
site.yml:
yaml
---
- name: 服务器初始化
hosts: all
become: yes
roles:
- common
- security
- monitor
- name: Web服务器配置
hosts: webservers
become: yes
roles:
- docker
- nginx
- name: 数据库服务器配置
hosts: dbservers
become: yes
roles:
- docker
- mysql
5.7 执行
bash
# 检查语法
ansible-playbook site.yml --syntax-check
# 模拟执行(不实际改变)
ansible-playbook site.yml --check
# 正式执行
ansible-playbook site.yml
# 只执行特定tag
ansible-playbook site.yml --tags "security"
# 限定特定主机
ansible-playbook site.yml --limit "web01.example.com"
六、进阶:异地服务器怎么管?
公司在北京、上海、深圳都有机房,网络不通怎么办?
方案1:跳板机
ini
[beijing]
bj-web01 ansible_host=192.168.1.10
[shanghai]
sh-web01 ansible_host=10.0.1.10
[beijing:vars]
ansible_ssh_common_args='-o ProxyJump=jump@jump.bj.example.com'
[shanghai:vars]
ansible_ssh_common_args='-o ProxyJump=jump@jump.sh.example.com'
缺点:
- 跳板机挂了就完蛋
- 速度慢(经过两跳)
- 配置复杂
方案2:VPN
在每个机房部署VPN客户端,打通网络。
缺点:
- VPN经常断
- 带宽受限
- 需要专人维护
方案3:SD-WAN组网(推荐)
我现在用的方案是星空组网,在每个机房的管理节点装一个客户端:
bash
# 北京机房
curl -sSL https://down.starvpn.cn/linux.sh | bash
xkcli login your_token
xkcli up
# 上海机房
# 深圳机房
# 同样操作...
组网后,所有机房的服务器都在一个虚拟局域网里:
北京机房 192.168.188.x
上海机房 192.168.188.y
深圳机房 192.168.188.z
主机清单直接用虚拟IP:
ini
[beijing]
192.168.188.10
192.168.188.11
[shanghai]
192.168.188.20
192.168.188.21
[shenzhen]
192.168.188.30
192.168.188.31
效果:
- 延迟:北京到上海 30ms(VPN要80ms+)
- 稳定性:用了3个月没断过
- 配置:比VPN简单多了
七、常见问题排查
7.1 SSH连接超时
bash
# 检查SSH连通性
ssh -vvv root@192.168.1.10
# 常见原因
1. 防火墙没开22端口
2. SSH服务没启动
3. 密钥权限问题(应该是600)
7.2 权限不足
yaml
# 在Playbook中加become
- name: 需要root权限的操作
hosts: all
become: yes # 使用sudo
become_user: root
7.3 模块执行失败
bash
# 增加详细输出
ansible all -m yum -a "name=nginx state=present" -vvv
# 查看模块文档
ansible-doc yum
7.4 Playbook执行太慢
yaml
# ansible.cfg 优化
[defaults]
forks = 50 # 并发数,默认5
host_key_checking = False
timeout = 30
[ssh_connection]
pipelining = True # 开启pipelining,减少SSH连接次数
control_path = /tmp/ansible-%%h-%%r
八、总结
用Ansible之前:
- 100台服务器改配置要半天
- 经常漏改、错改
- 凌晨被叫起来手动操作
用Ansible之后:
- 100台服务器1分钟搞定
- 操作可审计、可回滚
- 写好Playbook,新人也能操作
我的建议:
- 从简单的Ad-hoc命令开始
- 把重复操作写成Playbook
- 逐步积累自己的Role库
- 异地服务器用组网工具打通
如果你也在管理