第316篇:CI/CD 网络变更流水线
关键词
CI/CD、持续集成、持续部署、网络变更流水线、GitOps、自动化测试、灰度发布、变更审批
一、CI/CD 引入网络领域
1.1 为什么网络需要 CI/CD
传统网络变更 vs CI/CD 流水线:
传统变更流程:
┌─ 工程师本地修改配置
├─ 手工登录设备执行
├─ 无版本控制
├─ 无自动化测试
├─ 人工验证
└─ 变更记录全靠邮件
问题:
┌─ 人为错误多(漏配、错配)
├─ 回滚困难
├─ 无法审计
└─ 周期长(申请→审批→变更窗→验证)
CI/CD 变更流程:
┌─ Git 提交配置代码
├─ 自动语法检查 + 合规检查
├─ 自动在测试环境验证
├─ 自动生成变更差异
├─ 审批后自动下发
└─ 自动验证 + 回滚
1.2 网络 CI/CD 流水线架构
网络 CI/CD 流水线:
| 开发者 Push 到 Git ↓ ┌────────────────────────────────────┐ └────────────────────────────────────┘ ↓ ┌────────────────────────────────────┐ └────────────────────────────────────┘ ↓ ┌────────────────────────────────────┐ └────────────────────────────────────┘ ↓ ┌────────────────────────────────────┐ | Stage 1: 代码检查(Commit) ├─ YAML 语法检查 ├─ Jinja2 模板渲染验证 ├─ YANG 模型校验 └─ IP 冲突检测 Stage 2: 自动测试(Test) ├─ 在沙箱环境渲染配置 ├─ 与基线配置对比 diff ├─ 合规策略检查 └─ 连通性测试(模拟) Stage 3: 审批(Approve) ├─ 自动生成变更工单 ├─ 发送审批通知 └─ 等待人工/自动审批 Stage 4: 部署(Deploy) ├─ 分批下发(灰度) ├─ 变更后验证 ├─ 失败自动回滚 └─ 发送变更通知 | |
|---|---|---|
二、GitLab CI 实现网络流水线
2.1 GitLab CI 配置文件
# .gitlab-ci.yml — 网络变更 CI/CD 流水线
stages:
- validate
- test
- approve
- deploy
- verify
variables:
# 通用变量
ANSIBLE_HOST_KEY_CHECKING: "false"
NETWORK_CONFIG_DIR: "config"
TEMPLATE_DIR: "templates"
# ===== Stage 1: 验证 =====
validate-yaml:
stage: validate
script:
- python -c "import yaml; yaml.safe_load(open('config/devices.yml'))"
- echo "✓ YAML 格式验证通过"
only:
- master
- merge_requests
validate-templates:
stage: validate
script:
- python -m pip install jinja2
- python scripts/validate_templates.py
only:
- master
- merge_requests
validate-ip-conflicts:
stage: validate
script:
- python scripts/check_ip_conflicts.py
only:
- master
- merge_requests
# ===== Stage 2: 测试 =====
test-render:
stage: test
script:
- python scripts/render_configs.py --output rendered/
- diff -r rendered/ baselines/ || true
artifacts:
paths:
- rendered/
test-compliance:
stage: test
script:
- python scripts/compliance_check.py --config rendered/
only:
- master
# ===== Stage 3: 审批 =====
approve:
stage: approve
script:
# 生成变更摘要
- python scripts/generate_diff_summary.py
# 发送审批通知
- python scripts/notify_approvers.py
when: manual
only:
- master
# ===== Stage 4: 部署 =====
deploy-canary:
stage: deploy
script:
- python scripts/deploy_rolling.py --limit "canary" --batch 1
environment:
name: production/canary
when: manual
only:
- master
deploy-production:
stage: deploy
script:
- python scripts/deploy_rolling.py --batch 3
environment:
name: production
when: manual
needs: ["deploy-canary"]
only:
- master
# ===== Stage 5: 验证 =====
verify-deployment:
stage: verify
script:
- python scripts/verify_deployment.py
only:
- master
2.2 Python 验证脚本
#!/usr/bin/env python3
# scripts/validate_templates.py — 模板验证
import yaml
import os
import sys
from jinja2 import Environment, FileSystemLoader
def validate_templates():
"""验证所有模板可正常渲染"""
errors = []
# 加载数据
with open("config/devices.yml") as f:
devices = yaml.safe_load(f)
# 加载模板
env = Environment(
loader=FileSystemLoader("templates"),
trim_blocks=True,
lstrip_blocks=True,
)
for device in devices.get("devices", []):
template_name = f"{device.get('role', 'default')}.j2"
try:
template = env.get_template(template_name)
output = template.render(device=device)
if not output.strip():
errors.append(f"{device['name']}: 渲染结果为空")
except Exception as e:
errors.append(f"{device['name']}: {e}")
if errors:
print("❌ 模板验证失败:")
for e in errors:
print(f" • {e}")
sys.exit(1)
else:
print(f"✓ 所有模板验证通过 ({len(devices.get('devices', []))} 台设备)")
if __name__ == "__main__":
validate_templates()
三、Jenkins Pipeline 实现
3.1 Jenkinsfile
// Jenkinsfile — 网络变更流水线
pipeline {
agent any
parameters {
string(name: 'CHANGE_DESCRIPTION',
defaultValue: '',
description: '变更描述')
choice(name: 'DEPLOY_MODE',
choices: ['dry-run', 'canary', 'full'],
description: '部署模式')
}
stages {
stage('代码检查') {
steps {
sh 'python -m pip install -r requirements.txt'
sh 'python scripts/validate_yaml.py'
sh 'python scripts/validate_templates.py'
}
}
stage('生成配置') {
steps {
sh 'python scripts/render_configs.py --output output/'
sh 'python scripts/generate_diff.py --output output/diff.txt'
}
post {
success {
archiveArtifacts artifacts: 'output/**/*'
}
}
}
stage('审批') {
when {
expression { params.DEPLOY_MODE != 'dry-run' }
}
steps {
script {
// 生成审批请求
def change_summary = readFile('output/diff.txt')
mail to: 'network-team@corp.com',
subject: "[审批] 网络变更 - ${params.CHANGE_DESCRIPTION}",
body: "变更摘要:\n\n${change_summary}\n\n请审批: ${env.BUILD_URL}"
}
input message: '是否批准变更?',
ok: '批准',
submitterParameter: 'approver'
}
}
stage('灰度部署') {
when {
expression {
params.DEPLOY_MODE == 'canary' ||
params.DEPLOY_MODE == 'full'
}
}
steps {
// 先部署 1 台
sh 'python scripts/deploy.py --limit "canary-group" --check'
sh 'python scripts/deploy.py --limit "canary-group"'
sh 'python scripts/verify.py --limit "canary-group"'
}
}
stage('全量部署') {
when {
expression { params.DEPLOY_MODE == 'full' }
}
steps {
sh 'python scripts/deploy.py --batch 3'
}
}
stage('验证') {
steps {
sh 'python scripts/verify_deployment.py'
}
post {
success {
echo '✓ 变更验证通过'
}
failure {
echo '✗ 变更验证失败,触发回滚'
sh 'python scripts/rollback.py'
}
}
}
}
post {
always {
// 发送变更结果
emailext(
to: 'network-team@corp.com',
subject: "[完成] 网络变更 - ${params.CHANGE_DESCRIPTION}",
body: "变更结果: ${currentBuild.currentResult}\n\n详情: ${env.BUILD_URL}"
)
}
}
}
四、灰度发布与回滚
4.1 分批部署
#!/usr/bin/env python3
# scripts/deploy_rolling.py — 滚动部署与灰度发布
import argparse
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 time
log = setup_logger("rolling_deploy")
# 灰度分组
CANARY_GROUP = ["ACC-SW01", "ACC-SW02"] # 先验证的设备
def deploy_device(device, config_file):
"""部署单台设备"""
connector = DeviceConnector()
conn = connector.connect_netmiko(device)
if not conn:
return False, "连接失败"
# 先备份
backup = connector.backup_config(conn)
backup_file = f"backup/pre_deploy_{device['name']}.cfg"
with open(backup_file, "w") as f:
f.write(backup)
try:
# 读取配置
with open(config_file) as f:
config = f.read()
# 应用配置
output = conn.send_config_set(config.split("\n"))
conn.save()
log.info(f"✓ {device['name']}: 部署成功")
return True, "成功"
except Exception as e:
log.error(f"✗ {device['name']}: {e}")
# 自动回滚
with open(backup_file) as f:
backup_config = f.read()
conn.send_config_set(backup_config.split("\n"))
conn.save()
return False, f"已回滚: {e}"
finally:
connector.close(conn)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--batch", type=int, default=1,
help="每批设备数")
parser.add_argument("--limit", help="设备范围(canary/all/设备名)")
parser.add_argument("--check", action="store_true",
help="仅检查不下发")
args = parser.parse_args()
connector = DeviceConnector()
# 确定目标设备
if args.limit == "canary":
devices = [d for d in connector.devices
if d["name"] in CANARY_GROUP]
elif args.limit:
dev = connector.get_device(args.limit)
devices = [dev] if dev else []
else:
devices = connector.devices
log.info(f"目标设备: {[d['name'] for d in devices]}")
log.info(f"批次大小: {args.batch}")
if args.check:
log.info("--- 检查模式 ---")
for dev in devices:
log.info(f" 将部署: {dev['name']} ({dev['mgmt_ip']})")
return
# 分批部署
for i in range(0, len(devices), args.batch):
batch = devices[i:i + args.batch]
log.info(f"\n=== 部署批次 {i//args.batch + 1}: "
f"{[d['name'] for d in batch]} ===")
for dev in batch:
config_file = f"rendered/{dev['name']}_config.cfg"
if not os.path.exists(config_file):
log.warning(f"跳过 {dev['name']}: 配置文件不存在")
continue
ok, msg = deploy_device(dev, config_file)
status = "✓" if ok else "✗"
log.info(f"[{status}] {dev['name']}: {msg}")
# 批次内等待验证
if ok and args.batch == 1:
time.sleep(5)
log.info(f" 等待验证...")
log.info("=== 部署完成 ===")
if __name__ == "__main__":
main()
4.2 自动回滚
#!/usr/bin/env python3
# scripts/rollback.py — 自动回滚
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from lib.connector import DeviceConnector
from lib.logger import setup_logger
log = setup_logger("rollback")
def rollback_device(device):
"""回滚单台设备"""
backup_file = f"backup/pre_deploy_{device['name']}.cfg"
if not os.path.exists(backup_file):
log.error(f"{device['name']}: 无备份文件,无法回滚")
return False
connector = DeviceConnector()
conn = connector.connect_netmiko(device)
if not conn:
return False
try:
with open(backup_file) as f:
config = f.read()
conn.send_config_set(config.split("\n"))
conn.save()
log.info(f"✓ {device['name']}: 回滚成功")
return True
except Exception as e:
log.error(f"✗ {device['name']}: 回滚失败 {e}")
return False
finally:
connector.close(conn)
def main():
log.info("=== 全网回滚 ===")
connector = DeviceConnector()
success = 0
for dev in connector.devices:
if rollback_device(dev):
success += 1
log.info(f"回滚完成: {success}/{len(connector.devices)}")
if __name__ == "__main__":
main()
五、变更审批与通知
5.1 Webhook 集成
#!/usr/bin/env python3
# scripts/notify_approvers.py — 审批通知
import requests
import json
import sys
def send_approval_request(change_info):
"""发送审批请求"""
# 示例:发送到企业微信/钉钉/Slack
webhook_url = "https://hooks.example.com/network-approval"
message = {
"msgtype": "markdown",
"markdown": {
"title": "网络变更审批请求",
"text": f"""
### 网络变更审批请求
**变更描述**: {change_info.get('description', 'N/A')}
**变更范围**: {change_info.get('scope', 'N/A')}
**变更文件**: {change_info.get('files', 'N/A')}
**变更摘要**:
{change_info.get('diff_summary', 'N/A')[:500]}
请点击链接审批: {change_info.get('approval_url', '#')}
"""
}
}
try:
resp = requests.post(webhook_url, json=message, timeout=10)
print(f"✓ 审批通知已发送: {resp.status_code}")
except Exception as e:
print(f"✗ 通知发送失败: {e}")
def generate_change_ticket(change_info):
"""自动生成变更工单"""
# 示例:创建 ITSM 工单
ticket = {
"title": f"[自动] 网络配置变更 - {change_info['description']}",
"description": change_info.get('diff_summary', ''),
"category": "Network Change",
"priority": change_info.get('priority', 'Normal'),
"affected_items": change_info.get('affected_devices', []),
"requester": "CI/CD Pipeline",
}
# 调用 ITSM API
# resp = requests.post("https://itsm.corp.com/api/tickets", json=ticket)
print(f"变更工单已创建: {ticket['title']}")
return ticket
if __name__ == "__main__":
change = {
"description": "新增 VLAN 100-200",
"scope": "ACC-SW01, ACC-SW02",
"files": "config/vlans.yml",
"diff_summary": "+ 创建 VLAN 100 (SALES)\n+ 创建 VLAN 200 (ENG)",
"approval_url": "https://git.corp.com/network/merge_requests/42",
"affected_devices": ["ACC-SW01", "ACC-SW02"],
}
send_approval_request(change)
generate_change_ticket(change)
六、最佳实践
网络 CI/CD 实施要点:
1. 版本控制
┌─ 所有配置代码入 Git
├─ 分支策略(develop → staging → master)
├─ 每次变更必须 MR/PR
└─ 标签对应发布版本
2. 自动化测试
┌─ 语法检查(YAML/JSON/Jinja2)
├─ IP 冲突检测
├─ 合规策略检查
├─ 渲染结果 diff
└─ 沙箱环境验证
3. 安全部署
┌─ 灰度发布(先 1 台→10%→50%→全量)
├─ 自动回滚(部署失败 5 分钟内)
├─ 变更窗口(仅允许维护窗口执行)
└─ 所有操作记录审计日志
4. 持续改进
┌─ 统计部署成功率
├─ 分析回滚原因
├─ 优化测试覆盖率
└─ 缩短变更周期
下篇预告:第317篇 — Network-as-Code,将介绍将网络配置视为代码的管理理念,以及 GitOps 在网络自动化中的应用。