Некоторые люди говорят, что BGP — сложный протокол (я бы поспорил, особенно в сравнении с OSPF). Однако я ни разу не встречал никого, кто считал бы простым ACI, если, конечно, не брать в расчёт маркетинг. ACL и prefix‑lists являются частью программы CCNA; а вот контрактам ACI посвящён аж целый white paper. Долгое время для меня оставались загадкой детали реализации inter‑VRF контрактов. Не поймите меня неправильно — необходимая настройка подробно описана в документации, однако мне было подчас непонятно, почему нужны те или иные шаги. Сегодня я бы хотел поделиться парой‑тройкой своих находок по этой теме.
Топология весьма проста:
Host — это L3 коммутатор, который выполняет роль provider (VRF Provider) и consumer (VRF Consumer). В плане маршрутизации ACI выступает шлюзом по умолчанию:
Host# show run vrf Provider
interface Ethernet1/1.100
vrf member Provider
vrf context Provider
ip route 0.0.0.0/0 192.168.1.254
address-family ipv4 unicast
ip route 0.0.0.0/0 192.168.1.254 vrf Provider
Host# show ip interface brief vrf Provider
IP Interface Status for VRF " Provider "(47)
Interface IP Address Interface Status
Eth1/1.100 192.168.1.1 protocol-up/link-up/admin-up
Host# show run vrf Consumer
interface Ethernet1/2.100
vrf member Consumer
vrf context Consumer
ip route 0.0.0.0/0 192.168.2.254
address-family ipv4 unicast
ip route 0.0.0.0/0 192.168.2.254 vrf Consumer
Host# show ip interface brief vrf Consumer
IP Interface Status for VRF " Consumer "(48)
Interface IP Address Interface Status
Eth1/2.100 192.168.2.1 protocol-up/link-up/admin-up
Что касается ACI, нам нужны только пара EPG, BD и VRF в рамках одного tenant, а также некоторая настройка Access Policies.
Модуль Tenant:
resource "aci_tenant" "TestTenant" {
name = "TestTenant"
}
resource "aci_vrf" "TestVrf1" {
tenant_dn = aci_tenant.TestTenant.id
name = "TestVrf1"
}
resource "aci_vrf" "TestVrf2" {
tenant_dn = aci_tenant.TestTenant.id
name = "TestVrf2"
}
resource "aci_bridge_domain" "TestBD1" {
tenant_dn = aci_tenant.TestTenant.id
name = "TestBD1"
relation_fv_rs_ctx = aci_vrf.TestVrf1.id
}
resource "aci_subnet" "Subnet1" {
parent_dn = aci_application_epg.Provider.id
ip = "192.168.1.254/24"
scope = ["private", "shared"]
}
resource "aci_bridge_domain" "TestBD2" {
tenant_dn = aci_tenant.TestTenant.id
name = "TestBD2"
relation_fv_rs_ctx = aci_vrf.TestVrf2.id
}
resource "aci_subnet" "Subnet2" {
parent_dn = aci_bridge_domain.TestBD2.id
ip = "192.168.2.254/24"
scope = ["private", "shared"]
}
resource "aci_application_profile" "TestAP" {
tenant_dn = aci_tenant.TestTenant.id
name = "TestAP"
}
resource "aci_application_epg" "Provider" {
application_profile_dn = aci_application_profile.TestAP.id
name = "Provider"
relation_fv_rs_bd = aci_bridge_domain.TestBD1.id
}
resource "aci_application_epg" "Consumer" {
application_profile_dn = aci_application_profile.TestAP.id
name = "Consumer"
relation_fv_rs_bd = aci_bridge_domain.TestBD2.id
}
resource "aci_epg_to_domain" "ProviderDomain" {
application_epg_dn = aci_application_epg.Provider.id
tdn = aci_physical_domain.PhysicalDomain.id
}
resource "aci_epg_to_domain" "ConsumerDomain" {
application_epg_dn = aci_application_epg.Consumer.id
tdn = aci_physical_domain.PhysicalDomain.id
}
Модуль Access Policies:
resource "aci_vlan_pool" "TestPool" {
name = "TestPool"
alloc_mode = "static"
}
resource "aci_ranges" "range_1" {
vlan_pool_dn = aci_vlan_pool.TestPool.id
from = "vlan-1"
to = "vlan-1000"
alloc_mode = "static"
}
resource "aci_physical_domain" "PhysicalDomain" {
name = "PhysicalDomain"
relation_infra_rs_vlan_ns = aci_vlan_pool.TestPool.id
}
resource "aci_attachable_access_entity_profile" "TestAAEP" {
name = "TestAAEP"
}
resource "aci_aaep_to_domain" "PhysicalDomain-to-TestAAEP" {
attachable_access_entity_profile_dn = aci_attachable_access_entity_profile.TestAAEP.id
domain_dn = aci_physical_domain.PhysicalDomain.id
}
resource "aci_leaf_interface_profile" "TestInterfaceProfile" {
name = "TestInterfaceProfile"
}
resource "aci_access_port_block" "TestAccessBlockSelector" {
access_port_selector_dn = aci_access_port_selector.TestAccessPortSelector.id
name = "TestAccessBlockSelector"
from_card = "1"
from_port = "2"
to_card = "1"
to_port = “2"
}
resource "aci_access_port_selector" "TestAccessPortSelector" {
leaf_interface_profile_dn = aci_leaf_interface_profile.TestInterfaceProfile.id
name = "TestAccessPortSelector"
access_port_selector_type = "range"
relation_infra_rs_acc_base_grp = aci_leaf_access_port_policy_group.TestAccessInterfacePolicy.id
}
resource "aci_leaf_access_port_policy_group" "TestAccessInterfacePolicy" {
name = "TestAccessInterfaceProfile"
relation_infra_rs_att_ent_p = aci_attachable_access_entity_profile.TestAAEP.id
}
resource "aci_leaf_profile" "TestSwitchProfile" {
name = "TestSwitchProfile"
leaf_selector {
name = "LeafSelector"
switch_association_type = "range"
node_block {
name = "Block1"
from_ = "101"
to_ = "102"
}
}
relation_infra_rs_acc_port_p = [aci_leaf_interface_profile.TestInterfaceProfile.id]
}
Подсеть, в которой находится provider, должна быть задана в EPG вместо BD. Поскольку мы используем разные EPG, нужно определить контракт, чтобы установить между ними связность.
Модуль Contract:
resource "aci_application_epg" "Provider" {
application_profile_dn = aci_application_profile.TestAP.id
name = " Provider"
relation_fv_rs_bd = aci_bridge_domain.TestBD1.id
relation_fv_rs_prov = [aci_contract.TestContract.id]
}
resource "aci_application_epg" "Consumer" {
application_profile_dn = aci_application_profile.TestAP.id
name = " Consumer"
relation_fv_rs_bd = aci_bridge_domain.TestBD2.id
relation_fv_rs_cons = [aci_contract.TestContract.id]
}
resource "aci_contract" "TestContract" {
tenant_dn = aci_tenant.TestTenant.id
name = "TestContract"
scope = "tenant"
}
resource "aci_contract_subject" "TestSubject" {
contract_dn = aci_contract.TestContract.id
name = "TestSubject"
}
resource "aci_contract_subject_filter" "PermitIPSubj" {
contract_subject_dn = aci_contract_subject.TestSubject.id
filter_dn = aci_filter.PermitIPFilter.id
}
resource "aci_filter" "PermitIPFilter" {
tenant_dn = aci_tenant.TestTenant.id
name = "PermitIPFilter"
}
resource "aci_filter_entry" "PermitIPFilterEntry" {
filter_dn = aci_filter.PermitIPFilter.id
name = "permit_ip "
ether_t = "ip"
}
Как только мы применим этот контракт, Consumer сможет общаться с Provider:
Host# traceroute 192.168.1.1 vrf Consumer
traceroute to 192.168.1.1 (192.168.1.1), 30 hops max, 40 byte packets
1 192.168.2.254 (192.168.2.254) 1.946 ms 0.758 ms 0.691 ms
2 192.168.1.254 (192.168.1.254) 2.231 ms 0.708 ms 0.705 ms
3 192.168.1.1 (192.168.1.1) 0.708 ms 0.577 ms 0.578 ms
На данном этапе настройки корректны, поэтому мы можем перейти к наблюдениям. Почему необходимо определять подсеть provider в настройках EPG, а не BD? В классической настройке L3VPN нет подобного требования, поэтому, должно быть, оно относится сугубо к ACI. Рассмотрим, как именно происходит маршрутизация трафика:
leaf-102# show ip route vrf TestTenant:TestVrf2
<output omitted>
192.168.1.0/24, ubest/mbest: 1/0, attached, direct, pervasive
*via 10.0.88.66%overlay-1, [1/0], 00:07:29, static, tag 4294967294
192.168.2.0/24, ubest/mbest: 1/0, attached, direct, pervasive, dcs
*via 10.0.88.66%overlay-1, [1/0], 00:11:01, static, tag 4294967294
192.168.2.254/32, ubest/mbest: 1/0, attached, pervasive
*via 192.168.2.254, Vlan11, [0/0], 00:11:01, local, local
leaf-102#
leaf-102# show ip route vrf TestTenant:TestVrf2 192.168.1.0/24 det
<output omitted>
192.168.1.0/24, ubest/mbest: 1/0, attached, direct, pervasive
*via 10.0.88.66%overlay-1, [1/0], 00:07:38, static, tag 4294967294
recursive next hop: 10.0.88.66/32%overlay-1
vrf crossing information: VNID:0x238000 ClassId:0x2ab4 Flush#:0x1
Обратите внимание, что подсеть Provider доступна через статический маршрут с парой необычных атрибутов. Во-первых, next-hop – это адрес anycast IP for IPv4 hardware proxy:
spine-201# show ip interface lo9
IP Interface Status for VRF "overlay-1"
lo9, Interface status: protocol-up/link-up/admin-up, iod: 81, mode: anycast-v4
IP address: 10.0.88.66, IP subnet: 10.0.88.66/32
IP broadcast address: 255.255.255.255
IP primary address route-preference: 0, tag: 0
Чтобы proxy обработал пакет в правильном VRF, consumer leaf переписывает VNID так, чтобы тот попал в provider VRF (0x238000 = 2326528):
Оффтоп: для NX‑OS VXLAN характерно ровно противоположное поведение, если не брать в расчёт downstream VNI.
Inter‑VRF контракт ВСЕГДА применяет именно consumer leaf. Однако такой подход должен бы сломать conversation‑based forwarding: consumer начинает транзакцию, поэтому он не может предварительно получить пакет от provider, чтобы запомнить его pcTag. Решение очевидно: consumer должен заранее знать pcTag, относящийся к provider. Именно этот факт отражается в виде необходимости настраивать подсеть provider в EPG: как только контракт становится активен, APIC настраивает на consumer leaf статический маршрут с перезаписью VNID и provider pcTag, который в RIB называется ClassID (0×2ab4 = 10 932):
В результате consumer leaf обладает всей необходимой информацией, чтобы передать пакет в provider VRF и применить правильные политики:
leaf-102# show zoning-rule scope 2719744
+---------+--------+--------+----------+----------------+---------+---------+-------------------------+----------+------------------------+
| Rule ID | SrcEPG | DstEPG | FilterID | Dir | operSt | Scope | Name | Action | Priority |
+---------+--------+--------+----------+----------------+---------+---------+-------------------------+----------+------------------------+
| 4101 | 0 | 15 | implicit | uni-dir | enabled | 2719744 | | deny,log | any_vrf_any_deny(22) |
| 4100 | 0 | 0 | implarp | uni-dir | enabled | 2719744 | | permit | any_any_filter(17) |
| 4099 | 0 | 0 | implicit | uni-dir | enabled | 2719744 | | deny,log | any_any_any(21) |
| 4098 | 0 | 49153 | implicit | uni-dir | enabled | 2719744 | | permit | any_dest_any(16) |
| 4102 | 10932 | 49154 | 4 | uni-dir-ignore | enabled | 2719744 | TestTenant:TestContract | permit | fully_qual(7) |
| 4103 | 49154 | 10932 | 4 | bi-dir | enabled | 2719744 | TestTenant:TestContract | permit | fully_qual(7) |
| 4104 | 10932 | 0 | implicit | uni-dir | enabled | 2719744 | | deny,log | shsrc_any_any_deny(12) |
+---------+--------+--------+----------+----------------+---------+---------+-------------------------+----------+------------------------+
Что насчёт обратного трафика от provider EPG?
leaf-101# show ip route vrf TestTenant:TestVrf1 192.168.2.0/24 det
<output omitted>
192.168.2.0/24, ubest/mbest: 1/0, attached, direct, pervasive
*via 10.0.88.66%overlay-1, [1/0], 00:01:13, static, tag 4294967294
recursive next hop: 10.0.88.66/32%overlay-1
vrf crossing information: VNID:0x298000 ClassId:0 Flush#:0
Можно догадаться, что мы найдём похожий маршрут для consumer EPG:
Он указывает на Anycast IPv4 hardware proxy address.
Он задаёт корректной значение для перезаписи VNID.
Однако ClassID равен нулю. Означает ли это, что provider leaf не применяет политику? Действительно:
leaf-101# show zoning-rule scope 2326528
+---------+--------+--------+----------+---------+---------+---------+------+----------+----------------------+
| Rule ID | SrcEPG | DstEPG | FilterID | Dir | operSt | Scope | Name | Action | Priority |
+---------+--------+--------+----------+---------+---------+---------+------+----------+----------------------+
| 4101 | 0 | 16387 | implicit | uni-dir | enabled | 2326528 | | permit | any_dest_any(16) |
| 4098 | 0 | 0 | implicit | uni-dir | enabled | 2326528 | | deny,log | any_any_any(21) |
| 4099 | 0 | 0 | implarp | uni-dir | enabled | 2326528 | | permit | any_any_filter(17) |
| 4100 | 0 | 15 | implicit | uni-dir | enabled | 2326528 | | deny,log | any_vrf_any_deny(22) |
| 4102 | 10932 | 14 | implicit | uni-dir | enabled | 2326528 | | permit_override | src_dst_any(9) |
+---------+--------+--------+----------+---------+---------+---------+------+----------+----------------------+
Ненулевые значения pcTag в zoning table либо являются зарезервированными, либо относятся к BD:
Декодирование остальных записей в zoning table я оставлю в качестве упражнения (вначале можно изучить этот раздел).
Стоит подчеркнуть, что inter‑VRF трафик не попадает под endpoint learning ни для одного из направлений передачи. Такой подход позволяет заставить leaf‑коммутаторы всегда использовать статический маршрут, а значит, применять правильные политики и корректно переписывать VNID. Тут есть неочевидное следствие: inter‑VRF трафик всегда проходит через spine, даже если provider и consumer подключены к одному и тому же leaf.
Я надеюсь, теперь очевидно, что ACI — это очень сложная система с множеством внутренних нюансов. Впрочем, это не является негативной характеристикой; в конце концов, компьютеры включают в себя существенно больше элементов, чем стрелы из Каменного века. Однако для операторов ACI это неплохой повод помнить про сложность системы в целом, а также придерживаться опубликованных рекомендаций, предварительно протестировав всё на соответствие требованиям функциональности и производительности. В противном случае можно оказаться в серой зоне, что потенциально приведёт к малоприятной необходимости переделывать дизайн всей системы с нуля.
Спасибо за рецензию: Анастасии Куралёвой
Канал в Телеграме: https://t.me/networking_it_ru