Containerlab + Ansible 自动化部署 EVPN:从 FRR 镜像定制到 Arista cEOS 零接触下发
为了让读者一目了然,我们可以用流程化的方式总结这次自动化部署的闭环:我的ansible项目的目录树├── ansible.cfg # 配置文件(优化 SSH 性能)├── deploy_leaf.yml # leaf playbook:定义配置逻辑├── deploy_spine.yml # spine playbook:定义配置逻辑├── clab-evpn # containerlab dep
📖 导读
在上一篇文章中,我们聊了如何用 Containerlab 搭建 EVPN 拓扑。但作为追求极致效率的工程师,手动进入每个容器敲 CLI 显然不够优雅。今天,我们直接上硬核干货:如何用 Ansible 实现 Arista cEOS 与 FRR 的全自动化 EVPN 部署。
目录
🏗 七、 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:
-
容器重启或重新部署,管理 IP 可能会变。
-
设备多了,手动输入
ansible_host极易出错。
我的实战方案: Containerlab 在每次执行 deploy 后,会自动在 clab-<lab-name> 目录下生成一个完美的 ansible-inventory.yml。
为什么它是“神来之笔”?
-
实时同步:Containerlab 最清楚它给每个容器分配了什么 IP。这个IP地址在每一次deploy时都可能会变化,只有Containerlab最清楚这次是如何分配的。
-
内置分组:它会根据你在拓扑文件中定义的
kind(如ceos,linux)自动把设备归类。 -
属性完备:它甚至帮你写好了
ansible_connection: docker或ansible_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: eos或os_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
流程解析
-
解析 Playbook:Ansible 读取
deploy_leaf.yml,识别任务逻辑(如:配置 BGP)。 -
查询 Inventory:核心点! 直接读取 Containerlab 自动生成的
ansible-inventory.yml,瞬间获取所有 Leaf 和 Spine 的最新 IP。 -
建立通道:
-
身份验证:利用我们之前注入的 SSH 公钥实现免密。
-
安全策略:通过
ansible.cfg忽略指纹检查,确保通道瞬间打通。
-
-
开始任务:
-
Leaf 侧:调用
eos_config下发 CLI(解决 Resource Module 兼容性坑)。 -
Spine 侧:调用
shell模块在 FRR 中执行vtysh命令。
-
🎁 结语
从手动敲命令到 Ansible 自动化下发,不仅是效率的提升,更是从“实验思维”向“生产工程”的进化。通过这种方式,我们可以一键拉起数十台交换机的 EVPN 隧道。
更多推荐



所有评论(0)