第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 模板引擎,用于动态生成配置文件

Ansible 架构 - 图示

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配合,配置变更可追溯
预检查机制 部署前验证端口状态、路由连通性等前置条件

思考

  1. Ansible的vrp_config模块使用CLI模式下发配置,与NETCONF模式相比各有什么优缺点?在什么场景下应该选择NETCONF模式?
  2. 如果需要在Ansible部署过程中对配置进行"灰度发布"(先部署部分设备验证,再批量推广),如何在Playbook中实现?
  3. Ansible Vault可以加密敏感信息,但在大规模网络中管理大量设备的凭据,是否有更好的凭据管理方案?

下篇预告:第283篇:ZTP(零接触部署)在数据中心的实践