📖 导读

在上一篇文章中,我们聊了如何用 Containerlab 搭建 EVPN 拓扑。但作为追求极致效率的工程师,手动进入每个容器敲 CLI 显然不够优雅。今天,我们直接上硬核干货:如何用 Ansible 实现 Arista cEOS 与 FRR 的全自动化 EVPN 部署。

目录

🛠 一、 基座搭建:FRR 镜像二次定制

🛠 二、 环境底座:Ansible 安装与必备插件配置

🔑 三、 通道优化:SSH 免密与安全规避

📊 四、 自动化架构:逻辑与数据分离

五、 进阶思考:利用 Jinja2 模板合二为一

🚀 六、 披荆斩棘:Playbook 执行与成功日志全解析

🏗 七、 Ansible Deploy 全流程总结(大牛思维导图)

🎁 结语



🛠 一、 基座搭建:FRR 镜像二次定制

Ansible 依赖 SSH,而官方 FRR 镜像默认不带 SSH 服务。我们需要先进行镜像“魔改”。

1. 容器内安装 OpenSSH

docker exec -it clab-evpn-week2-spine1 bash

# 容器内执行
apt update && apt install openssh-server -y
echo 'root:root' | chpasswd
sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
service ssh start

2. 保存新镜像

docker commit clab-evpn-week2-spine1 my-frr-ssh:v1

3. 应用新镜像在YAML节点配置文件中

    spine1: 
      kind: linux
      # 应用新镜像
      image: my-frr-ssh:v1
      # 提前bind有使能bgp的daemon文件 
      binds: ["./clab-evpn-week2/spine1:/etc/frr"]
      # 关键:启动后立即执行运行ssh服务
      exec:
        - /usr/sbin/sshd

🛠 二、 环境底座:Ansible 安装与必备插件配置

在开始编写 Playbook 之前,我们需要确保宿主机(管理机)具备与 Arista 和 Linux 容器对话的能力。

1. 安装 Ansible 核心

建议在 Ubuntu/Debian 环境下安装,它可以完美支持我们的实验环境。

# 更新源并安装 
sudo apt update
sudo apt install software-properties-common -y
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible -y

# 验证安装结果
ansible --version

2. 安装高性能 SSH 插件:ansible-pylibssh

这是本次实验的关键。ansible-pylibssh 提供了更快的执行速度和更好的安全特性,是目前 Arista 等厂商推荐的连接后端。

# 安装编译所需的依赖库
sudo apt install python3-pip libssh-dev -y

# 通过 pip 安装高性能插件
pip3 install ansible-pylibssh

3. 安装网络自动化必备 Collection (插件)

Ansible 的核心功能是轻量化的,针对 Arista (EOS) 的专用指令集需要单独安装:

# 安装 Arista EOS 官方集合
ansible-galaxy collection install arista.eos

# 安装通用网络基础集合(提供 IP 地址解析等逻辑)
ansible-galaxy collection install ansible.netcommon

🔑 三、 通道优化:SSH 免密与安全规避

在生产环境中,Ansible 频繁交互时,手动输入密码不仅效率低下,且极易触发 SSH 会话限制。实现“零手工干预”登录是所有网络自动化的前置条件。

核心原理:公钥认证 (Public Key Authentication)

免密登录并非“没有密码”,而是通过非对称加密算法。我们将 Ansible 管理主机的 公钥 (Public Key) 植入到目标设备的“信任名单”中,从而在握手阶段完成身份校验。

  • 在管理主机 Ubuntu 生成密钥对

ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa
  • 在项目根目录下写一个超简单的 Bash 脚本,执行脚本一键把公钥推送到所有 4 台设备

#!/bin/bash

# 定义节点名称
NODES="clab-evpn-week2-leaf1 clab-evpn-week2-leaf2 clab-evpn-week2-spine1 clab-evpn-week2-spine2"

for node in $NODES; do
    echo "--- Configuring $node ---"
    
    # 1. 配置 root 账号 (通用)
    docker exec $node mkdir -p /root/.ssh
    docker cp ~/.ssh/id_rsa.pub $node:/root/.ssh/authorized_keys
    docker exec $node chmod 700 /root/.ssh
    docker exec $node chmod 600 /root/.ssh/authorized_keys
    docker exec $node chown root:root /root/.ssh/authorized_keys

    # 2. 针对 Arista Leaf 节点配置 admin 账号
    if [[ $node == *"leaf"* ]]; then
        echo "Adding SSH key for admin user on $node..."
        # Arista 的 admin 家目录
        ADMIN_HOME="/home/admin"
        
        docker exec $node mkdir -p $ADMIN_HOME/.ssh
        docker cp ~/.ssh/id_rsa.pub $node:$ADMIN_HOME/.ssh/authorized_keys
        
        # 必须确保 admin 用户拥有这些文件的所有权
        docker exec $node chown -R admin:admin $ADMIN_HOME/.ssh
        docker exec $node chmod 700 $ADMIN_HOME/.ssh
        docker exec $node chmod 600 $ADMIN_HOME/.ssh/authorized_keys
    fi
done

echo "Done! You can now try: ssh admin@<leaf_ip>"
  • 优化 ansible.cfg:在项目根目录(我的目录是~/labs/evpn-week2/ )创建此文件,Ansible会自动读取它,彻底避免 unknown_host 报错。在文件中写入:
[defaults]
host_key_checking = False  # 禁用 SSH 指纹检查

📊 四、 自动化架构:逻辑与数据分离

这是本文的核心。我们将配置拆分为:逻辑(Playbook)资产(Inventory)数据(host_vars)

1. 资产表 (Inventory)

在传统 Ansible 实验中,最痛苦的就是手动维护 inventory.yml

  1. 容器重启或重新部署,管理 IP 可能会变。

  2. 设备多了,手动输入 ansible_host 极易出错。

我的实战方案: Containerlab 在每次执行 deploy 后,会自动在 clab-<lab-name> 目录下生成一个完美的 ansible-inventory.yml

为什么它是“神来之笔”?

  • 实时同步:Containerlab 最清楚它给每个容器分配了什么 IP。这个IP地址在每一次deploy时都可能会变化,只有Containerlab最清楚这次是如何分配的。

  • 内置分组:它会根据你在拓扑文件中定义的 kind(如 ceos, linux)自动把设备归类。

  • 属性完备:它甚至帮你写好了 ansible_connection: dockeransible_host,省去了配置 SSH 代理的初始麻烦。

all:
  vars:
    # The generated inventory is assumed to be used from the clab host.
    # Hence no http proxy should be used. Therefore we make sure the http
    # module does not attempt using any global http proxy.
    ansible_httpapi_use_proxy: false
  children:
    arista_ceos:
      vars:
        # ansible_connection: set ansible_connection variable if required
        ansible_user: admin
        ansible_password: admin
      hosts:
        clab-evpn-week2-leaf1:
          ansible_host: 172.20.20.3
        clab-evpn-week2-leaf2:
          ansible_host: 172.20.20.5
    linux:
      hosts:
        clab-evpn-week2-pc1:
          ansible_host: 172.20.20.4
        clab-evpn-week2-pc2:
          ansible_host: 172.20.20.6
        clab-evpn-week2-spine1:
          ansible_host: 172.20.20.7
        clab-evpn-week2-spine2:
          ansible_host: 172.20.20.2

2. 数据模型 (host_vars/leaf1.yml)

将每台设备的差异化参数提取出来,这就是 Infrastructure as Code (IaC)。在项目总目录下创建host_vars文件夹,为每一台设备创建他们的参数文档:

Leaf-1 参数文档举例:

# 基础信息
hostname: leaf1
loopback_ip: 2.2.2.1
loopback_mask: 32
bgp_as: 65001

# 物理接口配置 (Underlay P2P)
l3_interfaces:
  - name: Ethernet1
    ip: 10.0.1.2/30
    description: "TO_SPINE1"
  - name: Ethernet2
    ip: 10.0.2.2/30
    description: "TO_SPINE2"

# 业务 VLAN 与 PC 连接 (Access)
access_vlan: 10
vni_id: 10010
pc_interface: Ethernet3

# BGP 邻居配置 (Underlay & Overlay)
bgp_neighbors:
  - ip: 10.0.1.1
    remote_as: 65000
  - ip: 10.0.2.1
    remote_as: 65000

Spine-1 参数文档举例

hostname: spine1
loopback_ip: 1.1.1.1/32
# 为后续 Underlay BGP 做准备
bgp_as: 65000
router_id: 1.1.1.1
# 物理接口与邻居数据
p2p_links:
  - interface: eth1
    ip: 10.0.1.1/30
    desc: "to-leaf1"
    neighbor_ip: 10.0.1.2
    remote_as: 65001
    peer_name: "LEAF1"
  - interface: eth2
    ip: 10.0.3.1/30
    desc: "to-leaf2"
    neighbor_ip: 10.0.3.2
    remote_as: 65002
    peer_name: "LEAF2"

3. 逻辑 (Playbook):

在真实的生产环境中,网络设备往往是多厂商混部的(Multi-vendor)。在本次拓扑中,Leaf 使用 Arista cEOS,Spine 使用 FRR,这种异构性决定了我们必须采用不同的处理策略。

操作系统底层的本质差异
  • Arista (cEOS):属于网络操作系统 (NOS)。Ansible 与其交互是通过 network_cli 连接,利用专用的资源模块(如 eos_config)来操作其特定的 CLI 命令行。

  • FRR (Linux):本质上是跑在 Linux 上的应用程序。Ansible 与其交互通常采用标准的 ssh 连接,利用 shell 模块调用 vtysh 命令来修改配置。

状态管理逻辑的不同

  • Arista 拥有成熟的配置检查机制(如 check_mode),可以感知配置是否发生变化(Changed)。

  • FRR 在容器中通常需要通过 shell 脚本强行下发,状态反馈不如原生网络模块细腻。

命令语法的细微差别

即使同为 BGP 配置,Arista 倾向于 router bgp 下的层级缩进,而 FRR 在 vtysh 下对 exit-address-family 等指令的依赖程度和报错处理逻辑也各不相同。

Leaf 侧:极致兼容的 eos_config

由于 Resource Modules(如 eos_bgp)在不同 EOS 版本间存在参数差异,对于大多数协议模块我们选择了最高效、最透明的 eos_config 做法:

Leaf 配置脚本 (deploy_leaf.yml)

---
- name: "Leaf节点全配置"
  hosts: arista_ceos  # 匹配 Inventory 中的组名
  gather_facts: no
  # 关键点 1:指定连接方式为 network_cli
  connection: ansible.netcommon.network_cli
  vars:
    ansible_user: admin
    # 关键点 2:指定网络操作系统,帮助 Ansible 选择正确的驱动
    ansible_network_os: arista.eos.eos
    # --- 关键提权配置 ---
    ansible_become: yes
    ansible_become_method: enable
    # 如果你的 admin 用户没有密码,下面这行可以不写;
    # 如果有 enable 密码,则需要定义 ansible_become_password
  tasks:
    - name: 配置 Hostname
      arista.eos.eos_hostname:
        config:
          hostname: "{{ hostname }}"  # 这里会自动去 host_vars 找对应的值

    - name: "1. 开启三层路由功能"
      arista.eos.eos_config:
        lines: ["ip routing"]

    - name: "2. 配置 Loopback 接口"
      arista.eos.eos_l3_interfaces:
        config:
          - name: Loopback0
            ipv4: [{ address: "{{ loopback_ip }}/{{ loopback_mask }}" }]
        state: merged

    - name: "3. 配置物理互联接口 (描述、模式与 IP 全部引用变量)"
      arista.eos.eos_config:
        lines:
          - "description {{ item.description }}"
          - "no switchport"
          - "ip address {{ item.ip }}"
        parents: "interface {{ item.name }}"
      loop: "{{ l3_interfaces }}"

    - name: "4. 创建业务 VLAN 并配置 PC 接口"
      block:
        - arista.eos.eos_vlans:
            config: [{ vlan_id: "{{ access_vlan }}", name: "EVPN_VLAN_{{ access_vlan }}" }]
        - arista.eos.eos_l2_interfaces:
            config:
              - name: "{{ pc_interface }}"
                access: { vlan: "{{ access_vlan }}" }

    - name: "5. 配置 VXLAN 接口 (VTEP) - 使用通用配置模块"
      arista.eos.eos_config:
        lines:
          - "vxlan source-interface Loopback0"
          - "vxlan udp-port 4789"
          - "vxlan vlan {{ access_vlan }} vni {{ vni_id }}"
        parents: ["interface Vxlan1"]
      # state: merged 是默认的,这里会确保 interface Vxlan1 存在并进入其配置模式

    - name: "6. 配置 BGP 全局、Underlay 与 EVPN"
      arista.eos.eos_config:
        lines:
          - "router bgp {{ bgp_as }}"
          - "  router-id {{ loopback_ip }}"
          - "  neighbor {{ item.ip }} remote-as {{ item.remote_as }}"
          - "  neighbor {{ item.ip }} send-community extended"
          - "  address-family ipv4"
          - "    neighbor {{ item.ip }} activate"
          - "    network {{ loopback_ip }}/32"
          - "  exit"
          - "  address-family evpn"
          - "    neighbor {{ item.ip }} activate"
          - "    neighbor {{ item.ip }} encapsulation vxlan"
      loop: "{{ bgp_neighbors }}"

    - name: "7. 配置 BGP VLAN 实例 (RD/RT)"
      arista.eos.eos_config:
        lines:
          - "vlan {{ access_vlan }}"
          - "  rd {{ loopback_ip }}:{{ access_vlan }}"
          - "  route-target both {{ access_vlan }}:{{ access_vlan }}"
          - "  redistribute learned"
        parents: "router bgp {{ bgp_as }}"
Spine 侧:灵活的 shell + vtysh

FRR 的自动化重点在于如何优雅地在 Shell 中嵌入多行指令:

---
- name: "Spine (FRR) 基础配置"
# 这种写法表示:匹配 linux 组中名字包含 spine 的主机
  hosts: "linux:&*spine*"
  gather_facts: no
  # 注意:FRR 节点通常直接使用 SSH,不需要 network_cli
  connection: ssh
  vars:
    ansible_user: root  # clab 默认 root
  tasks:
    - name: "配置接口与 BGP"
      ansible.builtin.shell: |
        vtysh -c "conf t" \
              -c "hostname {{ hostname }}" \
              -c "interface lo" \
              -c "  ip address {{ router_id }}/32" \
              {% for link in p2p_links %}
              -c "interface {{ link.interface }}" \
              -c "  description {{ link.desc }}" \
              -c "  ip address {{ link.ip }}" \
              {% endfor %}
              -c "router bgp {{ bgp_as }}" \
              -c "  bgp router-id {{ router_id }}" \
              -c "  no bgp ebgp-requires-policy" \
              {% for link in p2p_links %}
              -c "  neighbor {{ link.neighbor_ip }} remote-as {{ link.remote_as }}" \
              -c "  neighbor {{ link.neighbor_ip }} description {{ link.peer_name }}" \
              {% endfor %}
              -c "  address-family ipv4 unicast" \
              {% for link in p2p_links %}
              -c "    neighbor {{ link.neighbor_ip }} activate" \
              {% endfor %}
              -c "    network {{ router_id }}/32" \
              -c "  exit-address-family" \
              -c "  address-family l2vpn evpn" \
              {% for link in p2p_links %}
              -c "    neighbor {{ link.neighbor_ip }} activate" \
              {% endfor %}
              -c "  exit-address-family" \
              -c "end" \
              -c "write"

五、 进阶思考:利用 Jinja2 模板合二为一

目前的做法是为每个厂商写一个 Playbook,虽然清晰,但在管理大规模网络时会造成代码冗余。后续我们完全可以利用 Jinja2 模板将两者合二为一。如果需要Jinja2的练习和配置举例,请在评论区留言。

1. 核心思路:数据与逻辑彻底解耦

我们可以创建一个通用的任务(Task),它不再直接写命令,而是调用一个 .j2 模板文件。

  • Inventory 区分:在 inventory 中为设备打上标签(如 os_type: eosos_type: frr)。

  • 条件渲染:在 Jinja2 模板内部使用 if-else 逻辑。

2. 合并后的 Playbook 优势

  • 单一事实来源 (Single Source of Truth):所有的配置逻辑都集中在模板中。

  • 极高扩展性:如果后续增加华为或思科设备,只需在模板中增加一个 elif os_type == 'ios' 分支,而无需修改 Playbook 流程。

  • 代码整洁:Playbook 任务将变得极其简单,只需一行 template: src=bgp.j2 dest=/tmp/config


🚀 六、 披荆斩棘:Playbook 执行与成功日志全解析

当你屏住呼吸按下回车键,看到终端跳动的字符时,才是自动化最迷人的时刻。

1. 执行命令

在控制台输入以下命令,引用 Containerlab 自动生成的资产清单:

# -i 指定资产清单,-v 开启详细模式(可选)
ansible-playbook -i ./clab-evpn-week2/ansible-inventory.yml deploy_spine.yml

2. 成功日志深度解读(Log 示例)

一个完美的部署流程应该包含 ok(检查通过)和 changed(配置变更)。

lab@clab-vm:~/labs/evpn-week2$ ansible-playbook -i ./clab-evpn-week2/ansible-inventory.yml ./deploy_leaf.yml

PLAY [Leaf节点全配置] **********************************************************************************************

TASK [配置 Hostname] ***********************************************************************************************
ok: [clab-evpn-week2-leaf2]
ok: [clab-evpn-week2-leaf1]

TASK [1. 开启三层路由功能] *****************************************************************************************
changed: [clab-evpn-week2-leaf2]
changed: [clab-evpn-week2-leaf1]

TASK [2. 配置 Loopback 接口] ***************************************************************************************
changed: [clab-evpn-week2-leaf2]
changed: [clab-evpn-week2-leaf1]

TASK [3. 配置物理互联接口 (描述、模式与 IP 全部引用变量)] **********************************************************
changed: [clab-evpn-week2-leaf2] => (item={'name': 'Ethernet1', 'ip': '10.0.3.2/30', 'description': 'TO_SPINE1'})
changed: [clab-evpn-week2-leaf1] => (item={'name': 'Ethernet1', 'ip': '10.0.1.2/30', 'description': 'TO_SPINE1'})
changed: [clab-evpn-week2-leaf2] => (item={'name': 'Ethernet2', 'ip': '10.0.4.2/30', 'description': 'TO_SPINE2'})
changed: [clab-evpn-week2-leaf1] => (item={'name': 'Ethernet2', 'ip': '10.0.2.2/30', 'description': 'TO_SPINE2'})

TASK [arista.eos.eos_vlans] ****************************************************************************************
changed: [clab-evpn-week2-leaf1]
changed: [clab-evpn-week2-leaf2]

TASK [arista.eos.eos_l2_interfaces] ********************************************************************************
changed: [clab-evpn-week2-leaf2]
changed: [clab-evpn-week2-leaf1]

TASK [5. 配置 VXLAN 接口 (VTEP) - 使用通用配置模块] ****************************************************************
changed: [clab-evpn-week2-leaf1]
changed: [clab-evpn-week2-leaf2]

TASK [6. 配置 BGP 全局、Underlay 与 EVPN] **************************************************************************
changed: [clab-evpn-week2-leaf2] => (item={'ip': '10.0.3.1', 'remote_as': 65000})
changed: [clab-evpn-week2-leaf1] => (item={'ip': '10.0.1.1', 'remote_as': 65000})
changed: [clab-evpn-week2-leaf1] => (item={'ip': '10.0.2.1', 'remote_as': 65000})
changed: [clab-evpn-week2-leaf2] => (item={'ip': '10.0.4.1', 'remote_as': 65000})

TASK [7. 配置 BGP VLAN 实例 (RD/RT)] *******************************************************************************
changed: [clab-evpn-week2-leaf1]
changed: [clab-evpn-week2-leaf2]

PLAY RECAP *********************************************************************************************************
clab-evpn-week2-leaf1      : ok=9    changed=8    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
clab-evpn-week2-leaf2      : ok=9    changed=8    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
💡 如何看懂这个 Log?
  • ok=9:代表有 9 个步骤执行成功(包含默认的收集设备信息 Task)。

  • changed=8:这是核心指标!代表 Ansible 真正修改了设备的配置。如果再次执行,这个数字应该变为 0(体现了自动化的幂等性)。

  • failed=0:全绿通关,代表 BGP 邻居和接口配置已成功下发。


3. 最终验证:BGP 状态“收割”

部署完成后,直接在 Leaf1 上执行查询。如果看到以下输出,说明你的全套 Playbook 已经打通了 EVPN 的控制平面。

leaf1#show bgp evpn summary 
BGP summary information for VRF default
Router identifier 2.2.2.1, local AS number 65001
Neighbor Status Codes: m - Under maintenance
  Neighbor V AS           MsgRcvd   MsgSent  InQ OutQ  Up/Down State   PfxRcd PfxAcc PfxAdv
  10.0.1.1 4 65000            411       465    0    0 06:01:11 Estab   1      1      1
  10.0.2.1 4 65000            412       468    0    0 06:01:06 Estab   1      1      2

关键点提取:

  • State: Establ:BGP 邻居已建立。

  • PfxRcd: 1:从 Spine 收到了 1 条 EVPN 路由。看到这个数字不为 0,才算真正大功告成!


🏗 七、 Ansible Deploy 全流程总结(大牛思维导图)

为了让读者一目了然,我们可以用流程化的方式总结这次自动化部署的闭环:

我的ansible项目的目录树

.
├── ansible.cfg          # 配置文件(优化 SSH 性能)
├── deploy_leaf.yml      # leaf playbook:定义配置逻辑
├── deploy_spine.yml     # spine playbook:定义配置逻辑
├── clab-evpn            # containerlab deploy文件夹
    └── ansible-inventory.yml     # 资产表:定义设备信息
└── host_vars/           # 变量文件夹:定义每台设备的私有数据
    ├── leaf1.yml
    ├── leaf2.yml
    ├── spine1.yml
    └── spine2.yml

流程解析

  1. 解析 Playbook:Ansible 读取 deploy_leaf.yml,识别任务逻辑(如:配置 BGP)。

  2. 查询 Inventory核心点! 直接读取 Containerlab 自动生成的 ansible-inventory.yml,瞬间获取所有 Leaf 和 Spine 的最新 IP。

  3. 建立通道

    • 身份验证:利用我们之前注入的 SSH 公钥实现免密。

    • 安全策略:通过 ansible.cfg 忽略指纹检查,确保通道瞬间打通。

  4. 开始任务

    • Leaf 侧:调用 eos_config 下发 CLI(解决 Resource Module 兼容性坑)。

    • Spine 侧:调用 shell 模块在 FRR 中执行 vtysh 命令。


🎁 结语

从手动敲命令到 Ansible 自动化下发,不仅是效率的提升,更是从“实验思维”向“生产工程”的进化。通过这种方式,我们可以一键拉起数十台交换机的 EVPN 隧道。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐