第317篇:Network-as-Code 网络即代码
关键词
Network-as-Code、GitOps、声明式网络、基础设施即代码、配置即代码、版本控制、网络 DevOps
一、Network-as-Code 理念
1.1 什么是 Network-as-Code
Network-as-Code(网络即代码)是将 网络配置视为软件代码 来管理的理念和方法论:
传统网络 vs Network-as-Code:
传统网络:
┌─ 配置是手工输入的 CLI 命令
├─ 变更靠工程师记忆和经验
├─ 状态存放在设备本地
├─ 没有版本控制
└─ 配置漂移是常态
Network-as-Code:
┌─ 配置是 Git 仓库中的代码文件
├─ 变更通过 PR/MR 审查
├─ 期望状态声明在代码中
├─ 自动同步到设备
└─ 配置漂移自动修复
核心理念:
┌─ 声明式:描述"应该是什么",而非"怎么配置"
├─ 版本化:所有变更可追溯
├─ 自动化:从代码到设备自动同步
├─ 可重复:相同输入产生相同输出
└─ 可测试:变更前可验证
1.2 GitOps 网络模型
GitOps for Network:
| Git 仓库(单一事实源) ├─ config/ ├─ templates/ ├─ policies/ └─ tests/ ├─ validate.py 验证脚本 └── test_config.py 测试脚本 ▼ ┌────────────────────────────────────┐ └────────────────────────────────────┘ ▼ ┌────────────────────────────────────┐ | ├─ devices.yml 设备清单 ├─ network.yml 网络参数 └─ vlans.yml VLAN 规划 ├─ base.j2 基础模板 ├─ core.j2 核心设备模板 └─ access.j2 接入设备模板 ├─ compliance.yml 合规策略 └─ security.yml 安全策略 GitOps Operator / CI Pipeline 1. 检测代码变更 2. 验证语法和合规 3. 渲染设备配置 4. Diff 对比当前状态 5. 审批后部署 6. 验证并报告 网络设备(目标状态) running-config ≈ 代码期望状态 | |
|---|---|---|
二、GitOps 目录结构
2.1 标准仓库结构
network-gitops/
├── README.md
├── .gitlab-ci.yml # CI/CD 流水线
│
├── config/ # 网络配置数据(YAML)
│ ├── global.yml # 全局参数
│ ├── devices.yml # 设备清单
│ ├── sites/
│ │ ├── beijing.yml # 北京站点
│ │ └── shanghai.yml # 上海站点
│ ├── vlans/
│ │ ├── prod.yml # 生产 VLAN
│ │ └── dev.yml # 开发 VLAN
│ └── bgp/
│ ├── asn.yml # AS 号规划
│ └── peers.yml # BGP 邻居
│
├── templates/ # Jinja2 配置模板
│ ├── base/
│ │ ├── base.j2
│ │ └── macros.j2
│ ├── huawei/
│ │ ├── core_router.j2
│ │ ├── access_switch.j2
│ │ └── firewall.j2
│ └── cisco/
│ ├── ios_xr_router.j2
│ └── nxos_switch.j2
│
├── policies/ # 策略定义
│ ├── compliance.rego # OPA 合规策略
│ ├── security.rego # 安全策略
│ └── naming.rego # 命名规范
│
├── tests/ # 自动化测试
│ ├── test_render.py
│ ├── test_compliance.py
│ └── test_connectivity.py
│
├── scripts/ # 自动化脚本
│ ├── deploy.py
│ ├── verify.py
│ ├── validate.py
│ └── rollback.py
│
├── baselines/ # 基线配置
│ ├── CORE-SW01.cfg
│ └── ACC-SW01.cfg
│
└── rendered/ # 渲染输出(.gitignore)
2.2 声明式配置示例
# config/global.yml — 全局配置参数
---
ntp:
servers:
- 203.0.113.1
- 203.0.113.2
timezone: "CST+08:00"
snmp:
location: "Beijing-DC"
contact: "network@corp.com"
communities:
- name: "public-ro"
type: read-only
acl: "ACL-SNMP-RO"
syslog:
servers:
- 10.0.0.100
- 10.0.0.101
facility: local5
dns:
servers:
- 10.0.0.10
- 10.0.0.11
domain: corp.local
# config/devices.yml — 设备清单
---
devices:
- name: CORE-SW01
role: core
mgmt_ip: 10.255.1.1
device_type: huawei
model: CE12808
site: beijing
interfaces:
- name: 40GE0/0/1
description: "Spine-Leaf-01"
mode: trunk
vlans: all
- name: 40GE0/0/2
description: "Spine-Leaf-02"
mode: trunk
vlans: all
- name: ACC-SW01
role: access
mgmt_ip: 10.255.2.1
device_type: huawei
model: S5735-L48T4XE
site: beijing
upstream: CORE-SW01
vlans: [10, 20, 30]
interfaces:
- name: GigabitEthernet0/0/1
description: "Uplink"
mode: trunk
vlans: "10 20 30"
- name: GigabitEthernet0/0/2
description: "Office-F1"
mode: access
vlan: 10
- name: GigabitEthernet0/0/3
description: "Office-F2"
mode: access
vlan: 20
三、声明式同步引擎
3.1 期望状态与实际状态
#!/usr/bin/env python3
# scripts/gitops_sync.py — GitOps 同步引擎
"""
GitOps 同步引擎
比较 Git 中声明的期望状态与设备实际状态
自动修复配置漂移
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from lib.connector import DeviceConnector
from lib.logger import setup_logger
import yaml
import json
log = setup_logger("gitops")
class GitOpsSync:
"""GitOps 同步引擎"""
def __init__(self, repo_path: str):
self.repo_path = repo_path
self.connector = DeviceConnector()
def load_desired_state(self) -> dict:
"""加载期望状态(从 Git)"""
devices_file = os.path.join(self.repo_path, "config/devices.yml")
with open(devices_file) as f:
return yaml.safe_load(f)
def get_actual_state(self, device: dict) -> dict:
"""获取设备实际状态"""
conn = self.connector.connect_netmiko(device)
if not conn:
return {"status": "unreachable"}
actual = {"status": "connected"}
try:
# 主机名
hostname = conn.send_command(
"display current-configuration | include sysname"
)
actual["hostname"] = hostname.replace("sysname", "").strip()
# VLAN
vlan_output = conn.send_command("display vlan")
actual["vlans"] = [
line.split()[0] for line in vlan_output.split("\n")
if line.strip() and line.split()[0].isdigit()
]
# 接口配置
intf_output = conn.send_command("display current-configuration interface")
actual["interfaces"] = intf_output
except Exception as e:
actual["status"] = f"error: {e}"
finally:
self.connector.close(conn)
return actual
def diff_state(self, desired: dict, actual: dict) -> list:
"""比较期望状态与实际状态的差异"""
diffs = []
# 比较 VLAN
desired_vlans = set(
str(v["id"]) for v in desired.get("vlans", [])
)
actual_vlans = set(actual.get("vlans", []))
missing_vlans = desired_vlans - actual_vlans
extra_vlans = actual_vlans - desired_vlans
for vlan in missing_vlans:
diffs.append({
"type": "add",
"object": f"vlan {vlan}",
"desired": f"VLAN {vlan} 应存在",
"actual": "不存在",
})
for vlan in extra_vlans:
diffs.append({
"type": "remove",
"object": f"vlan {vlan}",
"desired": f"VLAN {vlan} 不应存在",
"actual": "存在",
})
return diffs
def auto_remediate(self, device: dict, diffs: list) -> bool:
"""自动修复配置漂移"""
conn = self.connector.connect_netmiko(device)
if not conn:
return False
commands = []
for diff in diffs:
if diff["type"] == "add" and "vlan" in diff["object"]:
vlan_id = diff["object"].split()[-1]
commands.append(f"vlan {vlan_id}")
commands.append("quit")
elif diff["type"] == "remove" and "vlan" in diff["object"]:
vlan_id = diff["object"].split()[-1]
commands.append(f"undo vlan {vlan_id}")
if commands:
try:
conn.send_config_set(commands)
conn.save()
log.info(f"✓ {device['name']}: 已修复 {len(commands)//2} 个漂移")
return True
except Exception as e:
log.error(f"✗ {device['name']}: 修复失败 {e}")
return False
finally:
self.connector.close(conn)
return True
def sync(self):
"""执行同步"""
log.info("=== GitOps 同步开始 ===")
desired = self.load_desired_state()
total_drifts = 0
for dev in desired.get("devices", []):
desired_state = dev
actual_state = self.get_actual_state(dev)
if actual_state.get("status") != "connected":
log.warning(f" {dev['name']}: 无法获取实际状态")
continue
diffs = self.diff_state(desired_state, actual_state)
if diffs:
log.info(f" {dev['name']}: 发现 {len(diffs)} 个漂移")
for d in diffs:
log.info(f" {d['type']}: {d['object']}")
self.auto_remediate(dev, diffs)
total_drifts += len(diffs)
else:
log.info(f" ✓ {dev['name']}: 配置一致")
log.info(f"=== 同步完成,共修复 {total_drifts} 个漂移 ===")
if __name__ == "__main__":
sync = GitOpsSync(repo_path=".")
sync.sync()
四、策略即代码
4.1 OPA 合规策略
# policies/compliance.rego — OPA 合规策略
package network.compliance
# 所有设备必须配置 NTP
deny[msg] {
device := input.devices[_]
not device.config.ntp
msg = sprintf("%v: 缺少 NTP 配置", [device.name])
}
# 必须使用 SSH v2
deny[msg] {
device := input.devices[_]
ssh := device.config.ssh
ssh.version != 2
msg = sprintf("%v: SSH 版本必须为 v2, 当前为 %v",
[device.name, ssh.version])
}
# SNMP v3 优先
warn[msg] {
device := input.devices[_]
snmp := device.config.snmp
snmp.version == "v2c"
msg = sprintf("%v: 建议升级 SNMP 到 v3", [device.name])
}
# 接口命名规范检查
deny[msg] {
device := input.devices[_]
intf := device.config.interfaces[_]
not re_match("^[A-Z][a-z]+[0-9/]+$", intf.name)
msg = sprintf("%v: 接口名 %v 不符合命名规范",
[device.name, intf.name])
}
4.2 Python 策略检查
#!/usr/bin/env python3
# scripts/policy_check.py — 策略检查
import yaml
import re
class PolicyEngine:
"""策略引擎"""
def __init__(self, policies: list):
self.policies = policies
def check_device(self, device: dict) -> list:
"""检查单台设备"""
violations = []
for policy in self.policies:
rule = policy.get("rule", "")
field = policy.get("field", "")
if rule == "required":
# 必填字段检查
if field not in device and field not in device.get("config", {}):
violations.append({
"device": device["name"],
"policy": policy["name"],
"severity": policy.get("severity", "error"),
"message": f"缺少必填字段: {field}",
})
elif rule == "pattern":
# 正则匹配
value = device.get(field, "")
pattern = policy.get("pattern", "")
if value and not re.match(pattern, value):
violations.append({
"device": device["name"],
"policy": policy["name"],
"severity": policy.get("severity", "error"),
"message": f"{field}='{value}' 不匹配 {pattern}",
})
elif rule == "value_in":
# 值范围检查
value = device.get(field)
allowed = policy.get("values", [])
if value and value not in allowed:
violations.append({
"device": device["name"],
"policy": policy["name"],
"severity": policy.get("severity", "error"),
"message": f"{field}='{value}' 不在允许值 {allowed} 中",
})
return violations
# 策略定义
POLICIES = [
{"name": "hostname-format", "rule": "pattern",
"field": "name", "pattern": r"^[A-Z0-9-]+$",
"severity": "error"},
{"name": "ntp-required", "rule": "required",
"field": "ntp_server", "severity": "error"},
{"name": "snmp-required", "rule": "required",
"field": "snmp_community", "severity": "warning"},
{"name": "device-type", "rule": "value_in",
"field": "device_type", "values": ["huawei", "cisco_ios", "juniper_junos"],
"severity": "error"},
]
engine = PolicyEngine(POLICIES)
devices = yaml.safe_load("""
devices:
- name: ACC-SW01
device_type: huawei
ntp_server: 203.0.113.1
- name: invalid-name
device_type: unknown
""")
for dev in devices.get("devices", []):
violations = engine.check_device(dev)
for v in violations:
print(f"[{v['severity'].upper()}] {v['device']}: {v['message']}")
五、Network-as-Code 落地路径
5.1 成熟度模型
Network-as-Code 成熟度:
L1: 手工脚本
┌─ 配置脚本保存在本地
├─ 工程师手动执行
└─ 无版本控制
L2: 代码入 Git
┌─ 配置数据和模板入 Git
├─ 手动触发部署
├─ 有版本历史
└─ CI 做语法检查
L3: 自动化流水线
┌─ Git Push → CI/CD 自动部署
├─ 有审批流程
├─ 自动测试
└─ 灰度发布
L4: GitOps 自愈
┌─ Git 作为单一事实源
├─ 自动检测配置漂移
├─ 自动修复漂移
└─ 全网状态 Git 中一致
L5: 自治网络
┌─ AI 驱动配置优化
├─ 自动规划
├─ 自动优化
└─ 闭环自愈
六、总结
Network-as-Code 核心价值:
配置即代码
┌─ YAML 描述期望状态
├─ Jinja2 模板生成配置
└─ Git 版本控制所有变更
声明式管理
┌─ 描述"要什么"而非"怎么做"
├─ 自动修复配置漂移
└─ 单一事实源
流水线自动化
┌─ CI:验证 + 测试
├─ CD:审批 + 灰度 + 部署
└─ GitOps:持续同步
工程文化转变
┌─ 从"消防员"到"软件工程师"
├─ 从"CLI 高手"到"代码作者"
└─ 从"手工操作"到"自动化编排"
下篇预告:第318篇 — Git 配置管理,将介绍 Git 在网络自动化中的应用,包括分支策略、配置版本管理和团队协作最佳实践。