ネットワーク自動化の実践: モデル駆動型アプローチによるL3VPNサービスプロビジョニング例
1. はじめに
現代のネットワーク運用は、俊敏性、信頼性、拡張性の要求に直面しており、従来の手作業によるCLIベースの構成管理では限界を迎えています。この課題に対する解決策として、ネットワーク自動化が不可欠な要素となっています。しかし、単なるスクリプトによる自動化から一歩進み、よりスケーラブルなシステムを構築するには、体系的なアプローチが求められます。
1.1 本記事の目的
本記事は、Cisco IOS-XEを搭載したルータを対象とし、具体的なL3VPNサービスプロビジョニングのユースケースを通じて、モデル駆動型ネットワーク自動化のエンドツーエンドのライフサイクルを解説することを目的とします。提示されたネットワーク構成図を基に、静的な「ベースモデル」から動的な「サービスモデル」を抽出し、そのプロビジョニング・デプロビジョニングを自動化するプロセスを詳述します。
本稿では、自動化の再現性を担保するため、設計思想が明確で、誰が設計しても同じようなデザインになる普遍的で一般的なL3VPN構成をユースケースとして採用しました。また、本文中に登場するアドレス類はすべて仮想的な例であり、実在するネットワークやアドレス体系とは関係がありません。
本稿で解説する内容は、従来の「ネットワークエンジニアリング」から、APIを介してネットワーク機能をサービスとして提供する「ネットワークサービスデリバリ」へと運用パラダイムを転換するためのケーススタディです。APIゲートウェイがネットワーク実装の複雑性(VRF、BGPルートターゲットなど)を抽象化し、サービス利用者がネットワークの詳細を意識することなく、必要な機能をオンデマンドで利用できる世界の実現をイメージしています。
2. ベースモデル
自動化を成功させるための第一歩は、対象となるネットワークアーキテクチャを明確に定義することです。ここでは、自動化の対象外となる静的な基盤構成(ベースモデル)と、自動化によって動的に管理されるサービス構成を明確に分離します。
2.1 対象機器・環境
- CEルータ (※2): Cisco CSR 1000V (Cloud Services Router) を想定します。OSバージョンはIOS-XE 16.xとします。このルータは、現代的なスタック(YANG, NETCONF, RESTCONF)をサポートしており、本ユースケースに最適です。
- APIゲートウェイ (※1): Ubuntu 20.04などのLinuxサーバ上で動作するPython FastAPIアプリケーションを想定します。このサーバは、自動化ロジックの実行、そして外部へのAPI提供という役割を担う制御点となります。
-
周辺コンポーネント:
-
REST Client: 自動化APIの利用者を表すコンポーネントです。手動での
curl
コマンド実行から、より高度なオーケストレーションシステムまで、様々な形態が考えられます。 - UPF: APNユーザーのトラフィック終端点となるUser Plane Function (UPF) を表します。今回はスタブとして配置しているため、ルータで代替しています。
- Edgeルータ: より広範なMPLSコアネットワークを代表するプロバイダエッジ(PE)ルータを想定しています。
-
REST Client: 自動化APIの利用者を表すコンポーネントです。手動での
本稿で解説するアーキテクチャはIOS-XE 16.xを前提としていますが、検証に用いたルータのバージョンは以下の通りです。
CE#sh ver | i Version
Cisco IOS XE Software, Version 16.09.01
2.2 ネットワーク構成の前提
自動化に着手する前に、静的に設定されている「Day 0」コンフィギュレーションについて詳述します。このベースモデルは、自動化システムが変更を加えるべきではない「不変なインフラ層」として扱われます。この明確な責務分離は、自動化システムが誤って基幹インフラに影響を与えることを防ぐための設計パターンです。今回はデータプレーンを構築することが目的ではないため、冗長化せずシンプルな構成にしております。
-
管理用VRF (MGT VRF): APIゲートウェイとCEルータ間の管理トラフィックは、専用の
MGT VRF
内で疎通します。これにより、本番のサービストラフィックと管理トラフィックが分離され、セキュリティと運用性が向上します。APIゲートウェイとCEルータのGi3インターフェースはこのVRFに収容されます。 -
WAN接続とBGP: CEルータ (AS 65001) とEdgeルータ (AS 65002) 間は、
192.168.1.0/30
のリンクで接続され、eBGPピアを確立します。ここで採用されているのはMPLS Inter-AS Option Bと呼ばれる方式です。これは、AS境界ルータ (ASBR) 間でVPNv4/VPNv6プレフィックスを交換し、ネクストホップを自身のアドレスに書き換える (next-hop-self
) ことで、異なるAS間でL3VPNサービスを延伸する標準的な手法です。 -
LAN接続: CEルータとUPF間の物理的な接続は、事前に確立されています。自動化の対象となるのは、この物理インターフェース上に、APNユーザーごとにVLANサブインターフェースを動的に作成する部分です。
これらの前提を用いて作成したCEルータのベースコンフィグレーションは以下の通りです。簡略化のため、APNユーザーは1〜3に限定して設定しています。(APNユーザはベースコンフィグに追加でユーザ収容イメージを確認いただくために含めております。 )
hostname CE
!
vrf definition MGT
rd 65001:65001
!
address-family ipv4
exit-address-family
!
vrf definition VRF001
rd 65001:1
route-target export 65001:100
route-target import 65001:100
!
address-family ipv4
exit-address-family
!
vrf definition VRF002
rd 65001:2
route-target export 65001:200
route-target import 65001:200
!
address-family ipv4
exit-address-family
!
vrf definition VRF003
rd 65001:3
route-target export 65001:300
route-target import 65001:300
!
address-family ipv4
exit-address-family
!
aaa new-model
aaa authentication login default local
aaa authorization exec default local
!
no ip domain lookup
ip domain name example.local
!
mpls label protocol ldp
!
netconf-yang
!
username apigw privilege 15 secret 5 cisco
username netops privilege 15 secret 5 cisco
!
interface Loopback0
ip address 1.1.1.1 255.255.255.255
!
interface GigabitEthernet1
no ip address
!
interface GigabitEthernet1.101
encapsulation dot1Q 101
vrf forwarding VRF001
ip address 100.100.1.1 255.255.255.0
!
interface GigabitEthernet1.102
encapsulation dot1Q 102
vrf forwarding VRF002
ip address 100.100.2.1 255.255.255.0
!
interface GigabitEthernet1.103
encapsulation dot1Q 103
vrf forwarding VRF003
ip address 100.100.3.1 255.255.255.0
!
interface GigabitEthernet2
description to Inter-AS Option B Underlay to remote ASBR
ip address 192.168.1.1 255.255.255.252
mpls ip
!
interface GigabitEthernet3
vrf forwarding MGT
ip address 100.68.32.1 255.255.255.0
!
!
router bgp 65001
bgp router-id interface Loopback0
bgp log-neighbor-changes
neighbor 2.2.2.2 remote-as 65002
neighbor 2.2.2.2 ebgp-multihop 2
neighbor 2.2.2.2 update-source Loopback0
!
address-family vpnv4
neighbor 2.2.2.2 activate
neighbor 2.2.2.2 send-community both
exit-address-family
!
address-family ipv4 vrf VRF001
network 10.0.1.0 mask 255.255.255.0
exit-address-family
!
address-family ipv4 vrf VRF002
network 10.0.2.0 mask 255.255.255.0
exit-address-family
!
address-family ipv4 vrf VRF003
network 10.0.3.0 mask 255.255.255.0
exit-address-family
!
ip route 2.2.2.2 255.255.255.255 192.168.1.2
ip route vrf VRF001 10.0.1.0 255.255.255.0 100.100.1.2
ip route vrf VRF002 10.0.2.0 255.255.255.0 100.100.2.2
ip route vrf VRF003 10.0.3.0 255.255.255.0 100.100.3.2
ip route vrf MGT 0.0.0.0 0.0.0.0 100.68.32.2
!
ip bgp-community new-format
ip ssh version 2
!
ip access-list standard ACL-NETCONF-SSH-IN
permit 100.68.32.2
deny any log
!
route-map RM-SET-COMM permit 10
set community 65001:100 additive
!
mpls ldp router-id Loopback0 force
!
line vty 0 4
access-class ACL-NETCONF-SSH-IN in
transport input ssh
スタブ設定例(機材リソースの関係で、スタブ2つを1台のRTで代用しています。)
hostname Stub
!
vrf definition VRF001
rd 65002:1
route-target export 65001:100
route-target import 65001:100
!
address-family ipv4
exit-address-family
!
vrf definition VRF002
rd 65002:2
route-target export 65001:200
route-target import 65001:200
!
address-family ipv4
exit-address-family
!
vrf definition VRF003
rd 65002:3
route-target export 65001:300
route-target import 65001:300
!
address-family ipv4
exit-address-family
!
vrf definition VRF101
rd 65010:1
!
address-family ipv4
exit-address-family
!
vrf definition VRF102
rd 65010:2
!
address-family ipv4
exit-address-family
!
vrf definition VRF103
rd 65010:3
!
address-family ipv4
exit-address-family
!
interface Loopback0
ip address 2.2.2.2 255.255.255.255
!
interface Loopback101
description UE pool VRF001 (10.0.1.0/24)
vrf forwarding VRF101
ip address 10.0.1.1 255.255.255.0
!
interface Loopback102
description UE pool VRF002 (10.0.2.0/24)
vrf forwarding VRF102
ip address 10.0.2.1 255.255.255.0
!
interface Loopback103
description UE pool VRF003 (10.0.3.0/24)
vrf forwarding VRF103
ip address 10.0.3.1 255.255.255.0
!
interface Loopback201
description Shared/NAS IP in VRF001
vrf forwarding VRF101
ip address 100.100.100.1 255.255.255.0
!
interface Loopback202
description Shared/NAS IP in VRF002
vrf forwarding VRF102
ip address 100.100.100.1 255.255.255.0
!
interface Loopback203
description Shared/NAS IP in VRF003
vrf forwarding VRF103
ip address 100.100.100.1 255.255.255.0
!
interface Loopback301
description to PDN1 for VRF001 (20.0.1.0/24)
vrf forwarding VRF001
ip address 20.0.1.1 255.255.255.0
!
interface Loopback302
description to PDN2 for VRF002 (20.0.2.0/24)
vrf forwarding VRF002
ip address 20.0.2.1 255.255.255.0
!
interface Loopback303
description to PDN3 for VRF003 (20.0.3.0/24)
vrf forwarding VRF003
ip address 20.0.3.1 255.255.255.0
!
interface GigabitEthernet1
no ip address
!
interface GigabitEthernet1.101
description to CE LAN (VRF001) VLAN 101
encapsulation dot1Q 101
vrf forwarding VRF101
ip address 100.100.1.2 255.255.255.0
!
interface GigabitEthernet1.102
description to CE LAN (VRF002) VLAN 102
encapsulation dot1Q 102
vrf forwarding VRF102
ip address 100.100.2.2 255.255.255.0
!
interface GigabitEthernet1.103
description to CE LAN (VRF001) VLAN 101
encapsulation dot1Q 103
vrf forwarding VRF103
ip address 100.100.3.2 255.255.255.0
!
interface GigabitEthernet2
description to Inter-AS Option B Underlay to AS65001-ASBR
ip address 192.168.1.2 255.255.255.252
mpls ip
mpls label protocol ldp
!
router bgp 65002
bgp router-id interface Loopback0
bgp log-neighbor-changes
neighbor 1.1.1.1 remote-as 65001
neighbor 1.1.1.1 ebgp-multihop 2
neighbor 1.1.1.1 update-source Loopback0
!
address-family vpnv4
neighbor 1.1.1.1 activate
neighbor 1.1.1.1 send-community both
exit-address-family
!
address-family ipv4 vrf VRF001
redistribute connected
exit-address-family
!
address-family ipv4 vrf VRF002
redistribute connected
exit-address-family
!
address-family ipv4 vrf VRF003
redistribute connected
exit-address-family
!
ip route 1.1.1.1 255.255.255.255 192.168.1.1
ip route vrf VRF101 0.0.0.0 0.0.0.0 100.100.1.1
ip route vrf VRF102 0.0.0.0 0.0.0.0 100.100.2.1
ip route vrf VRF103 0.0.0.0 0.0.0.0 100.100.3.1
!
mpls ldp router-id Loopback0 force
Stub#sh ip route vrf VRF001 | b ^Gate
Gateway of last resort is not set
10.0.0.0/24 is subnetted, 1 subnets
B 10.0.1.0 [20/0] via 1.1.1.1, 08:44:58
20.0.0.0/8 is variably subnetted, 2 subnets, 2 masks
C 20.0.1.0/24 is directly connected, Loopback301
L 20.0.1.1/32 is directly connected, Loopback301
Stub#sh mpls ldp neighbor
Peer LDP Ident: 1.1.1.1:0; Local LDP Ident 2.2.2.2:0
TCP connection: 1.1.1.1.646 - 2.2.2.2.24634
State: Oper; Msgs sent/rcvd: 606/606; Downstream
Up time: 08:45:17
LDP discovery sources:
GigabitEthernet2, Src IP addr: 192.168.1.1
Addresses bound to peer LDP Ident:
192.168.1.1 1.1.1.1
Stub#sh ip bgp summary
BGP router identifier 2.2.2.2, local AS number 65002
BGP table version is 1, main routing table version 1
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd
1.1.1.1 4 65001 589 588 1 0 0 08:46:14 0
Stub#sh ip bgp vpnv4 vrf VRF001
BGP table version is 110, local router ID is 2.2.2.2
Status codes: s suppressed, d damped, h history, * valid, > best, i - internal,
r RIB-failure, S Stale, m multipath, b backup-path, f RT-Filter,
x best-external, a additional-path, c RIB-compressed,
t secondary path, L long-lived-stale,
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found
Network Next Hop Metric LocPrf Weight Path
Route Distinguisher: 65002:1 (default for vrf VRF001)
*> 10.0.1.0/24 1.1.1.1 0 0 65001 i
*> 20.0.1.0/24 0.0.0.0 0 32768 ?
Stub#
2.3 基本要件
本アーキテクチャが実現すべきサービス要件は、マルチテナント型のL3VPNサービスを例として取り上げます。具体的には、「1:新規APNユーザーの要求に応じて、他のユーザーとは完全に分離されたL3VPN(VRF)を動的に生成し、2:専用のLANセグメント(VLANサブインターフェース)を割り当て、3:静的ルートを設定し、4:BGPを通じてその到達性をMPLSコアネットワークに広報する」 という一連のプロセスを自動化することが中心的な要件となります。
3. 自動化の選択肢
具体的な実装に入る前に、どのような技術を用いて自動化を実現するか、その選択肢を比較検討します。ツールの選択は、単なる技術的な好みではなく、長期的な運用安定性、拡張性、保守性といった、システムの成熟度を決定づける戦略となります。
3.1 クラスベース vs テンプレートベース
ネットワーク構成を生成するアプローチは、大きく「テンプレートベース」と「モデル駆動型(クラスベース)」に分類できます。
- テンプレートベース: このアプローチは、設定テンプレート(例: Jinja2ファイル)と変数(例: YAMLファイル)を組み合わせて、最終的なコンフィグレーション文字列(CLIコマンドやXML)を生成するものです。「テキスト生成」と表現でき、習得が容易で、単純なタスクを迅速に自動化するのに適しています。
- モデル駆動型: このアプローチは、デバイスの構成スキーマ(YANGモデル)を直接表現した構造化データ(Pythonオブジェクトなど)を操作するものです。「データ操作」と表現でき、ライブラリがPythonオブジェクトからプロトコル固有のフォーマット(NETCONF用のXML)への変換(シリアライゼーション)を担います。
テンプレートベースは初期開発速度に優れますが、OSのバージョンアップによるCLI構文の微妙な変化で容易に破損するなど、脆弱性を抱えています。一方、モデル駆動型アプローチは、自動化ロジックとデバイス固有の構文を分離します。YANGモデルという安定したスキーマに基づいて開発を行うため、OSのアップグレードに対する耐性が高く、よりテスト可能で保守性の高いコードとなります。
ただし、このアプローチは抽象化レイヤーを担うライブラリの品質と継続的なメンテナンスに依存するという側面も持ち合わせています。ライブラリがデバイスOSの進化に追随できなくなると、自動化システム全体の信頼性が損なわれるリスクがあることも理解しておく必要があります。特にYDK等については注意が必要となります。
特徴 | モデル駆動型 (YANG/YDK) | テンプレートベース (Jinja2/CLI) |
---|---|---|
データ検証 | スキーマに基づき厳格に検証 (優) | 文字列として生成されるため検証が困難 |
冪等性 | 状態の定義により容易に実現可能 | スクリプトの作り込みに依存し、実現が複雑 |
抽象化レベル | 高度 (低レベルな構文を隠蔽) | 低度 (CLI/XML構文を直接扱う) |
OS変更耐性 | 高い (モデルが互換性を吸収) | 低い (構文変更で直接影響を受ける) |
初期開発速度 | 中程度 | 速い |
長期保守性 | 高い | 低い |
理想的な用途 | 複雑なサービスプロビジョニング、本番環境 | 単純な設定変更、小規模な定型作業 |
3.2 RESTCONF vs NETCONF
デバイスとの通信プロトコルには、主にNETCONFとRESTCONFがあります。どちらもYANGモデルをベースとしたモダンなプロトコルですが、特性が異なります。
- RESTCONF: HTTP/HTTPS上で動作し、データ形式としてJSONまたはXMLを使用します。Web開発者にとって馴染み深い技術スタックであり、シンプルなCRUD (Create, Read, Update, Delete) 操作に適しています。
- NETCONF: SSH上で動作し、データ形式としてXMLを使用します。RPC (Remote Procedure Call) ベースの豊富な操作セットを持ち、ネットワーク運用に特化した機能が多く含まれています。
本記事では、NETCONFを選択しています。その理由は以下の通りです。
-
トランザクションの完全性: NETCONFは、
<lock>
,<edit-config>
,<commit>
,<unlock>
といった一連の操作を標準でサポートしており、複数の設定変更をアトミックなトランザクションとして実行できます。例えば、VRF、インターフェース、BGP設定を一度に行う際、途中でエラーが発生した場合に全ての変更をクリーンにロールバックできます。これはRESTCONFには標準化された同等の機能が存在しません。本記事のシンプルな実装例ではこの機能を利用していませんが、本番環境における複雑なサービス変更において、このトランザクション機能はシステムの堅牢性を担保する上で重要になります。 -
運用のための豊富な機能: NETCONFは、単純な設定変更以外にも、デバイスがサポートするYANGモデルを動的に取得する
<get-schema>
や、コンフィグのバックアップ・リストアに利用できる<copy-config>
など、運用に不可欠な操作を提供します。 - ネットワーク分野での成熟度: NETCONFはネットワークプログラマビリティの黎明期から存在し、複雑なネットワーク中心の操作に関して、ベンダーサポートとエコシステムが非常に成熟しています。
特徴 | NETCONF | RESTCONF |
---|---|---|
トランスポート | SSH | HTTP/HTTPS |
データエンコード | XML | JSON, XML |
主要パラダイム | RPC (遠隔手続き呼出) | RESTful (リソース指向) |
トランザクション | 標準サポート (lock, candidate, commit) | 標準化されていない (ベンダー依存) |
標準操作 |
get , get-config , edit-config , copy-config 等 |
GET , POST , PUT , PATCH , DELETE
|
モデル検出 |
get-schema RPC |
GET /restconf/data/ietf-yang-library:modules-state |
業界での採用 | ネットワーク機器管理 (特にキャリアグレード) | Webシステム連携、シンプルなデバイス管理 |
NETCONF/RESTCONF以外にも、様々なネットワーク管理方式が存在します。以下の表は、それぞれの特徴と主な用途を簡潔にまとめたものです。
gNMIはgRPCベースでストリーミングテレメトリや状態同期に優れた次世代プロトコルですが、本稿執筆時点では機器ベンダーによるサポート状況が発展途上であるため、今回の主要な検討対象からは外しています。
項目 | CLI (SSH) | SNMP | RESTCONF | NETCONF | gNMI | gRPC/MDT (telemetry) |
---|---|---|---|---|---|---|
管理方式 | テキストベース | OID (MIB) ベース | REST API (HTTP/HTTPS) | RPC (SSH + XML) | gRPC + Protobuf | gRPC Streaming |
モデル準拠 | 独自 (非モデル) | MIB | YANG | YANG | OpenConfig YANG / IETF YANG | Telemetry YANG |
データ形式 | テキスト | SNMP OID | JSON/XML | XML | Protobuf/JSON | Protobuf |
特徴 | 伝統的で広く利用 | 主に監視用 | 簡単にAPI化可能 | トランザクション強力・冪等 | 次世代。双方向ストリーム | ストリーミング監視特化 |
主用途 | 手作業/Ansible連携 | 監視・状態取得・構成管理 | PoC〜中規模API化 | 構成管理 | 構成管理 | Telemetry (運用監視) |
IOS-XE対応(例) | ◎ | △ | ◎ | ◎ | XE 16.12以降 (要機能) | XE 16.12以降 |
4. クラスベースでの実装
NETCONFとYDK(YANG Development Kit)を用いたモデル駆動型アプローチによる実装を具体的に解説します。
実装の第一歩として、APIゲートウェイから対象ルータへのNETCONF接続を確立し、基本的な動作を確認します。この操作を通じて、デバイスが提供するCapabilities情報から、今回利用するCisco-IOS-XE-nativeモデルのリビジョンが2018-07-10
であることを特定できます。
# ssh -s apigw@100.68.32.1 -p 830 netconf
apigw@100.68.32.1's password:
<?xml version="1.0" encoding="UTF-8"?>
<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
<capabilities>
.......
<capability>http://cisco.com/ns/yang/Cisco-IOS-XE-native?module=Cisco-IOS-XE-native&revision=2018-07-10</capability>
.......
<capability>urn:ietf:params:xml:ns:yang:ietf-netconf-with-defaults?module=ietf-netconf-with-defaults&revision=2011-06-01</capability>
<capability>
urn:ietf:params:netconf:capability:notification:1.1
</capability>
</capabilities>
<session-id>36</session-id></hello>]]>]]>
この接続をルータ本体側で確認すると、NETCONFサービスが動作し、セッションが確立されていることがわかります。(nginxはRESTCONFのときに動作しますので、正常)
CE#show netconf-yang sessions
R: Global-lock on running datastore
C: Global-lock on candidate datastore
S: Global-lock on startup datastore
Number of sessions : 1
session-id transport username source-host global-lock
-------------------------------------------------------------------------------
36 netconf-ssh apigw 100.68.32.2 None
CE#show platform software yang-management process
confd : Running
nesd : Running
syncfd : Running
ncsshd : Running
dmiauthd : Running
nginx : Not Running
ndbmand : Running
pubd : Running
4.1 YANGモデルの調査方法
自動化コードを記述する前に、対象デバイスがどのような設定項目(モデル)を、どのような階層構造でサポートしているかを正確に把握する必要があります。
標準仕様は下記の周辺で定義されていますのでご参照ください。
-
Capabilitiesの確認: NETCONFセッション確立時、デバイスは
<hello>
メッセージを送信します。これには、サポートする全てのYANGモデルとそのリビジョン情報が含まれています。ncclient
ライブラリを使えば、session.server_capabilities
からこの一覧を取得できます。これは、デバイスが持つ能力を機械可読な形で宣言する、最も信頼性の高い情報です。 -
スキーマの取得: 特定のモデルの詳細な定義を知るには、
<get-schema>
RPCを使用します。これにより、デバイスから直接YANGファイル(例:cisco-ios-xe-native.yang
)をダウンロードできます。デバイス本体からスキーマを取得するには、例えば以下のようなPythonスクリプトを利用できます。
# cat get_schema.py from ncclient import manager from lxml import etree HOST="100.68.32.1"; PORT=830; USER="apigw"; PASS="cisco" with manager.connect(host=HOST, port=PORT, username=USER, password=PASS, hostkey_verify=False, allow_agent=False, look_for_keys=False, timeout=30) as m: r = m.get_schema("Cisco-IOS-XE-native", version="2018-07-10") if hasattr(r, "data") and isinstance(r.data, (str, bytes)): print(r.data if isinstance(r.data, str) else r.data.decode("utf-8","ignore")) else: xml_text = r.xml if hasattr(r, "xml") else str(r) root = etree.fromstring(xml_text.encode()) data_elem = root.find('.//*[local-name()="data"]') if data_elem is not None: yang_text = etree.tostring(data_elem, method="text", encoding="unicode") print(yang_text) else: print(xml_text)
ただし、取得したYANGファイルは他の多数のファイルを
include
しているため、単体では全体の構造を把握できません。全体像は、公開されているリポジトリから取得するのが効率的です。例えば、VRF定義部のモデルを確認するには、複数のファイルをまたいで定義を追跡する必要があります。
// Cisco-IOS-XE-native@2018-07-10.yang ///////////////////////////////////////////////////////// // native / vrf ///////////////////////////////////////////////////////// // Note: /vrf must be before /ip and /ipv6 and /interface uses config-vrf-definition-grouping; // Cisco-IOS-XE-ip.yang grouping config-vrf-definition-grouping { container vrf { description "VRF commands"; // vrf definition * list definition { description "VRF definition mode"; key "name"; leaf name { description "WORD;;VRF name"; type string; } // vrf definition * / description leaf description { description "VRF specific description"; type string { length "1..244"; } } // vrf definition * / rd leaf rd { description "Specify Route Distinguisher"; type union { type string { pattern "[0-9]+:[0-9]+"; } type inet:ipv4-address; type ios-types:asn-ip-type; } } ....
この定義から、VRF設定の階層構造が以下であることが読み取れます。
grouping config-vrf-definition-grouping { container vrf { list definition { # CLI: "vrf definition <NAME>" key "name"; leaf name { type string; } # VRF名 leaf rd { type string; } # CLI: "rd 65001:1" container address-family { container ipv4 { container route-target { container export { leaf-list community { type string; } } # "route-target export 65001:100" container import { leaf-list community { type string; } } # "route-target import 65001:100" }
実際のYANGモデルは、多数のファイルが相互に
include
やimport
で参照し合う複雑な依存関係を持っています。例えば、Cisco-IOS-XE-native.yang
はCisco-IOS-XE-ip.yang
を、さらにietf-inet-types.yang
やCisco-IOS-XE-types.yang
を参照しており、これらの関連を全て手動で追跡するのは非効率的かつ困難です。 これを容易に可視化するために、pyang
やCiscoが提供するyangsuite
といったツールが役立ちます。 -
モデル構造の可視化: ダウンロードしたYANGファイルは、
yangsuite
ツールを用いてツリー形式で表示することで、その階層構造を視覚的に理解できます。
図は、手元でCisco-IOS-XE-Native.yangのモデルをロードして、リーフなどの確認をしているところです。
-
YDKクラスの探索: YDKは、これらのYANGモデルからPythonクラスを自動生成します。インタラクティブなPythonセッション(
ipython
など)で、dir()
やhelp()
関数を使えば、対応するクラスが持つ属性やメソッドを動的に探索できます。これにより、コードエディタの補完機能のように、利用可能な設定項目を確認しながら開発を進めることができます。>>> dir(Native.Native.Ip.Route) ['IpRouteInterfaceForwardingList', 'Static', 'Topology', 'Vrf', '__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_assign_yleaf', '_assign_yleaflist', '_get_child_by_seg_name', '_perform_setattr', '_prefix', '_revision', 'children', 'clone_ptr', 'get_absolute_path', 'get_child_by_name', 'get_children', 'get_name_leaf_data', 'get_order_of_children', 'get_segment_path', 'has_data', 'has_leaf_or_child_of_name', 'has_list_ancestor', 'has_operation', 'ignore_validation', 'is_presence_container', 'is_top_level_class', 'parent', 'path', 'set_filter', 'set_value', 'yang_name', 'yang_parent_name', 'yfilter', 'ylist_key_names']
また、YDKのソースコードを直接確認することも有効です。YANGモデルから生成されたPythonクラスは、ドキュメンテーションも含まれており可読性が高くなっています。
class Route(Entity): """ Establish static routes .. attribute:: ip_route_interface_forwarding_list **type**\: list of :py:class:`IpRouteInterfaceForwardingList <ydk.models.cisco_ios_xe.Cisco_IOS_XE_native.Native.Ip.Route.IpRouteInterfaceForwardingList>` .. attribute:: profile Enable IP routing table profile **type**\: :py:class:`Empty<ydk.types.Empty>` .. attribute:: static Allow static routes **type**\: :py:class:`Static <ydk.models.cisco_ios_xe.Cisco_IOS_XE_native.Native.Ip.Route.Static>` .. attribute:: vrf ......
4.3 Netconfのトランザクション機能
NETCONFプロトコルは、ネットワークデバイスのコンフィグレーションを安全かつ確実に変更するための、堅牢なトランザクションモデルを提供します。これは、running
、candidate
、startup
という3つのデータストア(コンフィグレーションデータベース)を中核として機能します。
- running: 現在デバイスで有効になっているアクティブなコンフィグレーションです。ここへの直接の変更は即座に反映されます。
-
candidate: 変更を行うためのステージングエリアです。複数の設定変更をここに投入し、検証した後、
commit
操作でrunning
に一括で適用できます。これにより、設定の途中段階がネットワークに影響を与えることを防ぎます。 -
startup: デバイス起動時に読み込まれるコンフィグレーションです。通常、
running
コンフィグをcopy
して保存します。
主要なオペレーションは以下の通りです。
-
<get-config>
/<get>
:running
などのデータストアから現在の設定情報を取得します。 -
<edit-config>
:candidate
やrunning
データストアに対して設定の変更(マージ、作成、置換、削除)を行います。 -
<commit>
:candidate
データストアの内容をrunning
データストアに適用し、変更を有効化します。 -
<copy-config>
: データストア間でコンフィグレーションをコピーします(例:running
からstartup
へ)。
このモデルにより、複数の変更を一つのトランザクションとして扱い、コミット前に検証することで、設定ミスによる影響を抑えることが可能になります。
4.4 YDK/Netconfでの実装例
APNユーザー(Prefix: 10.0.6.0/24, VLAN: 106)をプロビジョニングするPythonコードの例を以下に示します。YDKを用いて、設定オブジェクトを階層的に構築していく様子が分かります。
# -*- coding: utf-8 -*-
from __future__ import print_function
from ydk.providers import NetconfServiceProvider, CodecServiceProvider
from ydk.services import CRUDService, CodecService
from ydk.types import Empty
from ydk.models.cisco_ios_xe import Cisco_IOS_XE_native as Native
from ydk.models.cisco_ios_xe import Cisco_IOS_XE_types as XE_TYPES
import ipaddress
class CiscoProvisioner:
"""
A class to provision customer instance on a Cisco IOS-XE device.
This includes VRF, Sub-interface, BGP, and a user-specified Static Route.
"""
def __init__(self, address, port, username, password):
#Initializes the provisioner with device connection details.
self.device_address = address
self.provider = NetconfServiceProvider(
address=address, port=port, username=username, password=password
)
self.crud = CRUDService()
self.codec_service = CodecService()
self.codec_provider = CodecServiceProvider(type='xml')
def _build_full_config(self, instance_number, ue_static_route_cidr):
#Private method to build the YDK object for the entire customer configuration.
# --- Validation ---
if not 1 <= instance_number <= 9:
raise ValueError("Instance number must be between 1 and 9.")
try:
network = ipaddress.ip_network(ue_static_route_cidr)
static_prefix = str(network.network_address)
static_mask = str(network.netmask)
except ValueError as e:
raise ValueError("Invalid CIDR format for UE static route: '{}'. {}".format(ue_static_route_cidr, e))
# ---0. Instantiate the root object and Parameters ---
native = Native.Native()
n = instance_number
vrf_name = "VRF00{}".format(n)
bgp_as = 65001
rd = "{}:{}".format(bgp_as, n)
rt = rd + "00"
# --- 1. VRF Definition ---
vrf_def = native.vrf.Definition()
vrf_def.name = vrf_name
vrf_def.rd = rd
export_rt = vrf_def.route_target.Export()
export_rt.asn_ip = rt
vrf_def.route_target.export.append(export_rt)
import_rt = vrf_def.route_target.Import()
import_rt.asn_ip = rt
vrf_def.route_target.import_.append(import_rt)
vrf_def.address_family.ipv4 = vrf_def.address_family.Ipv4()
native.vrf.definition.append(vrf_def)
# --- 2. Interface Configuration (Sub-interface) ---
interface = native.interface.GigabitEthernet()
interface.name = "1.10{}".format(n)
interface.encapsulation.dot1q.vlan_id = 100 + n
interface.vrf.forwarding = vrf_name
interface.ip.address.primary.address = "100.100.{}.1".format(n)
interface.ip.address.primary.mask = "255.255.255.0"
native.interface.gigabitethernet.append(interface)
# --- 3. BGP Configuration ---
bgp = native.router.Bgp()
bgp.id = bgp_as
vrf_af_container = bgp.address_family.with_vrf.Ipv4()
vrf_af_container.af_name = XE_TYPES.BgpIpv4AfType.unicast
bgp_vrf_config = vrf_af_container.Vrf()
bgp_vrf_config.name = vrf_name
unicast_config = bgp_vrf_config.Ipv4Unicast()
network_statement = unicast_config.network.WithMask()
network_statement.number = "10.0.{}.0".format(n)
network_statement.mask = "255.255.255.0"
unicast_config.network.with_mask.append(network_statement)
bgp_vrf_config.ipv4_unicast = unicast_config
vrf_af_container.vrf.append(bgp_vrf_config)
bgp.address_family.with_vrf.ipv4.append(vrf_af_container)
native.router.bgp.append(bgp)
# --- 4. Static Route Configuration ---
vrf_route_context = Native.Native.Ip.Route.Vrf()
vrf_route_context.name = vrf_name
route_container = vrf_route_context.IpRouteInterfaceForwardingList()
route_container.prefix = static_prefix
route_container.mask = static_mask
route_entry = route_container.FwdList()
route_entry.fwd = "100.100.{}.2".format(n)
route_container.fwd_list.append(route_entry)
vrf_route_context.ip_route_interface_forwarding_list.append(route_container)
native.ip.route.vrf.append(vrf_route_context)
return native
def apply_full_config(self, instance_number, ue_static_route_cidr, debug=False):
#Applies the entire customer configuration set to the device.
print("--- Starting provisioning for instance {} ---".format(instance_number))
try:
config = self._build_full_config(instance_number, ue_static_route_cidr)
if debug:
print("--- Generated NETCONF RPC (XML Payload) ---")
xml_payload = self.codec_service.encode(self.codec_provider, config)
print(xml_payload)
print("-------------------------------------------")
print("--- Applying full configuration to device {} ---".format(self.device_address))
self.crud.create(self.provider, config)
print("\n*** Full configuration applied successfully! ***")
return True
except (ValueError, Exception) as e:
print("\n*** Failed to apply configuration (An Error Occurred) ***")
print("Error details: {}".format(e))
return False
# === Main Execution Block (for Example Usage) ===
if __name__ == "__main__":
# --- IMPORTANT: Change these values to match your device ---
DEVICE_ADDRESS = "100.68.32.1"
DEVICE_PORT = 830
DEVICE_USER = "apigw"
DEVICE_PASS = "cisco"
# Instantiate the provisioner class.
provisioner = CiscoProvisioner(
address=DEVICE_ADDRESS,
port=DEVICE_PORT,
username=DEVICE_USER,
password=DEVICE_PASS
)
# --- Example: Provision instance 6 with a specific UE static route ---
instance_id = 6
ue_ip_range = u"10.0.6.0/24"
provisioner.apply_full_config(instance_id, ue_ip_range, debug=True)
以下に、このスクリプトを実行した際の出力を示します。debug=True
オプションにより、デバイスに送信されたNETCONF RPCのXMLペイロードが標準出力に表示されていることが確認できます。
# python config_provisioner_ydk.py
--- Starting provisioning for instance 6 ---
--- Generated NETCONF RPC (XML Payload) ---
<native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
<vrf>
<definition>
<name>VRF006</name>
<rd>65001:6</rd>
<address-family>
<ipv4/>
</address-family>
<route-target>
<export>
<asn-ip>65001:600</asn-ip>
</export>
<import>
<asn-ip>65001:600</asn-ip>
</import>
</route-target>
</definition>
</vrf>
<ip>
<route>
<vrf>
<name>VRF006</name>
<ip-route-interface-forwarding-list>
<prefix>10.0.6.0</prefix>
<mask>255.255.255.0</mask>
<fwd-list>
<fwd>100.100.6.2</fwd>
</fwd-list>
</ip-route-interface-forwarding-list>
</vrf>
</route>
</ip>
<interface>
<GigabitEthernet>
<name>1.106</name>
<encapsulation>
<dot1Q>
<vlan-id>106</vlan-id>
</dot1Q>
</encapsulation>
<vrf>
<forwarding>VRF006</forwarding>
</vrf>
<ip>
<address>
<primary>
<address>100.100.6.1</address>
<mask>255.255.255.0</mask>
</primary>
</address>
</ip>
</GigabitEthernet>
</interface>
<router>
<bgp xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-bgp">
<id>65001</id>
<address-family>
<with-vrf>
<ipv4>
<af-name>unicast</af-name>
<vrf>
<name>VRF006</name>
<ipv4-unicast>
<network>
<with-mask>
<number>10.0.6.0</number>
<mask>255.255.255.0</mask>
</with-mask>
</network>
</ipv4-unicast>
</vrf>
</ipv4>
</with-vrf>
</address-family>
</bgp>
</router>
</native>
-------------------------------------------
--- Applying full configuration to device 100.68.32.1 ---
4.5 FastAPIを用いたノースバンドAPI化
前節ではYDKを用いたモデル駆動型のアプローチを示しましたが、ここではYDKとは異なる別のアプローチとして、ncclient
ライブラリとXMLテンプレートを用いた実装例を紹介します。これをFastAPIを用いてWeb APIとして公開することで、どのようにネットワーク機能をサービスとして抽象化できるかを示します。なお、本実装ではサービスのライフサイクルを簡潔に示すため、リソース作成(POST)メソッドに焦点を当てています。
API利用者は、VRFやBGPルートターゲットといったネットワーク内部の実装詳細を意識することなく、抽象化定義されたエンドポイント(例: POST /provision/customer
)を呼び出すだけで、必要なネットワークリソースをオンデマンドで確保できます。
まず、OpenAPI 3.0仕様に則ってAPIの設計を定義します。APIの利用者はエンドポイント、リクエスト/レスポンスのフォーマット、データモデルを理解できるかとおもいます。今回は簡易なAPIのため、本来の「デザインファースト」アプローチを適用する必要はありませんが、一応で仕様を示しています。
//API Specification (OpenAPI 3.0 / YAML )
openapi: 3.0.0
info:
title: Cisco Provisioning API
description: API to provision customer configurations on a Cisco IOS-XE device.
version: "1.0.0"
paths:
/provision/customer:
post:
tags:
- Provisioning
summary: Provision Customer Configuration
description: Provisions a new customer configuration (VRF, Sub-IF, BGP, Static Route) on the device.
requestBody:
description: Information for the customer to be provisioned.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CustomerProvisionRequest'
responses:
'200':
description: Successful Response when the configuration is applied correctly.
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessResponse'
'400':
description: Bad Request (e.g., invalid CIDR format).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'422':
description: Validation Error (e.g., instance number is out of range).
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationErrorResponse'
'500':
description: Internal Server Error (e.g., failure to apply configuration to the device).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
components:
schemas:
CustomerProvisionRequest:
type: object
required:
- instance_number
- ue_bandwidth
properties:
instance_number:
type: integer
description: "Instance number to identify the customer (1-9)."
example: 6
minimum: 1
maximum: 9
ue_bandwidth:
type: string
description: "Network address for the UE bandwidth (CIDR format)."
example: "10.0.6.0/24"
SuccessResponse:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: "Configuration for instance 6 was applied successfully."
ErrorResponse:
type: object
properties:
detail:
type: string
example: "Failed to apply configuration to the device. Check server logs."
ValidationErrorResponse:
type: object
properties:
detail:
type: array
items:
type: object
properties:
loc:
type: array
items:
type: string
example: ["body", "instance_number"]
msg:
type: string
example: "Input should be less than 10"
type:
type: string
example: "less_than"
今回のMockプロジェクトのディレクトリ構成は以下のようになります。
project/
├── config_provisioner.py
└── main.py
最初に、デバイスへの設定ロジックを担うプロバイダークラスを実装します。ここではncclient
ライブラリを用いてNETCONF通信を行い、Pythonのf-stringを簡易的なテンプレートとしてXMLペイロードを生成します。本番環境での利用を想定し、処理の成否に応じて例外を発生させるなど、基本的なエラーハンドリングを組み込んでいます。
#config_provisioner.py
import textwrap
import ipaddress
from ncclient import manager
import xml.dom.minidom
class ProvisioningError(Exception):
"""Custom exception class for provisioning errors."""
pass
class CiscoProvisionerNcclient:
"""
A class to provision a full customer instance on a Cisco IOS-XE device using ncclient.
"""
XML_CONFIG_TEMPLATE = textwrap.dedent("""
<config>
<native xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-native">
<vrf>
<definition>
<name>{vrf_name}</name>
<rd>{rd}</rd>
<address-family>
<ipv4/>
</address-family>
<route-target>
<export><asn-ip>{rt}</asn-ip></export>
<import><asn-ip>{rt}</asn-ip></import>
</route-target>
</definition>
</vrf>
<ip>
<route>
<vrf>
<name>{vrf_name}</name>
<ip-route-interface-forwarding-list>
<prefix>{static_prefix}</prefix>
<mask>{static_mask}</mask>
<fwd-list><fwd>{next_hop}</fwd></fwd-list>
</ip-route-interface-forwarding-list>
</vrf>
</route>
</ip>
<interface>
<GigabitEthernet>
<name>{interface_name}</name>
<encapsulation><dot1Q><vlan-id>{vlan_id}</vlan-id></dot1Q></encapsulation>
<vrf><forwarding>{vrf_name}</forwarding></vrf>
<ip>
<address>
<primary>
<address>{interface_ip}</address>
<mask>{interface_mask}</mask>
</primary>
</address>
</ip>
</GigabitEthernet>
</interface>
<router>
<bgp xmlns="http://cisco.com/ns/yang/Cisco-IOS-XE-bgp">
<id>{bgp_as}</id>
<address-family>
<with-vrf>
<ipv4>
<af-name>unicast</af-name>
<vrf>
<name>{vrf_name}</name>
<ipv4-unicast>
<network>
<with-mask>
<number>{bgp_network_prefix}</number>
<mask>{bgp_network_mask}</mask>
</with-mask>
</network>
</ipv4-unicast>
</vrf>
</ipv4>
</with-vrf>
</address-family>
</bgp>
</router>
</native>
</config>
""")
def __init__(self, address, port, username, password):
self.device_info = {
'host': address, 'port': port, 'username': username, 'password': password,
'hostkey_verify': False, 'device_params': {'name': 'iosxe'}
}
def _build_full_config_xml(self, instance_number, ue_static_route_cidr):
if not 1 <= instance_number <= 9:
raise ValueError("Instance number must be between 1 and 9.")
try:
network = ipaddress.ip_network(ue_static_route_cidr)
static_prefix = str(network.network_address)
static_mask = str(network.netmask)
except ValueError:
raise ValueError("Invalid CIDR format for UE bandwidth: '{}'".format(ue_static_route_cidr))
n = instance_number
bgp_as = 65001
params = {
'vrf_name': "VRF00{}".format(n), 'rd': "{}:{}".format(bgp_as, n),
'rt': "{}:{}00".format(bgp_as, n), 'static_prefix': static_prefix,
'static_mask': static_mask, 'next_hop': "100.100.{}.2".format(n),
'interface_name': "1.10{}".format(n), 'vlan_id': 100 + n,
'interface_ip': "100.100.{}.1".format(n), 'interface_mask': "255.255.255.0",
'bgp_as': bgp_as, 'bgp_network_prefix': static_prefix,
'bgp_network_mask': static_mask,
}
return self.XML_CONFIG_TEMPLATE.format(**params)
def apply_full_config(self, instance_number, ue_static_route_cidr, debug=False):
print("--- Starting provisioning for instance {} ---".format(instance_number))
try:
xml_payload = self._build_full_config_xml(instance_number, ue_static_route_cidr)
if debug:
dom = xml.dom.minidom.parseString(xml_payload)
print("--- Generated XML Payload ---")
print(dom.toprettyxml(indent=" "))
print("--- Applying configuration to device: {} ---".format(self.device_info['host']))
with manager.connect(**self.device_info) as m:
m.edit_config(target='running', config=xml_payload, default_operation='merge')
print("\n*** Configuration applied successfully ***")
return True # Return True on success
except Exception as e:
print("\n*** Configuration application failed ***")
print("Error details: {}".format(e))
return False # Return False on failure
# Block for standalone testing
if __name__ == "__main__":
print("--- Starting standalone test for the provisioner ---")
# --- IMPORTANT: Change these values to match your device ---
DEVICE_ADDRESS = "100.68.32.2"
DEVICE_PORT = 830
DEVICE_USER = "apigw"
DEVICE_PASS = "cisco"
# Instantiate the ncclient-based provisioner class.
provisioner = CiscoProvisionerNcclient(
address=DEVICE_ADDRESS,
port=DEVICE_PORT,
username=DEVICE_USER,
password=DEVICE_PASS
)
# --- Example: Provision instance 6 with a specific UE static route ---
instance_id = 6
ue_ip_range = "10.0.6.0/24" # The specified UE bandwidth
# Execute
success = provisioner.apply_full_config(instance_id, ue_ip_range, debug=True)
print("\n--- Standalone test finished ---")
if success:
print("Result: Success")
else:
print("Result: Failure")
次に、このプロバイダークラスを利用してHTTPリクエストを受け付けるFastAPIアプリケーションを実装します。
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel, Field
import os
# Import the class from the file
from config_provisioner import CiscoProvisionerNcclient
# --- Initialize FastAPI application ---
app = FastAPI(
title="Cisco Provisioning API",
description="API to provision customer configurations on a Cisco IOS-XE device",
version="1.0.0",
)
# --- Device Connection Information ---
# In a production environment, it is strongly recommended to load these from environment variables or a config file.
DEVICE_ADDRESS = os.getenv("DEVICE_ADDRESS", "192.168.1.97")
DEVICE_PORT = int(os.getenv("DEVICE_PORT", 830))
DEVICE_USER = os.getenv("DEVICE_USER", "apigw")
DEVICE_PASS = os.getenv("DEVICE_PASS", "cisco")
# --- API Request Data Model Definition ---
class CustomerProvisionRequest(BaseModel):
instance_number: int = Field(
...,
gt=0,
lt=10,
description="Instance number to identify the customer (1-9)",
examples=[6]
)
ue_bandwidth: str = Field(
...,
description="Network address for the UE bandwidth (CIDR format)",
examples=["10.0.6.0/24"]
)
# --- API Endpoint Definition ---
@app.post("/provision/customer", tags=["Provisioning"])
async def create_customer_provisioning(request: CustomerProvisionRequest):
"""
Provisions a new customer configuration (VRF, Sub-IF, BGP, Static Route) on the device.
"""
# Instantiate the device provisioner class
provisioner = CiscoProvisionerNcclient(
address=DEVICE_ADDRESS,
port=DEVICE_PORT,
username=DEVICE_USER,
password=DEVICE_PASS
)
# Execute the configuration deployment method
success = provisioner.apply_full_config(
instance_number=request.instance_number,
ue_static_route_cidr=request.ue_bandwidth,
debug=True # Set to True to print the XML to the server log
)
if success:
return {
"status": "success",
"message": "Configuration for instance {} was applied successfully.".format(request.instance_number)
}
else:
# If the configuration fails, return a 500 Internal Server Error
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to apply configuration to the device. Check server logs for details."
)
UvicornサーバでAPIアプリケーションを起動し、curl
コマンドでPOSTリクエストを送信して動作を確認します。
# uvicorn main:app --reload
INFO: Will watch for changes in these directories: ['~/yang-cisco-2025/ncclient']
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [93801] using WatchFiles
INFO: Started server process [93803]
INFO: Waiting for application startup.
INFO: Application startup complete.
# curl -X 'POST' \
'http://127.0.0.1:8000/provision/customer' \
-H 'Content-Type: application/json' \
-d '{
"instance_number": 6,
"ue_bandwidth": "10.0.6.0/24"
}' | jq
{
"status": "success",
"message": "Configuration for instance 6 was applied successfully."
}
FastAPIはOpenAPI仕様に基づいたインタラクティブなAPIドキュメント(Swagger UI)を自動生成します。指定されたURLにアクセスすることで、ブラウザからAPIの仕様確認やテスト実行が可能です。
http://12.0.0.1:8000/docs/
API呼び出し後、CEルータにログインし、意図したコンフィグレーションが正しく適用されていることを確認します。
CE#sh run | i ip route vrf VRF006
ip route vrf VRF006 10.0.6.0 255.255.255.0 100.100.6.2
CE#sh run int gi1.106 | b int
interface GigabitEthernet1.106
encapsulation dot1Q 106
vrf forwarding VRF006
ip address 100.100.6.1 255.255.255.0
end
CE#sh run | b address-family ipv4 vrf VRF006
address-family ipv4 vrf VRF006
network 10.0.6.0 mask 255.255.255.0
exit-address-family
CE#sh ip route vrf VRF006 | b ^Gat
Gateway of last resort is not set
10.0.0.0/24 is subnetted, 1 subnets
S 10.0.6.0 [1/0] via 100.100.6.2
100.0.0.0/8 is variably subnetted, 2 subnets, 2 masks
C 100.100.6.0/24 is directly connected, GigabitEthernet1.106
L 100.100.6.1/32 is directly connected, GigabitEthernet1.106
CE#sh ip bgp vpnv4 vrf VRF006 reg ^$
BGP table version is 123, local router ID is 1.1.1.1
Status codes: s suppressed, d damped, h history, * valid, > best, i - internal,
r RIB-failure, S Stale, m multipath, b backup-path, f RT-Filter,
x best-external, a additional-path, c RIB-compressed,
t secondary path, L long-lived-stale,
Origin codes: i - IGP, e - EGP, ? - incomplete
RPKI validation codes: V valid, I invalid, N Not found
Network Next Hop Metric LocPrf Weight Path
Route Distinguisher: 65001:6 (default for vrf VRF006)
*> 10.0.6.0/24 100.100.6.2 0 32768 i
5. 本番運用に向けた考慮点
自動化システムを開発し、APIを公開することはゴールではありません。それを本番環境で安定して運用し続けるためには、オペレーションに関するいくつかの重要な課題を考慮する必要があります。自動化は、ネットワーク障害の性質を、偶発的な「ヒューマンエラー」から、自動化コードの論理バグに起因する「システム的な障害」へと変化させます。この新しいリスクを管理するためには、ソフトウェア開発の規律をネットワーク運用に導入することが必要となります。
5.1 差分検知と同期
ドリフトの問題: 自動化システムが稼働している環境で、何者かが緊急対応などの理由で手動でルータの設定を変更した場合、SoT(Source of Truth)が保持する「意図した状態」と、デバイス上の「実際の状態」との間に乖離(ドリフト)が生じます。このドリフトは、将来の自動化処理の失敗や、予期せぬ動作の原因となります。
リコンサイルループ(調整ループ): この問題に対処するためには、継続的に状態を監視し、同期を保つ「リコンサイルループ」を実装します。これは、以下の処理を定期的に実行するプロセスです。
- SoT(DB)から意図した状態を読み込む。
- ネットワークデバイスからNETCONF
<get-config>
を用いて実際の状態を読み込む。 - 両者を比較し、差分を検出する。
- 差分が存在する場合、オペレータに警告を通知するか、あるいはSoTの状態が正であるとして、自動的にデバイスの状態を修正する。
5.2 ログ管理と監査
おそらく、最近はどこの会社でもISO関係の準拠が求められます。コンプライアンスやセキュリティ要件を満たすためには、誰が、いつ、何を、なぜ変更したのかを追跡できる、完全な監査証跡が必要となります。
-
包括的なロギング: 以下の情報を全て関連付けてログに記録するべきです。
- APIへのリクエスト情報(リクエスト元IP、ユーザー、リクエストボディ)。
- 生成されたNETCONFペイロード(送信前のXML)。
- デバイスからのRPC応答(
<rpc-reply>
)。 - SoTデータベースへの変更記録。
- 改ざん防止: ログの信頼性を高めるため、リクエスト情報から計算したハッシュ値をログエントリに埋め込むことで、改ざんを検知可能な監査証跡を構築できます。これは、特に規制の厳しい業界において重要な要件です。
5.3 セキュリティ
自動化システムは、ネットワーク全体を操作できる強力な権限を持つため、そのセキュリティ確保は最優先事項です。
- アクセス制御: FastAPIエンドポイントには、OAuth2などの認証・認可メカニズムを実装し、許可されたユーザーやシステムのみがAPIを呼び出せるように制御します。ロールベースのアクセス制御(RBAC)を導入し、例えば、あるユーザーは参照(GET)のみ、別のユーザーは作成・削除(POST/DELETE)も可能、といった細かい権限設定を行うべきです。
- クレデンシャル管理: デバイスのSSH認証情報(パスワードや秘密鍵)をコードや設定ファイルにハードコーディングすることは避けるべきです。HashiCorp VaultやAWS Secrets Managerのような、専用のシークレット管理システムを利用したり、実行時に動的に認証情報を取得する仕組みを構築します。
- トランスポートセキュリティ: RESTクライアントからAPIゲートウェイ、そしてAPIゲートウェイからルータまでの全ての通信経路は、それぞれHTTPSとSSHを用いて暗号化される必要があります。
6. まとめ
本記事では、単なるスクリプトを超えた、ネットワーク自動化システムの構築方法を、具体的なL3VPNプロビジョニングのユースケースを通して解説しました。
インテントベースのモデル駆動型アプローチは、YANGという標準化されたスキーマを用いることで、自動化ロジックとデバイス固有の実装を分離します。さらに、FastAPIを用いてこの自動化ロジックをAPIとして公開することで、ネットワーク機能を抽象化された「サービス」として提供できることを示しました。
今回実装した機能はサービスプロビジョニングの一部にすぎませんが、今後はデプロビジョニング(削除)処理の実装、状態を管理するSoT(Source of Truth)データベースとの連携、そしてCI/CDパイプラインへの統合といった、より高度なトピックへと発展させていくことが考えられます。
Discussion