网络自动化 · IAC + CI/CD · 入门到高手

网络自动化运维实战:把交换机当代码来管

一份面向「网络设备运维管理」的完整学习资料。用通俗的语言和生动的例子,讲清楚落地一套 IAC(基础设施即代码)+ CI/CD 自动化方案需要的基础架构、软件和方法。读完你将从「只会敲命令行」进阶到「合并即部署」的高手思维。

适用对象:网络工程师 / 运维工程师 / DevOps | 阅读时长:约 45 分钟 | 版本基线:NetBox v4.6 · Ansible core 2.19 · OpenTofu 1.11 · Batfish 0.36(2024–2026 实践)

热身一、为什么网络工程师也要学 IAC + CI/CD

先讲一个真实的场景。周五晚上 22:30,你接到告警:某分公司 30 台接入交换机要批量把管理 VLAN 从 10 改成 99。你打开 SecureCRT,一台一台 SSH 进去,敲 conf tvlan 99……敲到第 12 台时手滑,把 vlan 10 删了——整栋楼的管理通道瞬间断了。你冷汗直下,赶紧回滚。

这不是你技术差,而是「手工 CLI 运维」这件事本身就反人性、不可复制、不可回滚。软件工程师早就不这么干了:他们把代码写进 Git,提交后由流水线自动测试、自动发布,出问题一键回退。我们要做的,就是把这套成熟的方法「平移」到网络设备上。这就是 IAC + CI/CD 的本质。

一句话类比 把网络设备的配置当成「软件源代码」:Git 仓库是保险箱,Jinja2 模板是「配置生成器」,CI/CD 流水线是「自动质检 + 自动上线机器人」,而 NetBox 这样的系统就是你记录「网络应该长什么样」的「设计蓝图」。你不再登录设备改东西,而是改蓝图、提交审批、让机器人去执行。

传统手工运维的五大痛点

  • 配置漂移:设备真实状态和你的 Excel/CMDB 对不上,时间一久谁都不敢动。
  • 人工易错且不可重复:逐台敲命令,复制粘贴改 IP 漏一位就是事故,且无法保证每台都一致。
  • 无法审计追溯:谁、什么时候、为什么改了这台设备?只能靠记忆或翻聊天记录。
  • 没有预演环境:变更直接上生产,大多数网络设备没有数据库那样的「事务回滚」,敲错就是全网中断。
  • 规模不可扩展:设备从 50 台涨到 5000 台,手工模式直接崩溃。

IAC + CI/CD 就是这五个痛点的「对症药方」:用单一事实来源根除漂移,用代码保证一致,用 Git 提供审计,用仿真预演拦截错误,用流水线把「一次改一台」变成「一次改五千台」。下一章先把名词讲透。

概念二、四个核心名词,用大白话讲清楚

1. 基础设施即代码(IaC)

传统上,网络「应该长什么样」装在老网的脑子里、写在Word 里、散落在几十个设备里。IaC 的意思是:把期望的网络状态全部写成机器可读的文本文件(模板 + 变量),存进 Git。设备当前配置由这些文本「渲染」出来。想改网络?改文本,而不是登设备。

关键点IaC 分两种风格:命令式(配置)如 Ansible/Nornir,告诉设备「执行这些命令」;声明式(编排)如 Terraform/OpenTofu,声明「网络最终应该是这样」,由工具自己算出差量去补齐。一个管「怎么配」,一个管「要什么」。

2. 持续集成 / 持续部署(CI/CD)

CI(持续集成):你每提交一次配置改动,流水线自动帮你做语法检查、模板渲染、离线仿真预演,像「自动质检员」。CD(持续部署):质检通过后,流水线把配置下发到设备,并完成部署后校验。合起来就是「提交即被自动检验并(在审批后)自动上线」。

3. 单一事实来源(SoT, Single Source of Truth)

这是网络自动化最容易被忽视、却最关键的基石。SoT 是一个系统,唯一、权威地保存「网络的设计意图」:有哪些设备、接口怎么连、IP 怎么分、VLAN 叫什么。所有配置都由 SoT 里的数据「算」出来。这样设备状态和 SoT 永远一致,漂移从此消失。代表软件:NetBoxNautobot

4. GitOps

GitOps 是 CI/CD 的进阶形态:Git 仓库里的状态就是生产环境的「唯一事实」。只要把改动合并进主分支,系统就自动把网络变成那个样子;仓库和真实网络不一致时,会自动纠偏(或报警)。可以理解为「合并即部署」的终极形态。

记忆口诀SoT 存「意图」,Git 存「代码」,CI 做「质检」,CD 去「执行」,GitOps 让「仓库即生产」。

架构三、整体架构:一套能跑起来的五层模型

无论你管 50 台还是 5000 台设备,落地这套方案的骨架都长下面这样。从上到下是一条「提交 → 校验 → 下发 → 回采」的闭环:

第 1 层

① 单一事实来源 SoT(NetBox / Nautobot)

网络的「设计蓝图」:设备清单、接口、IP、VLAN、BGP 邻居、拓扑。CI 阶段只读它来渲染配置。

NetBox v4.6Nautobot v2.4
第 2 层

② Git 仓库(配置即代码)

存放 Jinja2 模板、变量文件、渲染出的配置、CI 定义(.gitlab-ci.yml / workflows)。提供 MR/PR 评审与完整追溯。

GitHubGitLabGitea
第 3 层

③ CI/CD 引擎(流水线编排)

监听 Git 事件,串起「lint → 渲染 → 仿真预演 → 审批 → 灰度 → 部署 → 校验」各阶段,保证可重复、零接触。

GitLab CIGitHub ActionsJenkinsDrone
第 4 层

④ 测试校验层(贯穿 CI,防事故闸门)

静态 lint、离线仿真(Batfish 可达性/ACL 分析)、动态验证(pyATS、NAPALM、SuzieQ),在合并前拦截 90% 的隐患。

Batfish 0.36pyATSpytest
第 5 层

⑤ 自动化执行层(真机下发)

把已校验的配置通过 SSH / NETCONF / gNMI 推到设备,并做 post-check 回采,结果回写 SoT 与 ITSM。

Ansible 2.19Nornir 3.6OpenTofu 1.11

数据是怎么流动的

网工在本地改模板/变量 → git commit && git push 并发起 MR → CI 引擎触发 → 从 SoT 拉取意图数据 + 用模板渲染出目标配置 → Batfish/pyATS 在仿真里预演「改完之后网络还通不通」 → 评审通过并合并 → CD 由执行层把配置下发真机 → 部署后 pyATS/SuzieQ 回采校验,并把结果回写 SoT/ITSM。

黄金法则(Cisco 最佳实践)生产下发只发生在 MR 合并之后;CI 阶段 SoT 是「只读」的,只用来渲染和校验;任何一个校验环节失败,流水线立即阻断,绝不带病上线。

入门四、工具初体验:先跑通最小闭环

别一上来就搭全套。入门阶段的目标:用最少的工具,亲手把「一台交换机」的配置改对、改可重复。你只需要三样东西:GitAnsible一个 Jinja2 模板

Step 1:把配置写进 Git

先在本地建仓库,把设备信息和模板分开存。这是「数据」和「逻辑」分离的第一步,也是后面能规模化的关键。

# 项目结构
network-lab/
├── inventory.ini        # 设备清单(连接信息)
├── templates/
│   └── ios.j2           # Jinja2 配置模板
├── group_vars/
│   └── switches.yml     # 变量:主机名、VLAN、IP 等
└── site.yml             # Ansible 剧本

Step 2:写一份「会呼吸」的配置模板

模板里用 {{ 变量 }} 占位,真正的值放在变量文件里。这样 100 台设备共用一个模板,只改数据。

# templates/ios.j2
hostname {{ inventory_hostname }}
!
vlan {{ mgmt_vlan }}
 name MANAGEMENT
!
interface {{ uplink }}
 description uplink-to-core
 ip address {{ uplink_ip }} 255.255.255.0
!
snmp-server community {{ snmp_ro }} RO

Step 3:用 Ansible 把配置推上去

inventory 告诉 Ansible「连谁、怎么连」;playbook 描述「要做什么」。

# inventory.ini
[switches]
sw1 ansible_host=192.0.2.1
[switches:vars]
ansible_network_os=cisco.ios.ios
ansible_user=admin
ansible_connection=network_cli
# site.yml
- name: 给交换机打基础配置
  hosts: switches
  gather_facts: false
  connection: network_cli
  tasks:
    - name: 用模板渲染并下发配置(变更后写存档)
      cisco.ios.ios_config:
        content: "{{ lookup('ansible.builtin.template', 'templates/ios.j2') }}"
        backup: true
        save_when: modified
跑起来先装集合:ansible-galaxy collection install cisco.ios,再执行 ansible-playbook -i inventory.ini site.yml --check --diff。注意 --check --diff 只「预演」并打印差异,不下发——这是你第一个「安全闸门」。

到此,你已经体验了 IAC 的雏形:配置来自模板+数据,改动可重复、可预览。但还差「自动质检」和「单一事实来源」。这就进入进阶篇。

进阶五、软件工具栈详解:每层该装什么

5.1 单一事实来源:NetBox 与 Nautobot

NetBox v4.6(Apache-2.0)

IPAM/DCIM 起家,社区最大、插件最多(NAPALM、Config Context)。适合「先把网络登记清楚、求稳」的团队。通过 REST/GraphQL API 把数据喂给 Ansible 渲染配置。

Nautobot v2.4(Open Core)

NetBox 的「企业进化版」,自带 Golden Configuration(配置合规对比)、SSoT(多源同步)、Data Validation Engine、Jobs(平台内跑 Python)。适合需要内置合规与深度自动化的中大型团队。

商用补充还有 Cisco NSO、IP Fabric、Device42。选型建议:纯登记/小团队用 NetBox;要合规审计/中大型用 Nautobot。

5.2 执行层三剑客:Ansible / Nornir / OpenTofu

Ansible(ansible-core 2.19,声明式推送首选)

agentless(无需在设备上装 agent),厂商集合丰富:cisco.iosarista.eosjunipernetworks.junos,还有多厂商统一的 napalm_* 模块。新版本默认用 ansible-navigator。下面是一段真实可跑的 Cisco IOS playbook:

- name: 配置接口与主机名
  hosts: ios
  gather_facts: false
  connection: network_cli
  tasks:
    - name: 设置主机名
      cisco.ios.ios_config:
        lines: hostname {{ inventory_hostname }}
    - name: 配置上行口(parents 定位父级配置节)
      cisco.ios.ios_config:
        lines:
          - description uplink-to-core
          - ip address 172.31.1.1 255.255.255.0
        parents: interface GigabitEthernet0/1

Nornir(v3.6.0,Python 原生、跑得飞快)

被称为「自动化界的 Flask」。没有伪语言,全程 Python,内置多线程并发,比 Ansible 快、可编程性更强。适合高速批量变更和数据驱动任务:

from nornir import InitNornir
from nornir_scrapli.tasks import send_configs
from nornir_utils.plugins.functions import print_result

nr = InitNornir(config_file="nornir.yaml")   # 内含 threaded 并发数
cmds = ["vlan 10", "name DATA", "vlan 20", "name VOICE"]

def push_vlans(task):
    task.run(task=send_configs, configs=cmds)

results = nr.run(task=push_vlans)
print_result(results)

Terraform / OpenTofu(声明式编排)

OpenTofu 是 Terraform 最后一个开源版本(1.5)的社区分支,由 Linux 基金会托管(已进入 CNCF Sandbox),MPL-2.0 许可,可无缝替代。Terraform 自 2023-08 起改用 BUSL 商业许可。它俩负责「编排」:声明网络资源(VLAN、接口、云 VPC)的期望状态并管理其生命周期。下面用 CiscoDevNet 的 iosxe provider 管理一台交换机的 VLAN:

terraform {
  required_providers {
    iosxe = { source = "CiscoDevNet/iosxe", version = "~> 0.1" }
  }
}
provider "iosxe" {
  address  = "https://192.0.2.1"
  username = "admin"
  password = var.ios_password      # 用变量,别硬编码
}
resource "iosxe_vlan" "example" {
  vlan_id  = 123
  name     = "Vlan123"
  shutdown = false
}

三者怎么选?一张表说清

维度AnsibleNornirTerraform / OpenTofu
学习曲线低(写 YAML)中(需 Python)中(HCL + 状态模型)
速度中(进程开销)高(原生并发)高(并行 + 增量)
可编程性低(DSL)极高(纯 Python)中(声明式)
网络生态极丰富(厂商集合 + napalm)丰富(scrapli/napalm)中(交换机 provider 渐增)
适合场景配置推送、合规、异构网络高速批量变更、数据任务资源编排、云/Underlay 生命周期
经典组合OpenTofu 负责「建资源」(VLAN、子网、云网络),Ansible 负责「细化配置」(接口、路由、策略)。Terraform 管「要什么」,Ansible 管「怎么配」,两者互补不冲突。

5.3 别忽视的 Python 基座

  • Netmiko:基于 SSH 的多厂商连接库,是 Ansible 网络模块和 Nornir 插件的底层基座。
  • NAPALM:多厂商统一抽象层,统一 get_facts / get_config / load_merge / commit / diff,让「对比一台设备的实际配置和预期配置」变得简单。
  • pyntc:更高层统一 API,封装 Netmiko/NAPALM,简化连接与配置备份。

进阶NetBox 深度专章:把「网络意图」变成单一事实来源

在进阶篇里我们说过 SoT 是「设计蓝图」。这一章把 NetBox 拆开讲:它到底长什么样、怎么让 Ansible 自动读它、怎么从「改 NetBox」变成「改全网」。把 NetBox 想成网络的「户籍系统」——每台设备、每个接口、每个 IP 都在这里有正式户口,谁改了、改成啥,一查便知。版本基线:NetBox v4.x(PostgreSQL 15+ 自 v4.7 起),pynetbox v7.x。

1. 核心数据模型:一套映射真实网络的「对象语言」

NetBox 的精妙在于它的数据模型刻意贴合真实网络,而不是拍脑袋的表。理解这几组对象,你就理解了「网络意图」怎么被机器读懂:

  • DCIM(数据中心基础设施)Site(站点)→ Rack(机柜)→ Device(设备,关联型号、角色、平台、租户)→ Interface(接口,可分 L2/L3、起 VLAN、绑 LAG、挂 IP)→ Cable(连线,含走线 SVG)。
  • IPAM(IP 地址管理)Prefix(CIDR 段,可绑 VRF/VLAN/站点)、IP Address(挂在接口上)、VLAN/VLANGroupVRF(含 import/export RT)、ASN。一句话:IP 不是孤立的数字,而是「挂在某个接口、属于某个 VLAN、在某个站点」的对象。
  • Circuits(线路)ProviderCircuit(运营商线路)→ CircuitTermination(终结到某台设备的接口)。
  • BGP:核心只建模 ASN;完整的 BGP 邻居/Peer Group/路由策略由社区插件 netbox-bgp 提供。
类比过去你改网络靠「脑子里的拓扑 + Excel」。现在拓扑是 Device+Cable,地址规划是 Prefix+IP Address,VLAN 分配是 VLAN 挂在 Interface 上——全部是带关系的结构化数据,能被 API 一键拉出来渲染成配置。

2. Config Context:按层级自动合并的「配置变量」

Config Context 是 NetBox 最被低估的杀手锏。它允许你把 NTP 服务器、Syslog 地址、BGP 参数等「变量」按「区域 / 站点 / 设备角色 / 平台」等维度挂载 JSON,并且按权重自动合并:低权重先合并,高权重覆盖冲突键,非冲突键保留。这样「全局默认 + 站点覆盖 + 单设备特例」三层变量天然成立。

// 区域级 weight=1000
{"ntp-servers":["172.16.10.22","172.16.10.33"],
 "syslog-servers":["172.16.9.100","172.16.9.101"]}
// 单站点 weight=2000,覆盖 syslog
{"syslog-servers":["192.168.43.107"]}
// 渲染结果(自动合并后)
{"ntp-servers":["172.16.10.22","172.16.10.33"],
 "syslog-servers":["192.168.43.107"]}

还可以用 ConfigContextProfile 配 JSON Schema 做数据形状校验,防止有人把变量写错格式。

3. REST + GraphQL API:让脚本自动「读蓝图」

NetBox 提供 REST(pynetbox 客户端)和 GraphQL(一次取嵌套数据)。下面两段是日常最高频的用法:

# pynetbox:拉某站点所有设备及其接口 IP
import pynetbox
nb = pynetbox.api('http://netbox', token='TOKEN')
for dev in nb.dcim.devices.filter(site='branch-sh', has_primary_ip=True):
    print(dev.name, dev.primary_ip4)
    for iface in dev.interfaces.all():
        for ip in iface.ip_addresses.all():
            print("  ", iface.name, ip.address)
# GraphQL:一次嵌套查出设备+接口+IP(requests 直连)
import requests
q = """query{device_list(filters:{site:{name:{exact:"branch-sh"}}}){
  name primary_ip4{address}
  interfaces{name ip_addresses{address}}}}"""
r = requests.post('http://netbox/graphql/',
  headers={'Authorization': f'Token {TOKEN}'}, json={'query': q})
print(r.json()['data']['device_list'])

4. 把 NetBox 当 SoT 驱动自动化(两步走)

第一步:动态清单。用官方 netbox.netbox.nb_inventory 插件,Ansible 直接拿 NetBox 当实时设备源,还能按角色/站点自动分组,并把 Config Context 注入成主机变量:

plugin: netbox.netbox.nb_inventory
api_endpoint: http://netbox
validate_certs: false
config_context: true          # 注入 context 变量
group_by: [device_roles, platforms]
compose:
  ansible_network_os: platform.slug
query_filters:
  - site: branch-sh
  - has_primary_ip: true

第二步:读 NetBox 渲染配置。下面这段脚本从 NetBox 取设备与合并后的 Context,用 Jinja2 渲染出交换机配置,全流程无需登录任何设备:

import pynetbox, jinja2
nb = pynetbox.api('http://netbox', token='TOKEN')
dev = nb.dcim.devices.get(name='branch-sh-sw1')
ctx = dev.config_context                      # 已合并的 context
tpl = jinja2.Environment(loader=jinja2.FileSystemLoader('.')).get_template('switch.j2')
open(f'{dev.name}.cfg','w').write(tpl.render(host=dev, ctx=ctx))
# switch.j2
hostname {{ host.name }}
{% for s in ctx['ntp-servers'] %}ntp server {{ s }}
{% endfor %}{% for s in ctx['syslog-servers'] %}logging host {{ s }}
{% endfor %}{% for v in host.interfaces.all() %}{% if v.tagged_vlans %}! int {{ v.name }} -> {{ v.tagged_vlans | map(attribute='vid') | list }}
{% endif %}{% endfor %}

5. 关键插件与生态

  • netbox-napalm-plugin:封装 NAPALM,做配置备份与「运行态 vs 期望态」合规校验。
  • netbox-bgp:完整 BGP 建模(Sessions / Communities / Routing Policy / Prefix Lists)。
  • Batfish:NetBox 本身无官方插件,通常由流水线里的 pybatfish 对导出的配置做预验(见 Batfish 专章)。
  • Golden Config / SSoT:核心开源 NetBox 无原生 Golden Config,需自组 Oxidized + diff;商业 NetBox Assurance 提供漂移检测。SSoT 聚合多系统真相。

6. NetBox vs Nautobot:到底选哪个

能力NetBoxNautobot
配置合规❌ 核心无(商业 Assurance)✅ Golden Config App
自动化引擎Custom scripts + webhook✅ Jobs(内建调度/审批/钩子)
扩展模型Plugins(边缘扩展)✅ Apps(一等公民)
Git 数据源外部工具✅ 原生(context/Jobs/模板存 Git)
Secrets社区插件✅ 核心框架(接 Vault)
GraphQL✅ v3 起(附加)✅ 1.0 起 GraphQL-first

选型建议:小团队 / 只想把网络登记清楚 → NetBox(社区最大、最轻、集成最广);中大型 / 要内置合规审计、把自动化「跑在 SoT 里」→ Nautobot(Golden Config 做预期配置生成与差异比对,Jobs 直接在平台内执行 Python)。

7. 完整小例子:分公司网络怎么建模与消费

在 NetBox 里建模:Site branch-sh → Rack R1 → 设备 branch-sh-core(role=core)、branch-sh-sw1(role=access);接口各配 10.1.0.1/31;VLAN USERS(100)MGMT(99);Prefix 10.1.0.0/24 绑 VLAN;netbox-bgp 建 BGP Session(local_as 65001 ↔ 65002);Config Context(role=core)给 NTP/Syslog/BGP 变量。CI 里用 nb_inventory 按 role 分组,用 branch-sh.j2 渲染两台设备配置,经 NAPALM 推送并回读比对,偏差触发 webhook 告警。从此改网络只需改 SoT,设备状态永远跟 NetBox 一致。

进阶Ansible 进阶专章:从「能跑」到「能扛生产」

入门篇你用 Ansible 跑通了一台交换机。进阶篇要解决三个问题:环境可复现(执行环境)、配置严格幂等(资源模块)、能管上千台异构设备(角色分层 + 动态清单 + 规模化)。版本基线:ansible-core 2.19、Ansible 发行版 12–13、cisco.ios 11.5.0、netbox.netbox 3.23、ansible-builder 3.x、AWX 24.6。

1. 执行环境(EE):让「我本地能跑」=「生产也能跑」

网络集合依赖一堆特定 Python 库(pyats、ncclient、netaddr)和系统包,宿主机直装必然版本冲突。EE 把这些连同 ansible-core 一起打包成容器镜像,实现「开发 = 生产」的可移植、可审计运行环境。用 ansible-builder 定义:

---
version: 3
build_arg_defaults:
  ANSIBLE_GALAXY_CLI_COLLECTION_OPTS: '--pre'
dependencies:
  ansible_core: { package_pip: ansible-core==2.19 }
  ansible_runner: { package_pip: ansible-runner }
  galaxy:
    collections: [cisco.ios, arista.eos, junipernetworks.junos, vyos.vyos, netbox.netbox]
  python: [pyats, netaddr]
  system: [bindep.txt]
images:
  base_image: { name: registry.access.redhat.com/ubi9/ubi:latest }
additional_build_steps:
  append_final:
    - RUN echo "net-ee ready"

构建:ansible-builder build --tag my-org/net-ee:2.19 --container-runtime podmanpodman push。日常用 ansible-navigator 在 EE 内运行:ansible-navigator run site.yml -i inventory --ee my-org/net-ee:2.19 -m stdout,或 ansible-navigator doc cisco.ios.ios_l3_interfaces 查模块文档、ansible-navigator inventory -i inventory --list 探索分组。

2. 资源模块(Resource Modules):严格幂等的正确姿势

新手最爱用 ios_config: lines:,但它对命令顺序/注释敏感、非严格幂等,跑两次可能结果不同,还会造成「配置漂移」假象。高手一律用资源模块:声明接口/Vlan/BGP 的「期望状态」,Ansible 自己算差量。下面这段跑两次结果完全一致:

- name: L2 access/trunk
  cisco.ios.ios_l2_interfaces:
    config:
      - name: GigabitEthernet1/0/1
        access: { vlan: 100 }
      - name: GigabitEthernet1/0/24
        trunk: { allowed_vlans: "100-110", native_vlan: 99 }
- name: L3 SVI
  cisco.ios.ios_l3_interfaces:
    config:
      - name: Vlan100
        ipv4: [ { address: "10.100.0.1/24" } ]
- name: BGP
  cisco.ios.ios_bgp_global:
    config:
      as_number: "65001"
      bgp: { router_id: { address: "10.100.0.1" } }
      neighbors:
        - neighbor: "10.0.0.2"
          remote_as: "65002"
          activate: true
幂等陷阱清单① 优先资源模块,少用 ios_config: lines:;② cli_command/ios_command 只读、永不幂等;③ check_mode 下 ios_config 可能误报 changed;④ 网络无 setup facts,需显式 gather_facts: falseios_facts;⑤ 只读任务加 changed_when: false,避免误触发 serial 熔断。

3. 角色与 group_vars / host_vars:数据逻辑彻底分离

管上千台异构设备的秘诀:把「逻辑」放进 roles/,把「数据」放进 group_vars/ + host_vars/。层次是 group_vars/ios.yml(平台默认)→ group_vars/access_sw.yml(接入组)→ host_vars/sw-branch-01.yml(单设备覆盖)。新增设备只加一个 host_vars,playbook 一行不用改。

4. 动态清单:让 NetBox 直接当设备源

# inventory/netbox.yml
plugin: netbox.netbox.nb_inventory
api_endpoint: "{{ lookup('env','NETBOX_API') }}"
token: "{{ lookup('env','NETBOX_TOKEN') }}"
validate_certs: false
group_by: [device_roles, sites, platforms]
query_filters:
  - status: active
compose:
  ansible_host: primary_ip4

5. 规模化与可靠性:分批、容错、本地渲染

  • serial 分批serial: 20 避免一次推千台引发的网络风暴;配合 max_fail_percentage 控制熔断。
  • delegate_todelegate_to: localhost + run_once: true 做本地渲染/网管校验。
  • 本地渲染再推送:先用 template 在 localhost 生成期望配置,再推送,便于 diff 审计与 --check
  • 错误处理:用 block/rescue/always 在失败时回滚或告警。
block:
  - cisco.ios.ios_config: { lines: [...] }
rescue:
  - debug: { msg: "下发失败,触发回滚/告警" }
always:
  - meta: { refresh_inventory: false }

6. AWX / Ansible Automation Platform:把执行层「平台化」

  • 调度:为 Job Template 设周期/一次性调度,带时区与通知。
  • 审批节点:Workflow 里放「审批节点」卡点,高危变更需人工点批准才继续下游。
  • 凭据管理:集中存 SSH/enable 密码、Vault、云 API,注入不明文落盘;配合 RBAC 与 Job Slicing 实现千台并发。

7. 完整例子:分公司接入交换机批量部署

目标:给一批接入交换机批量部署「VLAN + 接入口 + 上联 Trunk + STP + SNMP + AAA/RADIUS」。

# group_vars/access_sw.yml(纯数据)
mgmt_vlan: 99
vlans:
  - { vlan_id: 100, name: USERS }
  - { vlan_id: 110, name: VOICE }
access_ports: [GigabitEthernet1/0/1, GigabitEthernet1/0/2]
uplink: GigabitEthernet1/0/24
snmp: { community: "{{ vault_snmp_ro }}", location: "Branch-01" }
aaa:
  radius_servers: ["10.200.0.10"]
  key: "{{ vault_radius_key }}"
stp_priority: 4096
# roles/access_switch/tasks/main.yml
- name: 创建 VLAN
  cisco.ios.ios_vlans: { config: "{{ vlans }}", state: merged }
- name: 接入端口 L2
  cisco.ios.ios_l2_interfaces:
    config: [ { name: "{{ item }}", access: { vlan: 100 } } ]
  loop: "{{ access_ports }}"
- name: 上联 Trunk
  cisco.ios.ios_l2_interfaces:
    config: [ { name: "{{ uplink }}", trunk: { native_vlan: "{{ mgmt_vlan }}", allowed_vlans: "100-110" } } ]
- name: STP 优先级
  cisco.ios.ios_config:
    lines: ["spanning-tree vlan 1-4094 priority {{ stp_priority }}"]
- name: SNMP
  cisco.ios.ios_snmp_server:
    config: { location: "{{ snmp.location }}", communities: [ { name: "{{ snmp.community }}", ro: true } ] }
- name: AAA/RADIUS
  cisco.ios.ios_config:
    lines:
      - "aaa new-model"
      - "radius-server host {{ aaa.radius_servers[0] }} key {{ aaa.key }}"
      - "aaa authentication login default group radius local"
- name: 保存
  cisco.ios.ios_config: { save_when: changed }
# site.yml
- hosts: access_sw
  gather_facts: false
  roles: [access_switch]
  serial: 20

执行:ansible-navigator run site.yml -i inventory/netbox.yml --ee my-org/net-ee:2.19 -m stdout,生产前务必加 --check --diff 验证幂等。

进阶OpenTofu 专章:用「声明式」编排网络资源

OpenTofu 和 Terraform 负责的是 Ansible 不擅长的「资源生命周期」:创建/销毁 VPC、子网、专线、交换机上的 VLAN。它和 Ansible 是黄金搭档——一个管「要什么」,一个管「怎么配」。先厘清身世,再上代码。

1. OpenTofu 是谁:许可证与版本

TerraformOpenTofu
许可证BUSL-1.1(2023-08 起,非 OSI 开源)MPL-2.0(从 TF 1.5.x 分叉)
治理HashiCorp(IBM)Linux 基金会,CNCF Sandbox(2025-04 加入)
版本1.14 GA(2025-11)1.11.0(2025-12)、1.12.0(2026-05)

OpenTofu 1.11 亮点:ephemeral resources / write-only 属性(口令只存内存、不进 state)、enabled meta-argument。1.12 亮点:prevent_destroy 可动态取值、-json-into 同时出人读与机读产物、provider 并发安装。从 Terraform 迁移只需 5 步(备份 state → 装 tofu → tofu inittofu plan 应为 No changes → tofu apply),可回滚。

2. 核心概念与最小 HCL

HCL 声明式:provider(与 API 对话的插件)→ resource(受管对象)/ data source(只读查询)→ state(真实资源与配置的映射,含锁)→ plan(预演 diff)/ applymodule(复用单元)。远端 state 用 S3 backend + 原生锁:

terraform {
  required_version = ">= 1.11.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 6.0" }
  }
  backend "s3" {
    bucket       = "net-tfstate"
    key          = "prod/network.tfstate"
    region       = "ap-east-1"
    use_lockfile = true                # OpenTofu 原生 S3 锁(建议开桶版本控制)
  }
}
variable "vpc_cidr" { type = string, default = "10.20.0.0/16" }
provider "aws" { region = "ap-east-1" }
resource "aws_vpc" "core" {
  cidr_block = var.vpc_cidr
  tags       = { Name = "core-vpc" }
}
output "vpc_id" { value = aws_vpc.core.id }

3. 网络相关 provider 与真实示例

网络 provider 越来越丰富:CiscoDevNet/iosxe(交换机 VLAN/接口,NETCONF)、CiscoDevNet/iosxrCiscoDevNet/nxosvmware/nsxt、云网络 hashicorp/awsazurermgoogle、专线 equinix/equinixmegaport/megaport

# 示例 A:IOS-XE 建 VLAN + trunk 口(单 provider 管多台)
provider "iosxe" {
  username = var.dev_user
  password = var.dev_pass
  devices = [
    { name = "leaf-01", host = "10.1.1.11" },
    { name = "leaf-02", host = "10.1.1.12" },
  ]
}
locals { vlans = { 100 = "WEB", 101 = "APP", 102 = "DB" } }
resource "iosxe_vlan" "v" {
  for_each = local.vlans
  device   = "leaf-01"
  vlan_id  = each.key
  name     = each.value
  shutdown = false
}
resource "iosxe_interface_switchport" "uplink" {
  device              = "leaf-01"
  type                = "TenGigabitEthernet"
  name                = "1/0/1"
  mode_trunk          = true
  nonegotiate         = true
  trunk_allowed_vlans = join(",", keys(local.vlans))
  trunk_native_vlan   = 999
}
# 示例 B:AWS VPC + 子网 + 路由表(多 AZ)
resource "aws_subnet" "private" {
  for_each          = { "a" = "10.20.1.0/24", "b" = "10.20.2.0/24" }
  vpc_id            = aws_vpc.core.id
  cidr_block        = each.value
  availability_zone = "ap-east-1${each.key}"
}
resource "aws_route_table" "private" { vpc_id = aws_vpc.core.id }
resource "aws_route" "to_dc" {
  route_table_id         = aws_route_table.private.id
  destination_cidr_block = "192.168.0.0/16"
  transit_gateway_id     = var.tgw_id
}
resource "aws_security_group" "app" {
  vpc_id = aws_vpc.core.id
  ingress { from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = [var.vpc_cidr] }
}
# 示例 C:NSX-T Overlay 段
data "nsxt_policy_transport_zone" "overlay" { display_name = "TZ-Overlay" }
resource "nsxt_policy_segment" "app" {
  display_name        = "seg-app"
  transport_zone_path = data.nsxt_policy_transport_zone.overlay.path
  connectivity_path   = data.nsxt_policy_tier1_gateway.t1.path
  subnet { cidr = "172.16.10.1/24", dhcp_ranges = ["172.16.10.100-172.16.10.200"] }
}

4. 模块化:把「网络蓝图」拆成可复用 module

# sites/sh-dc1/main.tf —— 一站一目录,各自 state
module "fabric" {
  source = "../../modules/spine_leaf"
  for_each = {
    sh-dc1 = { asn_base = 65100, loopback_cidr = "10.0.0.0/24", p2p_cidr = "10.1.0.0/22" }
    bj-dc2 = { asn_base = 65200, loopback_cidr = "10.0.1.0/24", p2p_cidr = "10.1.4.0/22" }
  }
  site          = each.key
  spines        = 2
  leaves        = 4
  asn_base      = each.value.asn_base
  loopback_cidr = each.value.loopback_cidr
  p2p_cidr      = each.value.p2p_cidr
  lifecycle { enabled = var.build_fabric }   # OpenTofu 1.11 新语法
}

5. OpenTofu 在 CI/CD 里:plan 即「评审单」

# GitLab CI
image: ghcr.io/opentofu/opentofu:1.12.0
variables: { TF_IN_AUTOMATION: "true", TF_INPUT: "0" }
stages: [validate, plan, apply]
before_script: [ "cd $SITE_DIR", "tofu init -input=false -lock-timeout=120s" ]
validate: { stage: validate, script: ["tofu fmt -check -recursive", "tofu validate"] }
plan:
  stage: plan
  script:
    - tofu plan -out=tfplan -lock-timeout=120s -parallelism=10 -detailed-exitcode -json-into=plan.json || [ $? -eq 2 ]
    - tofu show -no-color tfplan > plan.txt
  artifacts: { paths: [tfplan, plan.txt, plan.json], expire_in: 1 week }
apply:
  stage: apply
  when: manual
  environment: { name: prod }
  script: ["tofu apply -input=false -auto-approve tfplan"]
  rules: [{ if: '$CI_COMMIT_BRANCH == "main"' }]

要点:plan 产物即评审对象(贴 MR 评论、用 OPA/Conftest 做策略校验);-detailed-exitcode(0 无变更 / 2 有变更)驱动流水线分支;state 一律远端 + 锁;对 NETCONF 设备把 -parallelism 调小。GitHub Actions 写法同构(用 opentofu/setup-opentofu + Environment protection rules 做审批)。

6. OpenTofu vs Ansible:各管一摊

维度OpenTofuAnsible
模型声明式 + 状态文件,有 drift 检测过程式/幂等任务,无全局 state
强项创建/销毁资源对象(VPC、子网、专线、API 对象)day-2 配置、模板渲染、巡检、多步编排
弱项复杂时序/命令式操作生命周期与依赖图、销毁即回收
经典组合① Tofu 建云 VPC/子网/VGW,输出给 Ansible 去配 CPE 的路由/ACL/IPsec;② Tofu 用 iosxe 拉起 underlay 与 BGP,Ansible 做 golden-config 合规巡检;③ 原则:「存在与拓扑」归 Tofu,「内部形态与流程」归 Ansible,避免两者管同一属性互相覆盖。

7. 完整例子:spine-leaf underlay(loopback + P2P + eBGP)

# modules/spine_leaf/main.tf(节选)
locals {
  spine_ids = range(1, var.spines + 1)
  leaf_ids  = range(1, var.leaves + 1)
  devices   = concat([for s in local.spine_ids : "spine-0${s}"],
                     [for l in local.leaf_ids  : "leaf-0${l}"])
  asn = merge({ for s in local.spine_ids : "spine-0${s}" => var.asn_base },
              { for l in local.leaf_ids  : "leaf-0${l}"  => var.asn_base + l })
  loopback = { for i, d in local.devices : d => cidrhost(var.loopback_cidr, i + 1) }
  links = { for pair in setproduct(local.spine_ids, local.leaf_ids) :
            "s${pair[0]}-l${pair[1]}" => {
              spine = "spine-0${pair[0]}", leaf = "leaf-0${pair[1]}"
              net   = cidrsubnet(var.p2p_cidr, 7, (pair[0]-1)*var.leaves + pair[1]-1)
              port  = pair[1] } }
}
resource "iosxe_interface_loopback" "lo0" {
  for_each = local.loopback
  device   = each.key
  name     = "0"
  description = "${var.site} router-id"
  ipv4_address = each.value
  ipv4_address_mask = "255.255.255.255"
}
resource "iosxe_interface_ethernet" "p2p" {
  for_each = merge(
    { for k, v in local.links : "${k}-spine" => { dev = v.spine, name = "1/0/${v.port}", ip = cidrhost(v.net, 0), peer = v.leaf } },
    { for k, v in local.links : "${k}-leaf"  => { dev = v.leaf,  name = "1/0/1",       ip = cidrhost(v.net, 1), peer = v.spine } })
  device = each.value.dev
  type   = "TenGigabitEthernet"
  name   = each.value.name
  description = "to ${each.value.peer}"
  switchport  = false
  shutdown    = false
  ipv4_address = each.value.ip
  ipv4_address_mask = "255.255.255.254"   # /31
}
resource "iosxe_bgp_neighbor" "underlay" {
  for_each = merge(
    { for k, v in local.links : "${k}-spine" => { dev = v.spine, ip = cidrhost(v.net, 1), remote = local.asn[v.leaf],  asn = local.asn[v.spine] } },
    { for k, v in local.links : "${k}-leaf"  => { dev = v.leaf,  ip = cidrhost(v.net, 0), remote = local.asn[v.spine], asn = local.asn[v.leaf]  } })
  device = each.value.dev
  asn    = tostring(each.value.asn)
  ip     = each.value.ip
  remote_as = tostring(each.value.remote)
  fall_over_bfd = true
  timers_keepalive_interval = 3
  timers_holdtime = 9
  depends_on = [iosxe_interface_ethernet.p2p]
}
output "loopbacks" { value = local.loopback }   # 供 overlay 模块 / Ansible inventory 使用
落地注意iosxe 是 NETCONF 直连,请先在实验环境验证 auto_commitdelete_mode 行为,避免 destroy 误删整段 YANG;生产核心设备用 1.12 的 prevent_destroy = var.is_prod 防误删;设备口令用 1.11 的 write-only 属性避免落盘 state。

高手Batfish 验证专章:合并前就能「跑」一遍网络

Ansible 能帮你下发配置,但它无法回答「改完之后网络还通不通」。Batfish 填补的就是这个缺口:它不登录任何设备,只离线解析你的配置文件,在内存里把整张网络「跑」起来,然后用断言回答可达性、BGP 邻居、ACL、环路等问题。版本基线:Batfish 服务 + pybatfish 0.36.0。

1. 核心能力:一套「厂商中立模型」

Batfish 解析 Cisco IOS/XR/NX-OS、Juniper、Arista、Cumulus、SONiC、F5、Palo Alto 等配置,构建厂商无关模型(VI model),再用 Question 查询行为。主要 Question:

  • 路由/会话bgpSessionStatusospfSessionStatusbgpEdges
  • 可达性/转发reachabilitytracerouteloopfibRoutesforwarding
  • ACL/策略aclReachabilityfilterLineReachabilityaclExplanations
  • 配置合规undefinedReferencesunusedStructuresassert

2. pybatfish 骨架

from pybatfish.client.session import Session
from pybatfish.question import bfq
from pybatfish.datamodel.flow import PathConstraints

bf = Session(host="localhost")          # 连 Batfish 服务(docker: batfish/allinone)
bf.set_network("dc1"); bf.init_snapshot("/path/snapshot", name="s1", overwrite=True)

3. 四个真实场景:CI 里一行 assert 拦住事故

# 场景1:某流量从 nodeA 必须能到 nodeB
ans = bfq.reachability(
    pathConstraints=PathConstraints(startLocation="nodeA", endLocation="nodeB"),
    actions="SUCCESS").answer().frame()
assert not ans.empty, "A→B 不可达,拦截合并"

# 场景2:全部 BGP / OSPF 邻居必须建立
bgp = bfq.bgpSessionStatus().answer().frame()
assert bgp["Established_Status"].isin(["ESTABLISHED"]).all(), "存在未建立的 BGP 邻居"
assert bfq.ospfSessionStatus().answer().frame()["Session_Status"].eq("FULL").all()

# 场景3:ACL 不会误阻断关键流量(检测永远不可达的行)
acl = bfq.aclReachability().answer().frame()
assert acl[acl["Unreachable_Lines"] != ""].empty, "ACL 含永远不可达行,可能误杀"

# 场景4:无转发环路 + 无未定义引用
assert bfq.loop().answer().frame().empty, "网络存在转发环路"
assert bfq.undefinedReferences().answer().frame().empty, "配置引用了不存在的 ACL/route-map"
为什么它值钱传统做法要「登设备敲 show、等告警」才能发现配错;Batfish 在 MR 合并前、不碰任何真机就能断言「改完之后 A 到 B 还通不通、BGP 全不全、ACL 会不会误杀」。这是把 90% 的事故挡在门外的关键闸门。

4. Batfish 在 CI/CD 里的位置:合并前预演

MR/PR 触发 → 渲染配置 → Batfish 解析并跑断言 → 失败即阻断。它与 NVIDIA Air 数字孪生互补:Batfish 做逻辑/策略正确性(离线、秒级、零成本),Air 做运行时仿真(真正启动 Cumulus/SONiC 跑 show/ping)。两者都是「质量门禁」的前置关卡,一个省钱、一个更真。

高手六、CI/CD 流水线:让机器人替你上线

进阶篇你还是会手动敲 ansible-playbook。高手的做法是:把这条命令塞进流水线,每次提交自动跑。核心思想八个字——「先验证,后上线」。用离线仿真替代「登设备试错」,把事故挡在合并之前。

6.1 流水线的标准阶段

1
静态检查 lint:yaml-lint 校验格式、ansible-lint 检查剧本规范。
2
模板渲染:从 SoT 拉数据 + Jinja2 渲染出目标配置,产出「待上线版本」。
3
仿真预演:Batfish 在内存里「跑」这份配置,验证改完网络还通不通(MR 触发)。
4
干跑 dry-runansible --check --diff 只比对不下发,预览逐行变更。
5
变更评审:MR/PR + 至少 1 人审批,高危变更双人复核。
6
灰度 + 生产下发:先小范围 canary,再全量;when: manual 作为人工卡点(审批门禁)。
7
部署后校验:pytest + pyATS 确认接口 up、BGP 邻居起来、业务流量正常。

6.2 一段可直接抄的 GitLab CI

stages: [lint, render, validate, deploy, verify]
variables: { ANSIBLE_FORCE_COLOR: "true" }

yaml_lint:
  stage: lint
  image: cytopia/yamllint
  script: [yamllint -c .yamllint.yml ., ansible-lint || true]

render_configs:
  stage: render
  image: python:3.11
  script: [pip install jinja2 netutils, python render.py -i inventory.yml -t templates/ -o build/]
  artifacts: { paths: [build/] }

batfish_check:            # 离线仿真,MR 触发
  stage: validate
  image: python:3.11
  services: [{ name: batfish/allinone }]
  script: [pip install pybatfish==0.36.0, python tests/batfish_validate.py]
  rules: [{ if: $CI_PIPELINE_SOURCE == "merge_request_event" }]

ansible_dryrun:          # 干跑,只对比不下发
  stage: validate
  image: ansible-net:latest
  script: [ansible-playbook -i inventory.yml site.yml --check --diff]
  rules: [{ if: $CI_PIPELINE_SOURCE == "merge_request_event" }]

deploy_canary:           # 灰度,manual = 审批门禁
  stage: deploy
  script: [ansible-playbook -i canary.yml site.yml]
  when: manual
  rules: [{ if: $CI_COMMIT_BRANCH == "main" }]

deploy_prod:
  stage: deploy
  script: [ansible-playbook -i prod.yml site.yml]
  when: manual
  environment: { name: production }

post_verify:             # 部署后校验
  stage: verify
  script: [pytest tests/test_post_deploy.py -q]

6.3 防事故神器:测试与校验工具

Batfish(离线仿真)

AWS 维护,不需要真实设备就能解析多厂商配置,验证可达性、BGP/OSPF 会话、ACL 语义、转发环路。合并前发现 90% 隐患。最小用法:起 batfish/allinone 容器 → init_snapshot → 断言。

from pybatfish.client.session import Session
from pybatfish.client.asserts import (
    assert_no_undefined_references,
    assert_no_forwarding_loops,
    assert_no_unestablished_bgp_sessions)
bf = Session(host="batfish")
bf.set_network("net")
bf.init_snapshot("build", name="candidate", overwrite=True)
assert_no_undefined_references()
assert_no_forwarding_loops()
assert_no_unestablished_bgp_sessions()

pyATS / Genie(状态验证)

Cisco 内部千万级 CI 框架。Genie 提供厂商无关解析器,最适合「部署前学基线、部署后比差异」。配 testbed.yaml 定义设备连接,用 genie run 跑现成用例。

# test_post_deploy.py(pytest 写法)
def test_bgp_up(conn):
    out = conn.execute("show ip bgp summary")
    assert "Established" in out

def test_uplink_up(conn):
    out = conn.execute("show interface Gi0/1")
    assert "line protocol is up" in out

再加上 ansible --check --diff 的逐行 diff、NAPALM 的 intended-vs-actual 合规对比、SuzieQ 的持续可观测校验,就构成了一道道「防事故闸门」。GitHub Actions 的写法与此同构,只是把 stages 写成 jobs + steps,思路完全一致。

高手业界实践专章:NVIDIA 与 Cisco 是怎么做的

前面讲的都是「工具怎么用」。这一章看两家头部公司真实的网络自动化 CI/CD 落地,把抽象方法论变成可参照的范本。你会发现:无论多牛的团队,骨架都逃不出「Git 唯一入口 + 自动校验 + 审批门禁 + 质量门禁」。

1. NVIDIA:用数字孪生把网络变更「先彩排再上线」(2025)

NVIDIA 在 2025 年的技术博客里公开了他们用 GitLab CI 管理数据中心网络的完整做法,核心是把 Cumulus Linux + NVIDIA Air 数字孪生接入 CI/CD。收益是四点:速度、质量(自动检查减错)、可靠性、可扩展性。

他们的开发团队真实流水线分七步:

提取 Git 仓库内容:拓扑(dot/json)、Jinja2 模板、配置脚本。
语法/格式检查:YAML、JSON、Jinja2 是否可解析。
用 Air API / Python SDK 创建仿真拓扑
配置拓扑参数(组织、过期时间)。
启动仿真并注入渲染好的配置到虚拟设备/服务器。
用 show 命令 + ping 验证布线与设计是否符合预期。
保存并关闭仿真(可销毁重建,保证可重复)。

工程上他们把代码拆成多仓库:AIR 自动化代码库 + 网络配置渲染库(Jinja2 模板)分离。Air 提供 REST API(底层)+ Python SDK(封装),可启动/注入/验证/销毁实验室。关键经验:把 CI/CD 变成「部署生产的唯一途径」,任何绕过流水线的手工改动都视为违规

可抄的点你没有 NVIDIA Air 也不要紧——用容器跑 Cumulus/SONiC 的免费镜像、或 GNS3/Containerlab 搭仿真,照样能在 CI 里做「启动 → 注入配置 → ping 验证」。数字孪生不一定贵,先有「能在隔离环境跑一遍」的意识最重要。

2. Cisco:Network Automation Delivery Model(NADM)官方交付模型

Cisco DevNet 给出的官方 CI/CD 架构更强调「组件化交付」:

  • Git 作为唯一入口:所有源码入库,每次 commit 触发自动构建。
  • 版本库 + Artifactory + Issue 系统(JIRA)+ 自动构建测试系统 Jenkins:Artifactory 存二进制/包,保证「测的是同一份、部署的也是同一份」。
  • 分阶段流水线:组件先在各自 CI 独立测试,再汇入主部署管线做集成/E2E;单组件升级(新 NED/NSO 版本)分钟级验证。
  • 质量门禁:CI/CD 是部署到生产的唯一途径;「完成定义」必须含通过 CI/CD 与测试;管线软件视作关键基础设施,专人维护。
  • 与 NSO / pyATS 的关系:NSO 作为持续演进项目,Function Pack、NED、NSO 版本任一变更即触发 Jenkins 集成测试;pyATS 做设备级实测,与 Batfish 的离线验证形成「逻辑层 + 实测层」双层校验。

3. 其他公司的公开经验(简)

  • Meta/Facebook:NetNORAD 主动探测丢包,并发 NSDI'22 论文讲 Change Cube 意图驱动遥测,强调变更追踪与意图分层验证。
  • Google:Network as Code,网络配置纳入版本与流水线,CI/CD 参考架构见 GKE 文档。
  • Cloudflare:强调小步增量发布、边缘自动化与一键回滚降低风险。

4. NVIDIA vs Cisco 对比 + 普通团队可抄清单

维度NVIDIACisco
仿真NVIDIA Air 数字孪生(运行时)NSO 集成 + pyATS 实测
配置生成Jinja2 模板渲染NSO Function Pack / 服务模型
编排GitLab CIJenkins + Artifactory
入口多仓库 GitGit 唯一入口
验证侧重拓扑/布线/可达性仿真NSO 组件集成 / E2E
普通团队可抄的 6 步清单 1) 配置即代码入 Git,每次 PR 触发 CI;
2) CI 第一步:YAML/Jinja2 语法 + Batfish init_snapshot 解析,捕获 undefinedReferences
3) 加断言:BGP/OSPF 全建立、关键路径 reachabilityaclReachability、无 loop
4) 有预算再上数字孪生(Air/容器仿真)跑 show/ping 做运行时验证;
5) 构建产物进 Artifactory,部署经同一管线,设「唯一入口 + 质量门禁」;
6) 专人维护管线,把「通过验证」写入变更完成定义。

落地七、从 0 到 1 演进路线(别想一步登天)

很多团队一上来就想搞 GitOps,结果半年没落地。正确姿势是逐级演进,每一级都解决上一级的痛点,再自然进入下一级。

阶段特征痛点(催生下一跃)跃迁条件
① 手动 CLI登录设备手敲命令易错、不可追溯、不可复现事故频发,想「可重复」
② 脚本化Python/Expect 批量执行脚本无版本控制、难维护需要审计与回滚
③ 模板化Jinja2 + 变量生成配置模板与数据仍靠人工渲染引入 Git 与 SoT
④ IaCAnsible/Terraform 声明式缺自动化校验,仍手敲执行接入 CI 做 lint/校验
⑤ CI/CDMR + 自动校验 + 灰度 + 回滚仍有人工触发部署信任自动化、测试覆盖够
⑥ GitOps合并即部署,仓库即生产需强审批与可观测成熟测试网 + 变更窗口

一个可参照的真实落地案例

NVIDIA 在 2025 年的技术博客里披露:他们用 GitLab CI 管理数据中心网络——工程师提交 Jinja2 模板与变量,流水线自动渲染配置、跑校验,评审通过后由 Ansible 下发,并用数字孪生(NVIDIA Air)做预演。核心经验是:把 CI/CD 变成「部署生产的唯一途径」,任何绕过流水线的手工改动都被视为违规。Cisco DevNet 也给出同样的「网络自动化交付模型」:Git 是单一入口,CI 是质量门禁,CD 是受控执行。

新手最容易踩的坑别在第 ① 阶段就想引入 SoT 和 GitOps。先让 1 台设备用 Ansible 跑通(阶段 ④),再补 CI(阶段 ⑤),等测试网和审批流程成熟了,才上 GitOps(阶段 ⑥)。每级都先在小范围验证,再扩大。

精进八、工程实践与避坑清单

分支与审批

  • main 分支受保护,改动走 feature 分支 → MR。
  • MR 关联工单与变更窗口;CI 全绿 + 至少 1 人批准才放行。
  • 高危变更双人复核,when: manual 做人工卡点。

密钥管理

  • 设备口令 / API Token 存 HashiCorp Vault 或 CI Secret,运行时注入。
  • 严禁硬编码进仓库;用最小权限账号。
  • 变量文件里的敏感项用 var.ios_password 引用。

回滚方案

  • Ansible/Terraform 天然幂等,重放旧 commit 即可回退。
  • Git revert 旧提交;设备侧用 commit confirmed 600 自动回滚。
  • 预演失败即阻断,绝不带病上线。

变更窗口与幂等

  • 流水线只在变更窗口期放行 deploy_prod,避开业务高峰。
  • 幂等性是安全重试/回滚的基石:重复执行结果一致。
  • 下发后接监控做健康检查,异常自动告警。
幂等性一句话写好 Ansible 任务,让「连跑三次」和「跑一次」的效果完全一样。这样你重试不慌、回滚无忧。命令要写全(如明确 shutdownno shutdown),别留歧义。

路径九、分级学习路线与资源

按你当前水平对号入座,循序渐进。别贪多,先把一层的工具跑顺,再进下一层。

入门

打地基(1–2 周)

Git 基础(commit/branch/MR)、Linux 命令行、Python 基础、Ansible 跑通一台交换机、Jinja2 模板。目标:手动改文本 → 自动下发一台设备。

进阶

建体系(3–6 周)

部署 NetBox 登记网络、Ansible 集合与 napalm、Nornir 并发、OpenTofu 编排 VLAN、变量与模板分层。目标:数据驱动、多设备管理。

高手

上流水线(6–12 周)

GitLab CI/GitHub Actions 流水线、Batfish 仿真、pyATS/pytest 校验、审批门禁、回滚与密钥管理、灰度发布。目标:合并即受控上线。

推荐官方资料(都附链接,按需深挖)

附录十、术语表(速查)

术语含义
IaCInfrastructure as Code,基础设施即代码,把网络状态写成文本由机器管理。
CI / CD持续集成 / 持续部署,提交即自动质检、受控上线。
SoTSingle Source of Truth,单一事实来源,权威记录网络意图的系统(NetBox/Nautobot)。
GitOps以 Git 仓库为生产唯一事实,合并即部署、状态自动纠偏。
Jinja2Python 模板引擎,用变量渲染出设备配置。
network_cliAnsible 经 SSH/CLI 连接网络设备的方式。
dry-run / --check预演模式,只计算差量并打印,不下发。
幂等性重复执行结果一致,是安全重试与回滚的基础。
gNMI / NETCONF现代网络设备可编程接口(取代纯 CLI)。
OpenTofuTerraform 的开源分支(LF/CNCF),MPL-2.0 许可。
写在最后网络自动化的本质不是「学一堆工具」,而是建立一种思维:网络是可被版本化、可被测试、可被自动交付的系统。工具会更新(今天 Ansible 2.19、明天也许有新的),但「SoT 存意图、Git 存代码、CI 做质检、CD 去执行」这套骨架十年不过时。从改好一台交换机开始,一步步走到「合并即部署」,你就是网络运维的高手了。