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

  1. Он указывает на Anycast IPv4 hardware proxy address.

  2. Он задаёт корректной значение для перезаписи 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