第282篇:数据中心网络自动化:Ansible 批量配置
关键词:Ansible、网络自动化、批量配置、Playbook、网络模块、Idempotency、Jinja2
一、引言
在数据中心网络运维中,手动逐台配置交换机不仅效率低下,还容易引入人为错误。Ansible作为最流行的IT自动化引擎,凭借其Agentless架构和声明式配置模型,成为网络设备批量配置的首选工具之一。
Ansible通过SSH连接网络设备,利用其丰富的网络模块(如ios、eos、vrp、nxos等)实现配置下发、验证和回滚,大幅提升了数据中心网络的运维效率和可靠性。
二、Ansible 网络自动化核心概念
2.1 Ansible 架构
| 组件 | 说明 |
|---|---|
| Control Node | 安装Ansible的控制机,执行Playbook |
| Managed Node | 被管理的网络设备(交换机、路由器等) |
| Inventory | 主机清单,定义被管理设备的IP和分组 |
| Playbook | YAML格式的任务编排文件 |
| Module | 执行具体操作的模块(如vrp_config) |
| Jinja2 Template | 模板引擎,用于动态生成配置文件 |

2.2 Ansible 网络模块对比
| 模块类型 | 模块名 | 传输方式 | 厂商 | 特点 |
|---|---|---|---|---|
| VRP | vrp_config |
CLI over SSH | 华为 | 基于CLI,简单直接 |
| VRP | vrp_netconf |
NETCONF over SSH | 华为 | 基于NETCONF,结构化 |
| IOS | ios_config |
CLI over SSH | 思科IOS | 思科低端设备 |
| NX-OS | nxos_config |
CLI over SSH | 思科NX-OS | 思科数据中心 |
| EOS | eos_config |
CLI over SSH/NETCONF | Arista | Arista交换机 |
| F5 | bigip_* |
REST API | F5 | F5负载均衡器 |
2.3 Ansible 执行模式
| 模式 | 说明 | 使用场景 |
|---|---|---|
| Ad-Hoc | 单条命令快速执行 | 临时巡检、快速查询 |
| Playbook | YAML编排文件 | 标准化批量部署 |
| Role | 可复用的角色封装 | 跨项目复用配置模板 |
| Collection | Ansible Galaxy集合 | 社区/厂商共享模块集 |
三、Ansible 网络配置实战
3.1 项目目录结构
ansible-dc/
inventory/
hosts.ini # 主机清单
group_vars/
spine.yml # Spine变量
leaf.yml # Leaf变量
group_vars/
all.yml # 全局变量
spine.yml
leaf.yml
host_vars/
dc1-spine-01.yml # 设备特定变量
dc1-spine-02.yml
playbooks/
01_base_config.yml # 基础配置
02_bgp_evpn.yml # BGP EVPN配置
03_vxlan.yml # VXLAN配置
04_verify.yml # 配置验证
roles/
ce_switch/
tasks/
main.yml
templates/
ce_base.conf.j2
defaults/
main.yml
bgp_evpn/
tasks/
templates/
ansible.cfg
requirements.yml # Galaxy依赖
3.2 Inventory 配置
hosts.ini:
[spine]
dc1-spine-01 ansible_host=10.255.0.1 ansible_user=admin ansible_password=Admin@123
dc1-spine-02 ansible_host=10.255.0.2 ansible_user=admin ansible_password=Admin@123
[leaf]
dc1-leaf-01 ansible_host=10.255.1.1 ansible_user=admin ansible_password=Admin@123
dc1-leaf-02 ansible_host=10.255.1.2 ansible_user=admin ansible_password=Admin@123
dc1-leaf-03 ansible_host=10.255.1.3 ansible_user=admin ansible_password=Admin@123
[fabric:children]
spine
leaf
[all:vars]
ansible_network_os=vrp
ansible_connection=network_cli
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
3.3 全局变量配置
group_vars/all.yml:
---
# 全局变量
fabric_asn: 65001
mgmt_vlan: 4094
mgmt_subnet: "10.255.255.0/24"
ntp_server: 10.255.255.1
snmp_community: "Huawei@Net"
snmp_trap_host: 10.255.255.100
log_host: 10.255.255.101
group_vars/spine.yml:
---
# Spine 特定变量
role: spine
vtep_interface: LoopBack
vtep_interface_id: 1
bgp_peer_range:
- 10.255.1.1
- 10.255.1.2
- 10.255.1.3
uplink_interfaces:
- 100GE1/0/25
- 100GE1/0/26
- 100GE1/0/27
- 100GE1/0/28
downlink_interfaces:
- 100GE1/0/1
- 100GE1/0/2
- 100GE1/0/3
- 100GE1/0/4
vni_list:
- 10017
- 10018
- 10019
group_vars/leaf.yml:
---
# Leaf 特定变量
role: leaf
vtep_interface: LoopBack
vtep_interface_id: 1
bgp_peer_range:
- 10.255.0.1
- 10.255.0.2
server_vlan_list:
- { vlan_id: 100, vlan_name: "Server_Net_100", vni: 10017 }
- { vlan_id: 101, vlan_name: "Server_Net_101", vni: 10018 }
uplink_interfaces:
- 100GE1/0/1
- 100GE1/0/2
- 100GE1/0/3
- 100GE1/0/4
server_interfaces:
- 25GE1/0/49
- 25GE1/0/50
- 25GE1/0/51
- 25GE1/0/52
3.4 设备特定变量
host_vars/dc1-spine-01.yml:
---
hostname: DC1-SPINE-01
vtep_ip: 10.255.0.1
loopback_ip: 10.255.0.1
mgmt_ip: 10.255.255.1
uplink_port_channel_id: 10
downlink_port_channel_id: 20
3.5 基础配置 Playbook
playbooks/01_base_config.yml:
---
- name: Configure CE Switches - Base Configuration
hosts: fabric
gather_facts: no
connection: network_cli
tasks:
- name: Set hostname
ansible.utils.vrrp:
hostname: "{{ hostname }}"
- name: Configure management interface
ansible.builtin.template:
src: templates/ce_base.conf.j2
dest: /dev/null
delegate_to: localhost
- name: Apply base configuration
ansible.netcommon.config:
src: templates/ce_base.conf.j2
replace: block
backup: yes
register: base_config_result
- name: Display configuration result
ansible.builtin.debug:
msg: "Base config applied to {{ inventory_hostname }}"
when: base_config_result.changed
- name: Save configuration
ansible.netcommon.cmd_run:
command: save force
when: base_config_result.changed
3.6 Jinja2 模板
templates/ce_base.conf.j2:
# ===== Auto-generated by Ansible =====
# Device: {{ hostname }}
# Role: {{ role }}
sysname {{ hostname }}
# --- NTP Configuration ---
ntp server {{ ntp_server }} version 4 preferred
# --- SNMP Configuration ---
snmp-agent
snmp-agent community read cipher {{ snmp_community }}
snmp-agent sys-info version all
snmp-agent target-host trap address udp-domain {{ snmp_trap_host }} \
params securityname public
# --- Logging Configuration ---
info-center loghost 10.255.255.101 source {{ vtep_ip }}
# --- VTEP Source Interface ---
interface {{ vtep_interface }} {{ vtep_interface_id }}
description VTEP_Source_IP
ip address {{ vtep_ip }} 255.255.255.255
# --- Management Interface ---
interface MEth 0/0/0
description Management_IP
ip address {{ mgmt_ip }} 255.255.255.0
dhcp select global
# --- Default Route ---
ip route-static 0.0.0.0 0.0.0.0 10.255.255.254
# --- SSH Configuration ---
stelnet server enable
netconf ssh server enable
ssh user admin authentication-type password
ssh user admin service-type netconf terminal stm
{% if role == "spine" %}
# --- Spine Uplink Interfaces ---
{% for iface in uplink_interfaces %}
interface {{ iface }}
description Uplink_to_Core
port link-mode bridge
{% endfor %}
# --- Spine Downlink Interfaces ---
{% for iface in downlink_interfaces %}
interface {{ iface }}
description Downlink_to_Leaf
port link-mode bridge
{% endfor %}
{% elif role == "leaf" %}
# --- Leaf Uplink Interfaces ---
{% for iface in uplink_interfaces %}
interface {{ iface }}
description Uplink_to_Spine
port link-mode bridge
{% endfor %}
# --- Leaf Server Interfaces ---
{% for iface in server_interfaces %}
interface {{ iface }}
description Server_Connection
port link-mode bridge
port default vlan-type
{% endfor %}
{% endif %}
# ===== End of Auto-generated Config =====
3.7 BGP EVPN 配置 Playbook
playbooks/02_bgp_evpn.yml:
---
- name: Configure BGP EVPN on Fabric
hosts: fabric
gather_facts: no
connection: network_cli
tasks:
- name: Configure BGP base
ansible.netcommon.config:
lines:
- "bgp {{ fabric_asn }}"
- " router-id {{ vtep_ip }}"
- " default-route-advertise"
- " quit"
parents: ""
backup: yes
register: bgp_base_result
- name: Configure BGP EVPN peers
ansible.netcommon.config:
lines: |
- "bgp {{ fabric_asn }}"
{% for peer in bgp_peer_range %}
- " peer {{ peer }} as-number {{ fabric_asn }}"
- " peer {{ peer }} evpn enable"
- " peer {{ peer }} connect-interface {{ vtep_interface }} {{ vtep_interface_id }}"
- " peer {{ peer }} bfd enable"
{% endfor %}
- " quit"
backup: yes
register: bgp_peer_result
- name: Configure VXLAN NVE
ansible.netcommon.config:
lines: |
- "interface Nve 1"
- " source-interface {{ vtep_interface }} {{ vtep_interface_id }}"
{% if role == "spine" %}
{% for vni in vni_list %}
- " vxlan vni {{ vni }} mac-vtep routing"
- " vxlan vni {{ vni }} destination-ipep dynamic"
{% endfor %}
{% elif role == "leaf" %}
{% for vlan in server_vlan_list %}
- " vxlan vni {{ vlan.vni }} mac-vtep routing"
- " vxlan vni {{ vlan.vni }} destination-ipep dynamic"
- " vxlan vni {{ vlan.vni }} binding evpn"
{% endfor %}
{% endif %}
backup: yes
register: nve_result
- name: Verify BGP peers
ansible.netcommon.cmd_run:
command: display bgp peer all | include Peer|State
register: bgp_verify
- name: Show BGP peer status
ansible.builtin.debug:
var: bgp_verify.stdout_lines
3.8 配置验证 Playbook
playbooks/04_verify.yml:
---
- name: Verify Fabric Configuration
hosts: fabric
gather_facts: no
connection: network_cli
tasks:
- name: Verify BGP EVPN sessions
ansible.netcommon.cmd_run:
command: display bgp evpn peer
register: bgp_evpn_peer
- name: Verify VXLAN sessions
ansible.netcommon.cmd_run:
command: display vxlan vtep session
register: vxlan_session
- name: Verify interface status
ansible.netcommon.cmd_run:
command: display interface brief
register: interface_brief
- name: Collect verification report
ansible.builtin.copy:
content: |
=== Verification Report: {{ inventory_hostname }} ===
Time: {{ ansible_date_time.iso8601 }}
--- BGP EVPN Peers ---
{{ bgp_evpn_peer.stdout }}
--- VXLAN Sessions ---
{{ vxlan_session.stdout }}
--- Interface Status ---
{{ interface_brief.stdout }}
dest: "/tmp/verify_{{ inventory_hostname }}.txt"
delegate_to: localhost
四、实战案例:批量部署与故障处理
4.1 完整Fabric部署流程
#!/bin/bash
# deploy_fabric.sh - 一键部署Fabric
set -e
echo "===== Step 1: 部署基础配置 ====="
ansible-playbook -i inventory/hosts.ini playbooks/01_base_config.yml \
--limit spine -v
echo "===== Step 2: 部署Leaf基础配置 ====="
ansible-playbook -i inventory/hosts.ini playbooks/01_base_config.yml \
--limit leaf -v
echo "===== Step 3: 部署BGP EVPN ====="
ansible-playbook -i inventory/hosts.ini playbooks/02_bgp_evpn.yml \
-v
echo "===== Step 4: 验证配置 ====="
ansible-playbook -i inventory/hosts.ini playbooks/04_verify.yml \
-v
echo "===== 部署完成,查看验证报告 ====="
cat /tmp/verify_*.txt
4.2 故障案例:Ansible 配置下发后设备状态异常
现象:使用Ansible批量部署BGP EVPN配置后,部分Leaf交换机BGP会话未Up,VXLAN隧道不通。
排查过程:
# 1. 检查Ansible执行日志
cat ansible.log | grep FAILED
# dc1-leaf-02: FAILED - "Error: BGP peer 10.255.0.1 configuration failed"
# 2. 检查设备上的配置
ansible dc1-leaf-02 -i inventory/hosts.ini -m vrp_command \
-a "commands='display bgp peer'"
# BGP peer存在但状态为Idle(Connect)
# 原因:Leaf到Spine的IBGP路由不通
# 3. 排查路由
ansible dc1-leaf-02 -i inventory/hosts.ini -m vrp_command \
-a "commands='display ip routing-table 10.255.0.1'"
# 无路由可达10.255.0.1
# 4. 检查底层连接
ansible dc1-leaf-02 -i inventory/hosts.ini -m vrp_command \
-a "commands='display interface brief'"
# 发现Uplink接口的对端Spine端口未启用
# Spine-02的100GE1/0/1端口处于shutdown状态
# 5. 修复Spine端口
ansible dc1-spine-02 -i inventory/hosts.ini -m vrp_config \
-a "lines='interface 100GE1/0/1; undo shutdown'"
# 6. 重新验证
ansible fabric -i inventory/hosts.ini -m vrp_command \
-a "commands='display bgp peer all | include Peer|State'"
根因:Spine-02的downlink端口处于shutdown状态,导致Leaf-02到Spine-02的IBGP路由不可达,BGP EVPN会话无法建立。
解决:
1. 在Ansible基础配置Playbook中添加端口状态验证任务
2. 使用vrp_interface模块确保端口处于up状态
3. 在部署Playbook前增加预检查(Pre-check)步骤
预检查 Playbook 示例:
---
- name: Pre-check before deployment
hosts: fabric
gather_facts: no
connection: network_cli
tasks:
- name: Check interface status
ansible.netcommon.cmd_run:
command: display interface brief
register: interface_status
- name: Fail if any uplink is down
ansible.builtin.fail:
msg: "Uplink interface is down on {{ inventory_hostname }}"
when: "'DOWN' in item"
loop: "{{ interface_status.stdout_lines }}"
- name: Check SSH connectivity
ansible.netcommon.cmd_run:
command: display ssh server
register: ssh_status
- name: Fail if SSH server is not enabled
ansible.builtin.fail:
msg: "SSH server is not enabled on {{ inventory_hostname }}"
when: "'ssh server enable' not in ssh_status.stdout"
4.3 故障案例:Jinja2 模板变量缺失导致配置错误
现象:Ansible批量部署时,部分设备配置中出现变量名未替换的字面值(如{{ undefined_var }})。
排查过程:
# 1. 检查Ansible执行输出
ansible-playbook playbooks/01_base_config.yml -v
# WARNING: Template variable 'router_id' is undefined
# 2. 检查变量来源
# 在host_vars中,部分设备未定义router_id变量
# spine使用了vtep_ip作为router-id,但leaf使用了不同的变量名
# 3. 修复方案
# 方案A:在所有host_vars中添加缺失变量
# 方案B:在defaults/main.yml中设置默认值
# 方案C:在模板中使用default过滤器
# 采用方案C(最安全)
# 修改模板:
# ip address {{ vtep_ip | default('10.255.0.0') }} 255.255.255.255
# 4. 增加模板语法检查
ansible-playbook playbooks/01_base_config.yml --check --diff
# --check: 干跑模式,不实际执行
# --diff: 显示配置差异
五、总结
| 能力 | Ansible 实现方式 | 效果 |
|---|---|---|
| 批量配置下发 | Playbook + Jinja2模板 | 分钟级完成数十台设备配置 |
| 配置一致性 | Idempotent模块设计 | 重复执行不会产生差异 |
| 配置验证 | Verify Playbook | 自动收集并对比配置状态 |
| 变更回滚 | backup=yes + 版本控制 | 一键回退到之前版本 |
| 权限管控 | Ansible Vault加密 | 敏感信息不暴露 |
| 最佳实践 | 说明 |
|---|---|
| 幂等性优先 | 使用config模块而非command,确保可重复执行 |
| 变更前置验证 | 部署前运行--check --diff干跑模式 |
| 变量分层管理 | 全局→组→设备,变量优先级清晰 |
| 配置版本管理 | Ansible仓库与Git配合,配置变更可追溯 |
| 预检查机制 | 部署前验证端口状态、路由连通性等前置条件 |
思考
- Ansible的
vrp_config模块使用CLI模式下发配置,与NETCONF模式相比各有什么优缺点?在什么场景下应该选择NETCONF模式? - 如果需要在Ansible部署过程中对配置进行"灰度发布"(先部署部分设备验证,再批量推广),如何在Playbook中实现?
- Ansible Vault可以加密敏感信息,但在大规模网络中管理大量设备的凭据,是否有更好的凭据管理方案?
下篇预告:第283篇:ZTP(零接触部署)在数据中心的实践