某互联网企业500台服务器统一运维解决方案从零搭建Ansible分布式自动化部署平台全流程实战记录
作为一个在互联网行业摸爬滚打多年的运维工程师,我经历过那个让人痛不欲生的时代——每天早上打开机房监控大屏,500台服务器,每台都要挨个上去检查服务状态、升级代码、调整配置。那时候我们团队三个人,每周至少要熬夜两次做批量发布,有一次因为手抖输错了一台机器的IP,差点把生产环境搞炸。
后来我们开始研究自动化运维工具,对比了SaltStack、Puppet、Chef,最后选择了Ansible。不是因为它是最强的,而是因为它零Agent、基于SSH、学习曲线平缓,特别适合我们这种中小型互联网团队。今天就把我们这趟搭建之旅完整的记录下来,希望能帮到正在面临同样困境的你。
为什么要搭建这个平台
在我们做这个决定之前,先说清楚一个问题:什么情况下你需要自动化运维平台?
如果你现在的状态是——每次上线都像在走钢丝,每次改配置都要登录几十台机器手动执行,每次出问题都要翻日志翻到怀疑人生,那你大概率需要这个。
我们公司的业务线大概是这样分布的:
- Web服务集群:150台,跑Nginx + PHP-FPM,负责对外提供API和页面服务
- 微服务集群:200台,跑Go和Java服务,拆分成三十几个微服务
- 大数据集群:80台,Hadoop、Spark、Flink各种组件混在一起
- 中间件集群:40台,MySQL、Redis、Kafka、Elasticsearch
- 支撑集群:30台,监控、日志、CI/CD、内部工具平台
加起来500台,分布在三个机房,跨两个城市。没有自动化之前,我们每周要执行至少20次批量操作,每次操作平均耗时4-6小时,而且容易出错。
技术选型:为什么是Ansible
很多人会问,为什么不用SaltStack?为什么不用Puppet?
说实话,我们在选型阶段把这些都试了一遍。
SaltStack的优势是速度极快,基于消息队列通信,大规模并发时比Ansible快不少。但它的缺点也很明显——需要安装Agent,而且Salt-master在高负载下容易出问题,我们测试到300台的时候就发现消息队列积压了。
Puppet和Chef是老牌选手,功能强大,但它们有各自的一套语言,学习成本比较高,而且也需要安装Agent。对于我们这种团队规模,投入产出比不太划算。
Ansible的优势在于:
- 零Agent:只需要目标机器上有Python环境,通过SSH就能控制
- 声明式配置:用YAML写playbook,可读性强,新人上手快
- 幂等性:同样的操作执行多次,结果一致,不会破坏环境
- 丰富的模块:开箱即用,不需要自己造轮子
- 社区活跃:roles和collection生态丰富,遇到问题很容易找到解决方案
它的缺点嘛,就是纯SSH连接在超大规模(上千台)时性能会下降,但我们500台的规模,完全没问题。
基础设施准备阶段
硬件和网络规划
在动手之前,我们先花了两天时间梳理网络拓扑。
我们的500台服务器分属三个VLAN,跨两个数据中心。跨机房通信会有延迟,所以我们决定把所有Ansible控制节点都部署在数据中心A,通过跳板机的方式管理数据中心B的机器。
控制节点规划:
- 主控制节点:8核16G,SSD,部署在DC-A
- 从控制节点(备用):4核8G,部署在DC-B,用于跨机房管理
被管理节点的网络要求:
- 所有机器必须能SSH到控制节点
- 控制节点必须能SSH到所有被管理节点
- 端口默认22,开放给控制节点IP段
我们在生产环境做了个安全组策略,只允许控制节点的IP访问22端口,其他所有IP一律拒绝。这个安全策略在上线前必须配好,不然会出大问题。
操作系统环境准备
我们的服务器大部分是CentOS 7.9和Ubuntu 20.04混合部署。Ansible官方推荐Python 3.8+,所以我们在控制节点上做了统一的环境准备。
先在控制节点上安装Python和pip:
# CentOS 7.9 环境准备
yum install -y python38 python38-pip python38-devel
alternatives --set python /usr/bin/python3.8
pip3 install --upgrade pip
# Ubuntu 20.04 环境准备
apt update
apt install -y python3 python3-pip python3-venv
python3 --version
pip3 --version
然后我们创建了一个专用的运维用户,不直接用root,这是基本的安全规范:
# 创建运维用户
useradd -m -s /bin/bash ops
passwd ops
# 配置sudo权限(不需要密码,方便自动化)
echo 'ops ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/ops
visudo -c # 验证配置文件语法是否正确
被管理节点上只需要确保有Python环境即可,Ansible会通过SSH连接过去检查并安装所需的Python模块。如果某些老旧机器没有Python3,可以用这个命令快速安装:
# CentOS
yum install -y python3
# Ubuntu
apt install -y python3
# 验证
python3 --version
Ansible安装和基础配置
控制节点安装
我们在主控制节点上创建了一个虚拟环境,避免污染系统Python:
# 创建项目目录
mkdir -p /opt/ansible/{roles,playbooks,inventory,vars,logs}
cd /opt/ansible
# 创建虚拟环境
python3 -m venv venv
source venv/bin/activate
# 安装Ansible和相关依赖
pip install ansible ansible-lint ansible-vault
# 验证安装
ansible --version
安装完成后,你会看到类似这样的输出:
ansible [core 2.14.0]
config file = None
configured module search path = ['/home/ops/.ansible/plugins/modules']
ansible python module location = /opt/ansible/venv/lib/python3.8/site-packages/ansible
executable location = /opt/ansible/venv/bin/ansible
python version = 3.8.10 (default, Nov 14 12:10:39)
ansible-core 2.14.0
基础配置文件
在/opt/ansible/下创建ansible.cfg配置文件:
[defaults]
# 主机清单路径
inventory = ./inventory/hosts
# 远程用户
remote_user = ops
# 私钥路径
private_key_file = ~/.ssh/id_rsa
# 角色默认路径
roles_path = ./roles
# playbook默认路径
library = ./library
# 日志路径
log_path = ./logs/ansible.log
# 并行fork数,500台机器建议开大一点
forks = 50
# 超时时间
timeout = 10
# 开启host key检查
host_key_checking = False
# 启用回调插件,输出更友好的日志
stdout_callback = yaml
# 禁用become警告
dangerously_disable_host_key_checking = False
[privilege_escalation]
# 需要root权限时自动提升
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
# SSH连接优化
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no
这里解释一下几个关键参数:
forks = 50:同时并发50台机器执行任务,我们500台大概10秒就能跑完一个playbook,之前手动执行要几个小时pipelining = True:SSH管道化,减少每次模块执行的SSH连接次数,性能提升明显ControlMaster=auto:SSH连接复用,同一个控制节点到同一个被管理节点的SSH连接会保持60秒,避免重复建立连接
主机清单配置
这是最关键的部分。我们500台机器的inventoy采用分层分组的方式组织:
# inventory/hosts
[web_servers]
web01 ansible_host=10.0.1.11
web02 ansible_host=10.0.1.12
web03 ansible_host=10.0.1.13
# ... 共150台
[microservices]
ms-auth ansible_host=10.0.2.11
ms-order ansible_host=10.0.2.12
ms-pay ansible_host=10.0.2.13
ms-user ansible_host=10.0.2.14
# ... 共200台,按微服务分组
[data_platform]
hadoop01 ansible_host=10.0.3.11
hadoop02 ansible_host=10.0.3.12
spark01 ansible_host=10.0.3.21
flink01 ansible_host=10.0.3.31
# ... 共80台
[middleware]
mysql-master ansible_host=10.0.4.11
mysql-slave01 ansible_host=10.0.4.12
mysql-slave02 ansible_host=10.0.4.13
redis01 ansible_host=10.0.4.21
kafka01 ansible_host=10.0.4.31
es01 ansible_host=10.0.4.41
# ... 共40台
[support]
monitor01 ansible_host=10.0.5.11
log01 ansible_host=10.0.5.12
ci01 ansible_host=10.0.5.21
# ... 共30台
# 分组便于批量操作
[all:children]
web_servers
microservices
data_platform
middleware
support
# 按机房分组
[dc_a:children]
web_servers
microservices
middleware
support
[dc_b:children]
data_platform
# 按功能分组
[backend:children]
microservices
data_platform
[database:children]
middleware
这样分组后,你可以方便地针对特定组执行操作:
# 只重启web集群
ansible web_servers -m shell -a "systemctl restart nginx"
# 只升级大数据集群的组件
ansible data_platform -m yum -a "name=xxx state=latest"
# 对所有机器执行健康检查
ansible all -m ping
SSH免密登录配置
这是很多新手容易踩坑的地方。500台机器,如果每台都要手动输入密码,那自动化就无从谈起。
我们在控制节点上生成了SSH密钥对:
# 生成密钥对(不设置密码,方便自动化)
ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa
# 查看公钥
cat ~/.ssh/id_rsa.pub
然后把公钥分发到所有被管理节点。我们有两种方式:
方式一:使用Ansible的shell模块自动分发
# 用root密码批量分发(需要先配置host_vars)
ansible all -m shell -a "echo 'ssh-rsa AAAA...' >> ~/.ssh/authorized_keys"
方式二:使用sshpass手动分发(适合初期少量机器)
yum install -y sshpass
sshpass -p 'your_password' ssh-copy-id -o StrictHostKeyChecking=no ops@10.0.1.11
实际操作中,我们采用的是混合方案:先用脚本把密钥分发到第一批50台机器,然后用这50台作为跳板机,通过ansible.cfg中的control_path机制实现SSH连接复用,再快速分发到其他机器。
# inventory/group_vars/all.yml - 配置SSH连接参数
---
ansible_ssh_common_args: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'
ansible_ssh_extra_args: '-o ControlMaster=auto -o ControlPersist=60s'
第一个playbook:健康检查
配置好环境后,我们写的第一个playbook是健康检查,用来验证所有机器是否连通、Python环境是否就绪。
# playbooks/health_check.yml
---
- name: 服务器健康检查
hosts: all
gather_facts: yes
strategy: linear
tasks:
- name: 检查SSH连通性
ansible.builtin.ping:
- name: 收集系统信息
ansible.builtin.setup:
register: system_info
- name: 显示主机信息
ansible.builtin.debug:
msg: |
主机名: {{ ansible_hostname }}
操作系统: {{ ansible_distribution }} {{ ansible_distribution_version }}
CPU核心数: {{ ansible_processor_cores }}
内存总量: {{ ansible_memtotal_mb }} MB
运行时间: {{ ansible_uptime_seconds }} 秒
IP地址: {{ ansible_default_ipv4.address }}
- name: 检查磁盘使用率
ansible.builtin.shell: df -h / | awk 'NR==2 {print $5}'
register: disk_usage
changed_when: false
- name: 显示磁盘信息
ansible.builtin.debug:
msg: "根分区使用率: {{ disk_usage.stdout }}"
- name: 检查内存使用率
ansible.builtin.shell: free | awk '/Mem/ {printf("%.2f%%"), $3/$2 * 100.0}'
register: mem_usage
changed_when: false
- name: 显示内存信息
ansible.builtin.debug:
msg: "内存使用率: {{ mem_usage.stdout }}"
- name: 检查关键服务状态
ansible.builtin.service:
name: "{{ item }}"
state: started
loop:
- sshd
- chronyd
ignore_errors: yes
- name: 检查SSH服务状态
ansible.builtin.debug:
msg: "SSH服务: {{ ansible_service_fact.sshd }} (状态: {{ ansible_facts.services['sshd'].status }})"
when: ansible_facts.services is defined
执行这个playbook:
cd /opt/ansible
source venv/bin/activate
ansible-playbook playbooks/health_check.yml -v
你会看到类似这样的输出:
PLAY [服务器健康检查] ********************************************************
TASK [Gathering Facts] *******************************************************
ok: [web01]
ok: [web02]
ok: [web03]
... (500台)
TASK [检查SSH连通性] *********************************************************
ok: [web01] => {
"changed": false,
"ping": "pong"
}
ok: [web02] => {
"changed": false,
"ping": "pong"
}
...
TASK [收集系统信息] **********************************************************
ok: [web01]
ok: [web02]
...
TASK [显示主机信息] **********************************************************
ok: [web01] => {
"msg": "主机名: web01\n操作系统: CentOS 7.9\nCPU核心数: 8\n内存总量: 32768 MB\n运行时间: 123456 秒\nIP地址: 10.0.1.11\n"
}
...
PLAY RECAP *******************************************************************
web01 : ok=7 changed=0 unreachable=0 failed=0
web02 : ok=7 changed=0 unreachable=0 failed=0
...
当500台机器全部显示ok=7 changed=0 unreachable=0 failed=0时,说明环境搭建成功。这个阶段我们花了大约3天时间,包括网络调试、SSH配置、权限问题处理等。
构建角色体系
角色(Role)是Ansible组织代码的核心方式。我们按照服务类型和业务逻辑,构建了以下几个核心角色:
角色目录结构
roles/
├── common/ # 基础系统配置(所有机器都需要)
│ ├── tasks/
│ │ └── main.yml
│ ├── handlers/
│ │ └── main.yml
│ ├── templates/
│ │ ├── ntp.conf.j2
│ │ ├── sshd_config.j2
│ │ └── sysctl.conf.j2
│ ├── vars/
│ │ └── main.yml
│ ├── files/
│ │ └── motd
│ └── meta/
│ └── main.yml
├── nginx/ # Nginx配置
│ ├── tasks/main.yml
│ ├── templates/nginx.conf.j2
│ └── handlers/main.yml
├── php/ # PHP-FPM配置
│ ├── tasks/main.yml
│ └── templates/php-fpm.conf.j2
├── mysql/ # MySQL配置
│ ├── tasks/main.yml
│ ├── templates/my.cnf.j2
│ └── handlers/main.yml
├── redis/ # Redis配置
│ ├── tasks/main.yml
│ └── templates/redis.conf.j2
├── kafka/ # Kafka配置
│ ├── tasks/main.yml
│ └── templates/server.properties.j2
├── elasticsearch/ # ES配置
│ ├── tasks/main.yml
│ └── templates/elasticsearch.yml.j2
├── app_deploy/ # 应用部署
│ ├── tasks/main.yml
│ └── templates/
├── monitoring/ # 监控Agent
│ ├── tasks/main.yml
│ └── templates/
└── log_aggregation/ # 日志采集
├── tasks/main.yml
└── templates/
common角色详解
这是最基础也是最常用的角色,所有机器都会用到。
# roles/common/tasks/main.yml
---
- name: 更新系统包
ansible.builtin.yum:
name: '*'
state: latest
when: ansible_distribution == 'CentOS'
notify: 重启系统
- name: 安装常用工具
ansible.builtin.yum:
name:
- curl
- wget
- git
- vim
- htop
- net-tools
- tree
- jq
- bash-completion
state: present
- name: 配置NTP时间同步
ansible.builtin.template:
src: ntp.conf.j2
dest: /etc/chrony.conf
notify: 重启chronyd
- name: 优化SSH配置
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
notify: 重启sshd
- name: 内核参数优化
ansible.builtin.template:
src: sysctl.conf.j2
dest: /etc/sysctl.conf
notify: 应用sysctl参数
- name: 配置防火墙规则
ansible.builtin.firewalld:
service: "{{ item }}"
state: enabled
permanent: true
immediate: true
loop:
- ssh
- http
- https
when: ansible_distribution == 'CentOS'
- name: 关闭SELinux
ansible.builtin.selinux:
state: permissive
when: ansible_distribution == 'CentOS'
- name: 配置motd欢迎信息
ansible.builtin.copy:
src: motd
dest: /etc/motd
mode: '0644'
- name: 配置ulimit
ansible.builtin.lineinfile:
path: /etc/security/limits.conf
line: "{{ item }}"
state: present
loop:
- '* soft nofile 65535'
- '* hard nofile 65535'
- '* soft nproc 65535'
- '* hard nproc 65535'
对应的handlers:
# roles/common/handlers/main.yml
---
- name: 重启系统
ansible.builtin.reboot:
msg: "Ansible系统更新后重启"
connect_timeout: 10
reboot_timeout: 300
pre_reboot_delay: 0
post_reboot_delay: 30
test_command: whoami
- name: 重启chronyd
ansible.builtin.systemd:
name: chronyd
state: restarted
- name: 重启sshd
ansible.builtin.systemd:
name: sshd
state: restarted
- name: 应用sysctl参数
ansible.builtin.command: sysctl -p
changed_when: true
模板文件示例:
# roles/common/templates/nt p.conf.j2
# NTP配置文件
# 根据机器角色选择NTP服务器
{% if groups['data_platform'] is defined and inventory_hostname in groups['data_platform'] %}
# 大数据集群使用内部NTP
server ntp-internal-01.local iburst
server ntp-internal-02.local iburst
{% else %}
# 其他机器使用公共NTP
server 0.centos.pool.ntp.org iburst
server 1.centos.pool.ntp.org iburst
server 2.centos.pool.ntp.org iburst
server 3.centos.pool.ntp.org iburst
{% endif %}
driftfile /var/lib/chrony/drift
makestep 1.0 3
logdir /var/log/chrony
# roles/common/templates/sshd_config.j2
# SSHD配置优化
Port 22
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding no
PrintMotd no
AcceptEnv LANG LC_*
Subsystem sftp /usr/lib/openssh/sftp-server
# 连接优化
ClientAliveInterval 60
ClientAliveCountMax 3
MaxSessions 10
LoginGraceTime 30
# 安全增强
AllowUsers ops deploy
DenyUsers root admin
AllowGroups ops wheel
# 根据主机角色定制
{% if 'database' in group_names %}
# 数据库服务器限制更严格
AllowUsers ops
{% endif %}
变量管理
好的变量管理是自动化运维成功的半壁江山。我们采用了分层变量策略:
inventory/
├── hosts # 主机清单
├── group_vars/
│ ├── all.yml # 所有机器通用变量
│ ├── web_servers.yml # Web集群变量
│ ├── microservices.yml # 微服务集群变量
│ ├── data_platform.yml # 大数据集群变量
│ ├── middleware.yml # 中间件集群变量
│ └── support.yml # 支撑集群变量
└── host_vars/
├── web01.yml
├── web02.yml
└── ...
group_vars/all.yml示例:
# 所有机器通用变量
---
# 基础信息
domain: example.com
env: prod
timezone: Asia/Shanghai
# 用户管理
ops_user: ops
ops_uid: 1000
# 包管理器(用于条件判断)
package_manager: yum
# 安全配置
ssh_permit_root_login: no
ssh_password_auth: no
selinux_state: permissive
firewall_enabled: true
# 系统优化
max_open_files: 65535
max_user_processes: 65535
transparent_hugepages: always
# 监控
monitoring_enabled: true
node_exporter_port: 9100
group_vars/web_servers.yml示例:
# Web服务器集群变量
---
# Nginx配置
nginx_version: 1.24.0
nginx_worker_processes: auto
nginx_worker_connections: 4096
nginx_keepalive_timeout: 65
# PHP配置
php_version: "8.1"
php_fpm_max_children: 128
php_fpm_start_servers: 8
php_fpm_min_spare_servers: 4
php_fpm_max_spare_servers: 16
php_memory_limit: "256M"
php_max_execution_time: 300
# 日志路径
log_path: /var/log/nginx
access_log_path: /var/log/nginx/access.log
error_log_path: /var/log/nginx/error.log
host_vars/web01.yml示例:
# 单台机器特定变量
---
# 主机名
hostname: web01
# 网络配置
interfaces:
eth0:
ip: 10.0.1.11
gateway: 10.0.1.1
# 磁盘挂载
data_disk: /data
log_disk: /var/log
# 资源限制
nginx_worker_connections: 8192 # 这台机器性能较好,提高连接数
php_fpm_max_children: 256
核心Playbook编写
系统初始化Playbook
# playbooks/init_system.yml
---
- name: 系统初始化
hosts: all
become: yes
serial: "20%" # 每次只处理20%的机器,防止大规模重启导致服务中断
pre_tasks:
- name: 记录初始化开始时间
ansible.builtin.debug:
msg: "开始对 {{ inventory_hostname }} 进行系统初始化"
- name: 创建备份目录
ansible.builtin.file:
path: "/backup/{{ inventory_hostname }}/{{ ansible_date_time.date }}"
state: directory
mode: '0755'
roles:
- role: common
post_tasks:
- name: 记录初始化完成时间
ansible.builtin.debug:
msg: "{{ inventory_hostname }} 系统初始化完成"
- name: 生成初始化报告
ansible.builtin.template:
src: init_report.j2
dest: "/backup/{{ inventory_hostname }}/{{ ansible_date_time.date }}/init_report.txt"
handlers:
- name: 重启系统
ansible.builtin.reboot:
reboot_timeout: 600
这里用到了serial: "20%",这个参数非常重要。500台机器如果一次性全部重启,可能会导致网络风暴或者存储IO压力过大。分批执行可以控制风险。
应用部署Playbook
这是最复杂的场景,我们以PHP应用部署为例:
# playbooks/deploy_web_app.yml
---
- name: Web应用部署
hosts: web_servers
become: yes
serial: "10%" # 分批部署,先部署10%观察效果
vars:
app_name: myapp
app_version: "{{ lookup('env', 'APP_VERSION') | default('v1.0.0') }}"
deploy_path: /data/apps/{{ app_name }}
repo_url: git@github.com:example/{{ app_name }}.git
pre_tasks:
- name: 检查应用版本
ansible.builtin.debug:
msg: "准备部署 {{ app_name }} {{ app_version }} 到 {{ inventory_hostname }}"
- name: 创建部署目录
ansible.builtin.file:
path: "{{ deploy_path }}/{{ app_version }}"
state: directory
owner: ops
group: ops
mode: '0755'
- name: 检查当前版本
ansible.builtin.shell: |
if [ -L "{{ deploy_path }}/current" ]; then
readlink "{{ deploy_path }}/current"
else
echo "none"
fi
register: current_version
changed_when: false
- name: 显示当前版本
ansible.builtin.debug:
msg: "当前版本: {{ current_version.stdout }}"
roles:
- role: nginx
- role: php
tasks:
- name: 拉取代码
ansible.builtin.git:
repo: "{{ repo_url }}"
dest: "{{ deploy_path }}/{{ app_version }}"
version: "{{ app_version }}"
force: yes
register: git_result
- name: 安装依赖
ansible.builtin.shell: |
cd {{ deploy_path }}/{{ app_version }}
composer install --no-dev --optimize-autoloader
npm install --production
args:
chdir: "{{ deploy_path }}/{{ app_version }}"
when: git_result.changed
- name: 设置目录权限
ansible.builtin.file:
path: "{{ item }}"
state: directory
recurse: yes
owner: ops
group: ops
mode: '0755'
loop:
- "{{ deploy_path }}/{{ app_version }}/storage"
- "{{ deploy_path }}/{{ app_version }}/bootstrap/cache"
- "{{ deploy_path }}/{{ app_version }}/public/uploads"
- name: 创建符号链接
ansible.builtin.file:
src: "{{ deploy_path }}/{{ app_version }}"
dest: "{{ deploy_path }}/current"
state: link
force: yes
- name: 重载Nginx配置
ansible.builtin.systemd:
name: nginx
state: reloaded
post_tasks:
- name: 验证部署
ansible.builtin.uri:
url: "http://127.0.0.1/health"
status_code: 200
register: health_check
until: health_check.status == 200
retries: 5
delay: 10
ignore_errors: yes
- name: 显示部署结果
ansible.builtin.debug:
msg: |
部署完成!
版本: {{ app_version }}
路径: {{ deploy_path }}/current
健康检查: {{ health_check.status | default('未执行') }}
- name: 清理旧版本(保留最近5个)
ansible.builtin.shell: |
cd {{ deploy_path }}
ls -dt {{ app_name }}-v* | tail -n +6 | xargs -r rm -rf
changed_when: false
这个playbook体现了几个重要的最佳实践:
- 分批部署:
serial: "10%"确保一次只部署50台(10%),观察效果后再继续 - 健康检查:部署后自动验证服务是否正常
- 版本管理:保留历史版本,出现问题可以快速回滚
- 幂等性:所有操作都是幂等的,可以重复执行
回滚playbook就很简单了:
# playbooks/rollback_web_app.yml
---
- name: 回滚Web应用
hosts: web_servers
become: yes
serial: "10%"
vars:
app_name: myapp
deploy_path: /data/apps/{{ app_name }}
rollback_version: "{{ lookup('env', 'ROLLBACK_VERSION') }}"
tasks:
- name: 切换版本
ansible.builtin.file:
src: "{{ deploy_path }}/{{ rollback_version }}"
dest: "{{ deploy_path }}/current"
state: link
force: yes
- name: 重载Nginx
ansible.builtin.systemd:
name: nginx
state: reloaded
- name: 验证回滚
ansible.builtin.uri:
url: "http://127.0.0.1/health"
status_code: 200
数据库运维Playbook
# playbooks/manage_mysql.yml
---
- name: MySQL集群维护
hosts: middleware
become: yes
serial: 1 # 数据库必须一台一台来,防止主从延迟
pre_tasks:
- name: 检查MySQL状态
ansible.builtin.systemd:
name: mysqld
register: mysql_status
- name: 检查主从状态
ansible.builtin.shell: |
mysql -e "SHOW SLAVE STATUS\\G" | grep -E "Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master"
register: slave_status
delegate_to: "{{ groups['middleware'][0] }}" # 只在主库执行
run_once: true
- name: 显示主从状态
ansible.builtin.debug:
var: slave_status.stdout
tasks:
- name: 执行MySQL优化
ansible.builtin.shell: |
mysql -e "
SET GLOBAL innodb_buffer_pool_size = {{ mysql_buffer_pool_size | default('1G') }};
SET GLOBAL max_connections = {{ mysql_max_connections | default(500) }};
FLUSH PRIVILEGES;
"
delegate_to: "{{ groups['middleware'][0] }}"
run_once: true
when: mysql_status.status.Active == 'yes'
- name: 检查慢查询
ansible.builtin.shell: |
mysql -e "SHOW GLOBAL STATUS LIKE 'Slow_queries'"
delegate_to: "{{ groups['middleware'][0] }}"
run_once: true
- name: 备份数据库
ansible.builtin.shell: |
mysqldump -h 127.0.0.1 -u root --all-databases > /backup/mysql/{{ inventory_hostname }}_{{ ansible_date_time.date }}.sql
when: ansible_hostname == 'mysql-master'
post_tasks:
- name: 再次检查主从状态
ansible.builtin.shell: |
mysql -e "SHOW SLAVE STATUS\\G" | grep -E "Slave_IO_Running|Slave_SQL_Running|Seconds_Behind_Master"
register: slave_status_after
delegate_to: "{{ groups['middleware'][0] }}"
run_once: true
- name: 显示维护结果
ansible.builtin.debug:
msg: "MySQL维护完成,主从延迟: {{ slave_status_after.stdout }}"
这里serial: 1很关键——数据库操作必须串行执行,否则可能导致主从数据不一致。
秘密管理
运维过程中会涉及很多敏感信息:数据库密码、API密钥、SSH私钥等。Ansible提供了vault机制来保护这些秘密。
# 创建加密文件
ansible-vault create inventory/group_vars/secrets.yml
# 这会打开一个编辑器,输入你的秘密
---
mysql_root_password: 'S3cur3P@ssw0rd!'
redis_password: 'R3d1sP@ss!'
app_secret_key: 'aabbccdd11223344'
github_deploy_key: |
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
然后在playbook中引用:
# playbooks/deploy_mysql.yml
---
- name: MySQL部署
hosts: middleware
become: yes
vars_files:
- group_vars/secrets.yml
tasks:
- name: 配置MySQL
ansible.builtin.template:
src: my.cnf.j2
dest: /etc/my.cnf
vars:
mysql_root_password: "{{ mysql_root_password }}"
执行时需要提供vault密码:
# 方式一:交互式输入
ansible-playbook playbooks/deploy_mysql.yml --ask-vault-pass
# 方式二:指定密码文件
echo "MyVaultPassword" > /opt/ansible/vault_password
ansible-playbook playbooks/deploy_mysql.yml --vault-password-file /opt/ansible/vault_password
# 方式三:环境变量(CI/CD场景)
export ANSIBLE_VAULT_PASSWORD_FILE=/opt/ansible/vault_password
ansible-playbook playbooks/deploy_mysql.yml
在CI/CD流水线中,我们会把vault密码存在Kubernetes Secret或者HashiCorp Vault中,由流水线动态获取。
日志和监控
Ansible日志配置
我们配置了详细的日志记录,便于问题排查:
# ansible.cfg
log_path = /opt/ansible/logs/ansible.log
log_format = %%(asctime)s %%(levelname)-8s %%(message)s
日志文件按日期分割:
# crontab - 每天凌晨切割日志
0 0 * * * mv /opt/ansible/logs/ansible.log /opt/ansible/logs/ansible_$(date +\%Y\%m\%d).log
执行结果分析
我们编写了一个简单的报告生成脚本:
#!/usr/bin/env python3
# scripts/analyze_results.py
import json
import sys
from datetime import datetime
from collections import defaultdict
def analyze_playbook_output(output_file):
"""分析Ansible playbook执行结果"""
results = {
'total_hosts': 0,
'ok': 0,
'changed': 0,
'failed': 0,
'unreachable': 0,
'skipped': 0,
'failed_hosts': [],
'changed_hosts': []
}
with open(output_file, 'r') as f:
for line in f:
if 'PLAY RECAP' in line:
continue
if 'ok=' in line and 'changed=' in line:
parts = line.strip().split()
host = parts[0]
results['total_hosts'] += 1
for part in parts[1:]:
if 'ok=' in part:
results['ok'] += int(part.split('=')[1])
elif 'changed=' in part:
results['changed'] += int(part.split('=')[1])
results['changed_hosts'].append(host)
elif 'failed=' in part:
results['failed'] += int(part.split('=')[1])
results['failed_hosts'].append(host)
elif 'unreachable=' in part:
results['unreachable'] += int(part.split('=')[1])
elif 'skipped=' in part:
results['skipped'] += int(part.split('=')[1])
return results
if __name__ == '__main__':
output_file = sys.argv[1] if len(sys.argv) > 1 else 'playbook_output.log'
results = analyze_playbook_output(output_file)
print("=" * 60)
print("Ansible执行报告")
print("=" * 60)
print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"总主机数: {results['total_hosts']}")
print(f"成功: {results['ok']}")
print(f"变更: {results['changed']} ({len(results['changed_hosts'])}台)")
print(f"失败: {results['failed']} ({len(results['failed_hosts'])}台)")
print(f" unreachable: {results['unreachable']}")
if results['failed_hosts']:
print("\n失败主机列表:")
for host in results['failed_hosts']:
print(f" - {host}")
CI/CD集成
我们把Ansible集成到了GitLab CI/CD流水线中:
# .gitlab-ci.yml
stages:
- test
- deploy
- verify
variables:
ANSIBLE_HOST_KEY_CHECKING: "False"
ANSIBLE_REMOTE_USER: "ops"
ANSIBLE_PRIVATE_KEY_FILE: "$SSH_PRIVATE_KEY"
test:
stage: test
script:
- ansible-playbook playbooks/health_check.yml --syntax-check
- ansible-lint playbooks/
only:
- branches
- merges
deploy_web:
stage: deploy
script:
- export APP_VERSION=$CI_COMMIT_SHORT_SHA
- ansible-playbook playbooks/deploy_web_app.yml \
--limit web_servers \
--vault-password-file /run/secrets/vault_password
environment:
name: production
only:
- tags
deploy_microservice:
stage: deploy
script:
- ansible-playbook playbooks/deploy_app.yml \
--limit microservices \
--vault-password-file /run/secrets/vault_password
environment:
name: production
only:
- tags
verify:
stage: verify
script:
- ansible all -m ping
- ansible-playbook playbooks/health_check.yml
only:
- tags
实际运行效果
平台上线后,我们的运维效率发生了质的变化:
| 指标 | 改造前 | 改造后 | 提升 |
|---|---|---|---|
| 批量部署500台耗时 | 4-6小时 | 15-20分钟 | 15倍 |
| 配置变更一致性 | 人工检查 | 自动化校验 | 100% |
| 故障恢复时间 | 30-60分钟 | 5-10分钟 | 6倍 |
| 人为操作失误 | 每月2-3次 | 几乎为0 | - |
| 运维人员数量 | 3人 | 2人 | - |
当然,这不是说运维可以躺平了。平台上线后我们反而需要花更多时间在优化playbook、扩展角色、完善监控上。自动化运维不是减少工作,而是把重复性工作交给机器,让人去做更有价值的事情。
踩过的坑和经验
坑一:SSH连接超时
刚开始时,500台机器同时执行任务,大量SSH连接导致控制节点资源耗尽。解决方式:
- 增加
forks参数到100 - 启用SSH ControlMaster连接复用
- 在目标机器上优化SSH配置,增加
MaxSessions
# 目标机器ssh_config优化
MaxSessions 20
MaxStartups 100:30:200
坑二:幂等性问题
有一个playbook在循环中执行yum install,结果每次执行都显示changed,因为Yum会重新检查依赖。解决方案是使用state: present而不是latest,并且加上update_cache: yes只在必要时更新缓存。
坑三:大文件传输
有次需要分发一个2GB的日志分析脚本,直接用copy模块导致内存溢出。改用archive模块压缩后再传输,或者直接用rsync通过shell模块执行。
- name: 分发大文件
ansible.builtin.shell: |
rsync -az --progress
/opt/ansible/files/big_script.sh
{{ item }}:/tmp/big_script.sh
loop: "{{ groups['web_servers'] }}"
坑四:变量覆盖
变量层级太多导致意外覆盖。我们制定了严格的命名规范:
- 角色变量:
role_前缀 - 主机变量:
host_前缀 - 全局变量:小写无下划线
- 敏感变量:统一放在vault中
坑五:并行执行的副作用
某些操作不适合并行执行,比如数据库变更、配置中心更新等。我们制定了规则:
- 数据库操作:
serial: 1 - 中间件配置:
serial: "25%" - 应用部署:
serial: "10%" - 系统初始化:
serial: "20%"
给新手的建议
如果你正准备搭建类似的Ansible平台,我有几条建议:
从小规模开始:先管理10-20台机器,跑通流程后再扩展。不要一开始就挑战500台。
先写health check:在写任何复杂playbook之前,先写一个健康检查的playbook,确保你能连上所有机器。
版本控制一切:把inventory、roles、playbooks、vars都放到Git仓库里,每次变更都有迹可查。
测试先行:先在小范围测试机上验证playbook,确认无误后再应用到生产环境。
文档跟上:每个role都要有README,说明用途、变量、依赖。半年后你自己都看不懂自己写的东西。
不要怕犯错:我们一开始也踩过很多坑,关键是建立回滚机制,错了能快速恢复。
培养团队能力:自动化运维不是一个人的事,要让团队成员都能写playbook,这样才能持续维护。
未来规划
目前平台运行了半年多,稳定可靠。接下来的改进方向:
- 引入Ansible AWX:作为Web管理界面,支持审批流程、定时任务、权限管理
- 集成监控告警:playbook执行结果自动推送到监控系统
- 混沌工程:定期随机抽一台机器执行异常操作,验证平台的容错能力
- 多云支持:扩展支持阿里云、腾讯云等云厂商的API操作
- AI辅助:探索用AI生成playbook、分析日志、预测问题
自动化运维这条路,越往前走越有意思。500台机器听起来很多,但当你的playbook能一键搞定时,那种成就感是难以言表的。希望这篇记录能帮到正在路上的你,如果遇到问题,欢迎一起交流。