CML2.8 Free Tierを使いこなす: IOLを用いてEVPN-VXLANを検証してみた

## はじめに

:::note info
本記事は、シスコの有志による Cisco Systems Japan Advent Calendar 2024 シリーズ 2 の25日目として投稿しています。
:::

2017年版: https://qiita.com/advent-calendar/2017/cisco
2018年版: https://qiita.com/advent-calendar/2018/cisco
2019年版: https://qiita.com/advent-calendar/2019/cisco
2020年版: https://qiita.com/advent-calendar/2020/cisco
2020年版(2枚目): https://qiita.com/advent-calendar/2020/cisco2
2021年版: https://qiita.com/advent-calendar/2021/cisco
2021年版(2枚目): https://qiita.com/advent-calendar/2021/cisco2
2022年版(1,2): https://qiita.com/advent-calendar/2022/cisco
2023年版: https://qiita.com/advent-calendar/2023/cisco
2024年版: https://qiita.com/advent-calendar/2024/cisco

先日、Cisco Modeling Labs(CML)の最新バージョンCML 2.8がリリースされました。今回のリリースでは無償版（Free Tier）が公開され、より多くの方々がCMLの魅力を体験できるようになりました。

昨年、@kykanno さんの[記事](https://qiita.com/kykanno/items/41ba540bf635d97e3f3b)では、CML 2を用いてベータ版 Catalyst 9000vでEVPN VXLANの設定と検証を行う方法が紹介されました。本記事ではその内容の続編として、CML2でIOL-L2を使用してEVPN VXLANの設定と検証を行い、さらにAnsibleを用いてEVPN VXLANラボ展開の自動化を実現します。これにより、CML 2.8 Free Tierの可能性を最大限に引き出したいと思います。

## 事前準備

### CML2.8 Free Tierのインストール

@ecodrive さんの[記事]()ではCML2.8 Free Tierのインストール手順について詳しく説明されていますので、ぜひご参照ください

### CML2.8 Free Tierサービスの起動

以下のサービスを起動します。

```bash
sudo systemctl enable --now ssh.service
sudo systemctl enable --now virl2-patty.service
```

### Ansible実行環境の準備

今回、EVPN VXLAN Farbic構築の自動化を実現するため、Ansibleを使用します。

下記に、自動化シナリオで使用されるトポロジーを示します。

<img width="737" alt="ansible_lab_topology" src="https://user-images.githubusercontent.com/99259970/155182099-7e5d98f4-8e4e-4b01-96a8-30b9badc5be2.png">

今回使用するAnsible playbookなどについては [trustywolf/cml-free-evpn-vxlan](https://github.com/trustywolf/cml-free-evpn-vxlan) をご参照ください。

実際のCML2のトポロジー図はこちらです:

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129139/11723575-41ea-847c-b905-0b704c16b2a5.png)

AnsibleでCML2ラボを自動展開するため、手動での操作は不要です。

Ansible実行用のPCでは、以下の操作を実行し、実行環境の初期化を行います。

```bash
git clone https://github.com/trustywolf/cml-free-evpn-vxlan.git

cd cml-free-evpn-vxlan/dag

python3 -m venv .venv
source .venv/bin/activate
pip3 install -r requirements.txt

ansible-galaxy collection install cisco.cml
```

## CML2ラボの自動展開

ラボを自動展開するため、以下のPlaybookを実行します。

```bash
ansible-playbook cisco.cml.build -e wait='yes' \
-e cml_host=<cml_host> \
-e cml_username=<cml_username> \
-e cml_password=<cml_password> \
-e cml_lab="cml-iol-evpn-vxlan" \
-e cml_lab_file="{{ lookup('env', 'PWD') }}/../cml-iol-evpn-vxlan.yaml"
```

下図に示すように、ラボが展開され、Power onになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129139/8d6d5ea3-6fad-dfe4-26ae-0518998259e6.png)

同様に、ラボを削除する際に、以下のPlaybookを実行します:

```bash
ansible-playbook cisco.cml.clean -t erase \
-e cml_host=<cml_host> \
-e cml_username=<cml_username> \
-e cml_password=<cml_password> \
-e cml_lab="cml-iol-evpn-vxlan" \
-e cml_lab_file="{{ lookup('env', 'PWD') }}/../cml-iol-evpn-vxlan.yaml"
```

下図に示すように、ラボが削除されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129139/80aa7c18-5004-7605-7ad7-f44ddda56b18.png)

次に、[Ansible CML Collection](https://github.com/CiscoDevNet/ansible-cml)のdynamic inventory pluginの使用し、ラボのinventory情報を取得します:

```bash
touch cml.yml
```

```yaml
plugin: cisco.cml.cml_inventory
host: <cml_host>
username: <cml_username>
password: <cml_password>
validate_certs: "no"
lab: cml-iol-evpn-vxlan
group_tags: spine, leaf, host
```

```bash
% ansible-playbook cisco.cml.inventory -i cml.yml --limit=spine,leaf,host
SSL Verification disabled
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details

PLAY [cml_hosts] *********************************************************************************************************************************************************************

TASK [debug] *************************************************************************************************************************************************************************
ok: [S1] => {
    "msg": "Node: S1(ioll2-xe), State: BOOTED, Address: cml-free.example.com:2201"
}
ok: [S2] => {
    "msg": "Node: S2(ioll2-xe), State: BOOTED, Address: cml-free.example.com:2202"
}
ok: [L1] => {
    "msg": "Node: L1(ioll2-xe), State: BOOTED, Address: cml-free.example.com:2203"
}
ok: [L2] => {
    "msg": "Node: L2(ioll2-xe), State: BOOTED, Address: cml-free.example.com:2204"
}
ok: [H] => {
    "msg": "Node: H(ioll2-xe), State: BOOTED, Address: cml-free.example.com:2205"
}

PLAY RECAP ***************************************************************************************************************************************************************************
H                          : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
L1                         : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
L2                         : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
S1                         : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
S2                         : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```

最後に、疎通確認を行います:

```bash
% ansible -i cml.yml cml_hosts -m ping --limit=spine,leaf,host
SSL Verification disabled
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
S2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
L2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
H | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
S1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
L1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
```

ラボの各ノードがAnsibleのインベントリに追加され、疎通できるようになりました。
しかし、以下のコマンドを実行すると、

```bash
% ansible -i cml.yml cml_hosts --limit=spine,leaf -m cisco.ios.ios_command -a "commands='show ip int b'"
SSL Verification disabled
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
S2 | FAILED! => {
    "changed": false,
    "msg": "ssh connection failed: ssh connect failed: Permission denied"
}
S1 | FAILED! => {
    "changed": false,
    "msg": "ssh connection failed: ssh connect failed: Permission denied"
}
L1 | FAILED! => {
    "changed": false,
    "msg": "ssh connection failed: ssh connect failed: Permission denied"
}
L2 | FAILED! => {
    "changed": false,
    "msg": "ssh connection failed: ssh connect failed: Permission denied"
}
```

SSH接続不可のエラーが出てしまいました。今回のラボ構成では、外部からラボのノードへSSH接続するため、PATty Toolを使用しPATの変換を行っていましたが、これが原因と考えられます。そこで、PAT変換の設定を削除し、CMLサーバをJump Hostとして利用することで、Ansibleからラボのノードへ接続を実現します。

`group_vars/all.yml`に以下の内容を追加します:

```yaml
ansible_ssh_common_args: '-o ProxyCommand="ssh -W %h:%p -q <cml_username>@<cml_host> -p 1122"'
```

疎通確認およびAnsibleの実行確認を行います:

```bash
% ansible -i inventory.yml all -m ping
S1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
L2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
L1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
S2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
```

```bash
% ansible -i inventory.yml all -m cisco.ios.ios_command -a "commands='show cdp nei'" 
L1 | SUCCESS => {
    "changed": false,
    "stdout": [
        "Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge\n                  S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, \n                  D - Remote, C - CVTA, M - Two-port Mac Relay \n\nDevice ID        Local Intrfce     Holdtme    Capability  Platform  Port ID\nH.lab            Eth 0/3           152             R S I  Linux Uni Eth 0/1\nS2.lab           Eth 0/2           178             R S I  Linux Uni Eth 0/1\nS1.lab           Eth 0/1           154             R S I  Linux Uni Eth 0/1\n\nTotal cdp entries displayed : 3"
    ],
    "stdout_lines": [
        [
            "Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge",
            "                  S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, ",
            "                  D - Remote, C - CVTA, M - Two-port Mac Relay ",
            "",
            "Device ID        Local Intrfce     Holdtme    Capability  Platform  Port ID",
            "H.lab            Eth 0/3           152             R S I  Linux Uni Eth 0/1",
            "S2.lab           Eth 0/2           178             R S I  Linux Uni Eth 0/1",
            "S1.lab           Eth 0/1           154             R S I  Linux Uni Eth 0/1",
            "",
            "Total cdp entries displayed : 3"
        ]
    ]
}
L2 | SUCCESS => {
    "changed": false,
    "stdout": [
        "Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge\n                  S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, \n                  D - Remote, C - CVTA, M - Two-port Mac Relay \n\nDevice ID        Local Intrfce     Holdtme    Capability  Platform  Port ID\nH.lab            Eth 0/3           174             R S I  Linux Uni Eth 0/2\nS2.lab           Eth 0/2           153             R S I  Linux Uni Eth 0/2\nS1.lab           Eth 0/1           154             R S I  Linux Uni Eth 0/2\n\nTotal cdp entries displayed : 3"
    ],
    "stdout_lines": [
        [
            "Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge",
            "                  S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, ",
            "                  D - Remote, C - CVTA, M - Two-port Mac Relay ",
            "",
            "Device ID        Local Intrfce     Holdtme    Capability  Platform  Port ID",
            "H.lab            Eth 0/3           174             R S I  Linux Uni Eth 0/2",
            "S2.lab           Eth 0/2           153             R S I  Linux Uni Eth 0/2",
            "S1.lab           Eth 0/1           154             R S I  Linux Uni Eth 0/2",
            "",
            "Total cdp entries displayed : 3"
        ]
    ]
}
S2 | SUCCESS => {
    "changed": false,
    "stdout": [
        "Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge\n                  S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, \n                  D - Remote, C - CVTA, M - Two-port Mac Relay \n\nDevice ID        Local Intrfce     Holdtme    Capability  Platform  Port ID\nL1.lab           Eth 0/1           152             R S I  Linux Uni Eth 0/2\nL2.lab           Eth 0/2           141             R S I  Linux Uni Eth 0/2\n\nTotal cdp entries displayed : 2"
    ],
    "stdout_lines": [
        [
            "Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge",
            "                  S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, ",
            "                  D - Remote, C - CVTA, M - Two-port Mac Relay ",
            "",
            "Device ID        Local Intrfce     Holdtme    Capability  Platform  Port ID",
            "L1.lab           Eth 0/1           152             R S I  Linux Uni Eth 0/2",
            "L2.lab           Eth 0/2           141             R S I  Linux Uni Eth 0/2",
            "",
            "Total cdp entries displayed : 2"
        ]
    ]
}
S1 | SUCCESS => {
    "changed": false,
    "stdout": [
        "Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge\n                  S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, \n                  D - Remote, C - CVTA, M - Two-port Mac Relay \n\nDevice ID        Local Intrfce     Holdtme    Capability  Platform  Port ID\nL1.lab           Eth 0/1           169             R S I  Linux Uni Eth 0/1\nL2.lab           Eth 0/2           164             R S I  Linux Uni Eth 0/1\n\nTotal cdp entries displayed : 2"
    ],
    "stdout_lines": [
        [
            "Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge",
            "                  S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, ",
            "                  D - Remote, C - CVTA, M - Two-port Mac Relay ",
            "",
            "Device ID        Local Intrfce     Holdtme    Capability  Platform  Port ID",
            "L1.lab           Eth 0/1           169             R S I  Linux Uni Eth 0/1",
            "L2.lab           Eth 0/2           164             R S I  Linux Uni Eth 0/1",
            "",
            "Total cdp entries displayed : 2"
        ]
    ]
}
```

ご覧の通り、Ansible実行環境の整備が問題なく完了しました。

## EVPN-VXLAN Farbicの自動構築

### Underlayのプロビジョニング

ループバックインターフェースと物理インターフェースの設定、スパインおよびリーフデバイス間のIS-ISおよびPIMプロトコルの設定を行います。その結果、スパインとリーフスイッチ間で完全なIP到達性が確立され、アンダーレイネットワークがマルチキャストルーティングをサポートするようになります。

ラボの各ノードのUnderlay設定は以下のファイルにより定義されます:

```bash
% ls host_vars/node_vars/
L1.yml  L2.yml  S1.yml  S2.yml
```

以下のPlaybookを実行することで、`preview_files`フォルダの下に`<hostname>-underlay.txt`のファイルが生成され、各ノードに適用するコンフィグを確認することが可能です。

```bash
ansible-playbook -i inventory.yml playbook_underlay_preview.yml
```

```bash
% ls preview_files/
L1-underlay.txt  L2-underlay.txt  S1-underlay.txt  S2-underlay.txt
```

各ファイルの内容を確認し、問題がない場合は、以下のPlaybookを実行することで、各ノードへコンフィグが適用されます。

```bash
ansible-playbook -i inventory.yml playbook_underlay_commit.yml
```

CMLの各ノードでshowコマンドを使用し、状態を確認します。

```cisco_ios
L1#show isis neighbors 

Tag UNDERLAY:
System Id       Type Interface     IP Address      State Holdtime Circuit Id
S1              L2   Et0/1         172.16.13.1     UP    22       02
S2              L2   Et0/2         172.16.23.2     UP    22       02

L1#show ip pim neighbor 
PIM Neighbor Table
Mode: B - Bidir Capable, DR - Designated Router, N - Default DR Priority,
      P - Proxy Capable, S - State Refresh Capable, G - GenID Capable,
      L - DR Load-balancing Capable
Neighbor          Interface                Uptime/Expires    Ver   DR
Address                                                            Prio/Mode
172.16.13.1       Ethernet0/1              00:00:57/00:01:16 v2    1 / S P G
172.16.23.2       Ethernet0/2              00:00:57/00:01:16 v2    1 / S P G
```

ご覧の通り、Underlayのプロビジョニングが無事完了しました。

### Overlayのプロビジョニング

Overlay設定は以下のファイルにより定義されます:

```bash
% cat group_vars/overlay_db.yml
```

以下のPlaybookを実行することで、Overlay設定のチェックを行います:

```bash
% ansible-playbook -i inventory.yml playbook_yml_validation.yml
```

また、以下のPlaybookを実行することで、Overlay設定を投入する前に、各NodeのUnderlayのチェックを行います:

```bash
% ansible-playbook -i inventory.yml playbook_overlay_precheck.yml
```

ここで、`version_license_check`のエラーが出ましたが、今回のラボ環境では特に影響がないため、無視しても問題ありません。

最後に、先ほどと同様に、各ノードへのコンフィグの確認および適用を行います。

```bash
% ansible-playbook -i inventory.yml playbook_overlay_preview.yml
```

```bash
% ls preview_files/ | grep overlay
L1-overlay.txt
L2-overlay.txt
S1-overlay.txt
S2-overlay.txt
```

```bash
% ansible-playbook -i inventory.yml playbook_overlay_commit.yml
```

### アクセスポートのプロビジョニング

最後に、Leafスイッチのアクセスポートのプロビジョニングを行います。

アクセスポートの設定は、以下のファイルで定義されています。

```bash
% ls host_vars/access_intf/
L1.yml  L2.yml
```

```bash
% ansible-playbook -i inventory.yml playbook_access_add_preview.yml
```

```bash
% ansible-playbook -i inventory.yml playbook_access_add_commit.yml
```

```bash
% ls preview_files/ | grep add
L1-add-intf.txt
L2-add-intf.txt
```

```bash
% ansible-playbook -i inventory.yml playbook_access_add_commit.yml
```

これにより、2台のLeafスイッチの`Ethernet0/3`ポートを、アクセスポートVLAN102に設定しました。

## 疎通確認

今回のラボでは、CML2.8 Free Tierの台数制限により、ホストを模擬する機器は1台しか用意できませんでした。そのため、ホスト模擬スイッチでVRFを使用することで、疑似的に2台のホストを作成します。

ホスト模擬スイッチに以下のコンフィグを投入します。

```cisco_ios
vrf definition H1
 !
 address-family ipv4
 exit-address-family
 !
 address-family ipv6
 exit-address-family
!
vrf definition H2
 !
 address-family ipv4
 exit-address-family
 !
 address-family ipv6
 exit-address-family
!
default interface Ethernet0/1
interface Ethernet0/1
 no switchport
 vrf forwarding H1
 ip address 10.1.102.201 255.255.255.0
 no shutdown
!
default interface Ethernet0/2
interface Ethernet0/2
 no switchport
 vrf forwarding H2
 ip address 10.1.102.202 255.255.255.0
 no shutdown
!
interface Ethernet0/3
shutdown
!
end
```

初期状態では、VRF `H1` のARPテーブルには VRF 'H2'所属の`Ethernet0/2`のアドレスとMACアドレスが存在しません。

```cisco_ios
H#show arp vrf H1
Protocol  Address          Age (min)  Hardware Addr   Type   Interface
Internet  10.1.102.201            -   aabb.cc00.6210  ARPA   Ethernet0/1
```

pingコマンドで疎通確認を行います:

```cisco_ios
H#ping vrf H1 10.1.102.202 source Ethernet0/1
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 10.1.102.202, timeout is 2 seconds:
Packet sent with a source address of 10.1.102.201 
.!!!!
Success rate is 80 percent (4/5), round-trip min/avg/max = 2/2/3 ms
```

pingで疎通確認後、VRF `H1` のARPテーブルにはVRF 'H2'所属の`Ethernet0/2`のアドレスとMACアドレスが無事に学習できました。

```
H#show arp vrf H1
Protocol  Address          Age (min)  Hardware Addr   Type   Interface
Internet  10.1.102.201            -   aabb.cc00.6210  ARPA   Ethernet0/1
Internet  10.1.102.202            0   aabb.cc00.6220  ARPA   Ethernet0/1
```

これにより、EVPN-VXLAN Farbicの正常性確認ができました。

## まとめ

本記事では、Cisco Modeling Labs (CML) 2.8 Free Tierを活用し、IOL-L2を用いたEVPN VXLANのラボ環境構築と、Ansibleによる自動化について解説しました。これらの手順を通して、CML 2.8 Free Tierのポテンシャルを最大限に引き出し、EVPN-VXLANラボの構築から自動化までの一連の流れを、実践的に学んでいただけると考えています。CML 2.8 Free Tier、EVPN-VXLAN、Ansibleに関心のある方々にとって、少しでも役立つ情報となれば幸いです。

## 参考資料

- [BGP EVPN VXLAN Configuration Guide, Cisco IOS XE 17.15.x (Catalyst 9300 Switches)
](https://www.cisco.com/c/en/us/td/docs/switches/lan/catalyst9300/software/release/17-15/configuration_guide/vxlan/b_1715_bgp_evpn_vxlan_9300_cg.html)
- [Campus EVPN Fabric Based on Cisco Catalyst 9000 Switches
](https://learningnetwork.cisco.com/s/article/campus-evpn-fabric-based-on-cisco-catalyst-9000-switches)
- [Cat9kEVPN/cat9k-evpn-ansible](https://github.com/Cat9kEVPN/cat9k-evpn-ansible)
- [CiscoDevNet/ansible-cml](https://github.com/CiscoDevNet/ansible-cml)
- [How to Use Ansible with CML](https://blogs.cisco.com/learning/how-to-use-ansible-with-cml)
- [Cisco CML 2.8 Free Tier上にSWを自動配置してAnsibleで設定したい！](https://qiita.com/recuraki/items/167b354310377f12d8d1)
- [IOS-XE Catalyst 9k（CML） で EVPN-VXLANを構築してみた](https://qiita.com/kykanno/items/41ba540bf635d97e3f3b)
- [Intel N97のミニPCを買った話（ついでにCML2.8 Free Tierをベアメタルにインストール）](https://qiita.com/ecodrive/items/5e69c74ad195804fff50)
- [Ansibleを使ってCMLでラボ構築](https://qiita.com/oigarash/items/ac0f3b1364e95ce5cd16)

## 免責事項

本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Web サイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。