Некоторые люди говорят, что 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