Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

💙 Cisco ACI L4-L7 Device Terraform Module

Manage a Cisco ACI L4-L7 device — a service appliance registered into the policy model for service-graph insertion (class vnsLDevVip, DN uni/tn-{name}/lDevVip-{name}) — together with its concrete devices, concrete interfaces, logical interfaces, and cross-tenant imports, as a typed, secure-by-default building block targeting CiscoDevNet/aci ~> 2.20.

Terraform Provider Module Version Type Resources

🧩 Overview

This module manages an ACI L4-L7 device together with its cluster/interface topology as one coherent, secure-by-default unit:

  • 🧱 The L4-L7 device (aci_l4_l7_device.this) — the service-appliance registration in the ACI Management Information Tree (MIT), addressed by the Distinguished Name uni/tn-{tenant}/lDevVip-{name}.
  • 🖥️ Concrete devices (aci_concrete_device.this, for_each) — the cluster's physical or virtual members, keyed by a stable natural key.
  • 🔌 Concrete interfaces (aci_concrete_interface.this, for_each) — the data-plane interfaces on each concrete device, linked back to their member by a caller-supplied key.
  • 🧷 Logical interfaces (aci_l4_l7_logical_interface.this, for_each) — the service-graph-facing interface groups that bundle one or more concrete interfaces.
  • 🌉 Cross-tenant imports (aci_imported_logical_device.this, for_each) — references to this device created under other tenants, so a service graph elsewhere can consume it.
  • 🛡️ A conservative device posture by default — single-context, non-trunking, non-promiscuous, physical, GoTo function type, with no forced domain relations.
  • 🔑 Scope, not credentials — the device takes its parent tenant DN (tenant_dn) as a required input; authentication and the APIC URL are the caller's provider concern and are never module variables.

💡 Why it matters: the L4-L7 device is the anchor a service graph inserts between EPGs and contracts — a firewall, load balancer, or other appliance the fabric redirects traffic through. Getting its cluster topology (concrete devices, concrete interfaces, logical interfaces) right in one reviewable unit is what makes service-graph insertion deterministic and auditable, rather than a scattering of loosely-related resources across a configuration.

❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!

🗺️ Where this fits in the family

graph LR
  tenant["terraform-aci-tenant"]:::sib
  dev["terraform-aci-l4-l7-device (this module)"]:::this
  ldevvip["aci_l4_l7_device - class vnsLDevVip - DN uni/tn-{t}/lDevVip-{n}"]:::keystone
  cdev["aci_concrete_device - class vnsCDev (for_each)"]:::keystone
  cif["aci_concrete_interface - class vnsCIf (for_each)"]:::keystone
  lif["aci_l4_l7_logical_interface - class vnsLIf (for_each)"]:::keystone
  ldevif["aci_imported_logical_device - class vnsLDevIf (for_each)"]:::keystone
  physdom["terraform-aci-physical-domain"]:::sib
  vmmdom["terraform-aci-vmm-domain"]:::sib
  sgt["terraform-aci-l4-l7-service-graph-template"]:::sib
  othertenant["terraform-aci-tenant (target tenant)"]:::sib

  tenant -->|"tenant_dn"| dev
  dev -->|"manages"| ldevvip
  dev -->|"for_each concrete_devices"| cdev
  cdev -->|"for_each concrete_interfaces"| cif
  dev -->|"for_each logical_interfaces"| lif
  cif -->|"relation_to_concrete_interfaces"| lif
  dev -->|"for_each imports"| ldevif
  othertenant -->|"parent_dn"| ldevif
  physdom -->|"relation_to_physical_domain (by DN)"| dev
  vmmdom -->|"relation_to_vmm_domain (by DN)"| dev
  dev -->|"id (device DN)"| sgt

  classDef this fill:#00BCEB,color:#fff,stroke:#00BCEB;
  classDef keystone fill:#0D274D,color:#fff,stroke:#0D274D;
  classDef sib fill:#f5f5f5,color:#333,stroke:#cccccc;
Loading

The L4-L7 device sits under its parent tenant (via tenant_dn) and, optionally, binds to a physical or VMM domain by DN. It owns its concrete devices, their concrete interfaces, and its logical interfaces, and can be imported by reference into other tenants. It emits its id (the device DN) for a service-graph template to consume as the device selected for insertion.

🧬 What this module builds

graph TD
  tdn["tenant_dn (required)"]:::in
  devi["l4_l7_device object (posture + typed relations)"]:::in
  cd["concrete_devices map (for_each)"]:::in
  ci["concrete_interfaces map (for_each)"]:::in
  li["logical_interfaces map (for_each)"]:::in
  imp["imports map (for_each)"]:::in

  this["aci_l4_l7_device.this (keystone, vnsLDevVip)"]:::this
  cdevr["aci_concrete_device.this (for_each, vnsCDev)"]:::this
  cifr["aci_concrete_interface.this (for_each, vnsCIf)"]:::this
  lifr["aci_l4_l7_logical_interface.this (for_each, vnsLIf)"]:::this
  ldevifr["aci_imported_logical_device.this (for_each, vnsLDevIf)"]:::this

  oid["output: id (device DN)"]:::out
  ocd["output: concrete_device_dns (map)"]:::out
  oci["output: concrete_interface_dns (map)"]:::out
  oli["output: logical_interface_dns (map)"]:::out
  oim["output: import_dns (map)"]:::out

  tdn --> this
  devi --> this
  this --> oid
  cd --> cdevr
  this --> cdevr
  cdevr --> ocd
  ci --> cifr
  cdevr --> cifr
  cifr --> oci
  li --> lifr
  this --> lifr
  cifr --> lifr
  lifr --> oli
  imp --> ldevifr
  this --> ldevifr
  ldevifr --> oim

  classDef this fill:#00BCEB,color:#fff,stroke:#00BCEB;
  classDef in fill:#f5f5f5,color:#333,stroke:#cccccc;
  classDef out fill:#eeeeff,color:#333,stroke:#9999ff;
Loading

Resource inventory

Resource Name Cardinality Role
aci_l4_l7_device this 1 (keystone) The L4-L7 device / service-appliance registration (vnsLDevVip).
aci_concrete_device this 0..N (for_each over concrete_devices) Cluster members (vnsCDev).
aci_concrete_interface this 0..N (for_each over concrete_interfaces) Data-plane interfaces on a concrete device (vnsCIf).
aci_l4_l7_logical_interface this 0..N (for_each over logical_interfaces) Service-graph-facing interface groups (vnsLIf).
aci_imported_logical_device this 0..N (for_each over imports) Cross-tenant references to this device (vnsLDevIf).

✅ Provider / Versions

Requirement Value
Terraform >= 1.3.0 (uses optional() object defaults)
Provider CiscoDevNet/aci ~> 2.20
Provider block None in this module — the caller configures and authenticates the provider (username/password, X.509 signature, or login domain) out of band.
Scope Tenant-scoped — requires a parent tenant_dn.

Schema notes that bite (verified against the live provider schema):

  • 🔒 l4_l7_device.name is immutable. Changing it forces replacement of the device — and every concrete device/interface and logical interface under it. Treat renames as migrations.
  • ⚠️ The parent is wired through tenant_dn, not parent_dn. aci_l4_l7_device is a classic (SDKv2) resource whose live schema exposes only tenant_dn — there is no parent_dn attribute on this resource to prefer. This module wires the parent tenant through tenant_dn precisely because that is the current (not deprecated) attribute for this specific resource.
  • ℹ️ Boolean-style knobs are yes/no strings under the hood. active, is_copy, managed, promiscuous_mode, and trunking are surfaced as bool and rendered to the provider's string form in main.tf.
  • ⚠️ The VMM-domain relation is a genuine nested block, not a typed attribute. relation_vns_rs_al_dev_to_dom_p is a TypeSet block (max 1 item) — this module renders it with a dynamic block that emits zero or one instances, not an = assignment. The physical-domain relation (relation_vns_rs_al_dev_to_phys_dom_p) is, by contrast, a flat string attribute.
  • ⚠️ mode accepts only one live value. The provider's own validator confirms mode must be null (inherit the default) or "legacy-Mode" — no other value currently validates, despite the schema marking it computed.
  • ℹ️ Concrete interfaces and logical-interface bindings are cross-referenced by key, not by raw DN. concrete_interfaces[*].concrete_device_key and logical_interfaces[*].relation_to_concrete_interfaces reference keys in this module's own concrete_devices / concrete_interfaces maps — validated at plan time, resolved to DNs in main.tf.
  • ⚠️ validate_relation_dn (provider default true) fails apply if relation_to_physical_domain, relation_to_vmm_domain.domain_dn, or a concrete interface's relation_to_fabric_path points at an object that does not exist — create the referenced domain/path first, or in the same apply.

🔑 Required APIC Roles & Privileges

Scope the caller's APIC login to the least privilege this module needs:

  • Create / modify the device and its concrete devices, interfaces, and logical interfaces: the tenant-admin role (or a custom role with tenant-L4-L7-service-devices write privilege), scoped to the tenant's own security domain.
  • Referenced physical domain, VMM domain, and fabric paths: read privilege on each object named in relation_to_physical_domain, relation_to_vmm_domain, and concrete_interfaces[*].relation_to_fabric_path.
  • Cross-tenant imports: write privilege scoped to the target tenant's security domain (the tenant named by imports[*].parent_dn), in addition to read on this device.

The module never sees a credential — authentication is a provider/caller concern supplied out of band (e.g. ACI_USERNAME / ACI_PASSWORD, or ACI_PRIVATE_KEY / ACI_CERT_NAME for signature-based auth).

Cisco ACI Prerequisites

  • A reachable Cisco APIC (ACI_URL) whose version is compatible with the ~> 2.20 provider, with the provider configured and authenticated by the caller. In production, set insecure = false with proper CA trust — the provider's own default (insecure = true) is not a safe steady state.
  • The parent tenant (tenant_dn) already exists.
  • If relation_to_physical_domain is set, the referenced physical domain exists; if relation_to_vmm_domain is set, the referenced VMM domain exists — either way, so the provider's DN validation passes.
  • Any fabric path named in concrete_interfaces[*].relation_to_fabric_path (a leaf port, port channel, or vPC) is already configured.
  • Any tenant named in imports[*].parent_dn already exists.

📁 Module Structure

terraform-aci-l4-l7-device/
├── providers.tf     # terraform{} + required_providers (aci ~> 2.20); no provider block
├── variables.tf     # tenant_dn + l4_l7_device object + concrete_devices/concrete_interfaces/logical_interfaces/imports maps
├── main.tf          # aci_l4_l7_device.this (keystone) + four for_each child resource types
├── outputs.tf       # id (the device DN) first, then name and four child DN maps
├── README.md        # this document
├── SCOPE.md         # cross-module contract (scope, consumes/emits, roles, prerequisites)
├── LICENSE          # MIT
└── .gitignore       # canonical library ignore set

⚙️ Quick Start

# The caller configures the provider (authentication is out of band).
provider "aci" {
  # username / password, or private_key + cert_name for signature auth;
  # url = "https://apic.example.com"; set insecure = false in production.
}

module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn    = module.tenant.id # from terraform-aci-tenant
  l4_l7_device = { name = "fw-cluster-01" }
}

output "l4_l7_device_dn" {
  value = module.l4_l7_device.id # pass this to a service-graph-template module
}

🔌 Cross-Module Contract

Consumes

Input Type Typical source
tenant_dn string (DN) terraform-aci-tenant
l4_l7_device object({...}) caller (name + cluster/context posture + metadata tail + typed relations)
concrete_devices map(object({...})) caller
concrete_interfaces map(object({...})) caller (references concrete_devices by key)
logical_interfaces map(object({...})) caller (references concrete_interfaces by key)
imports map(object({...})) caller (parent_dn from another terraform-aci-tenant instance)

Emits

Output Description Consumed by
id L4-L7 device DN (uni/tn-{tenant}/lDevVip-{name}) — primary reference service-graph template (device selection), imports[*].logical_device
name Device name composition / audit
concrete_device_dns Map of concrete_devices key → concrete device DN audits / downstream reference
concrete_interface_dns Map of concrete_interfaces key → concrete interface DN audits / downstream reference
logical_interface_dns Map of logical_interfaces key → logical interface DN service-graph logical interface context
import_dns Map of imports key → imported logical device DN cross-tenant service-graph consumers

📚 Example Library

1 · Minimal — a physical device with secure defaults
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn    = module.tenant.id
  l4_l7_device = { name = "fw-cluster-01" }
}

💡 The minimal call creates a single-context, non-trunking, non-promiscuous physical GoTo device. No concrete devices, interfaces, or relations exist yet.

2 · A firewall service type
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  l4_l7_device = {
    name         = "fw-cluster-01"
    service_type = "FW"
  }
}

ℹ️ service_type describes the appliance's role to APIC (ADC, COPY, FW, NATIVELB, or OTHERS) — it informs service-graph function-node selection but does not itself change forwarding behavior.

3 · An active/active cluster
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  l4_l7_device = {
    name   = "adc-cluster-01"
    active = true
  }
}

⚠️ active = true puts the cluster in active/active mode instead of the default active/standby — confirm the appliance vendor supports active/active before enabling it.

4 · Binding to a physical domain
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  l4_l7_device = {
    name                        = "fw-cluster-01"
    relation_to_physical_domain = module.physical_domain.id
  }
}

ℹ️ relation_to_physical_domain is a flat DN relation to a phys:DomP object — the domain through which the fabric reaches a physical appliance's front/back interfaces.

5 · A virtual device bound to a VMM domain
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  l4_l7_device = {
    name        = "adc-vm-cluster-01"
    device_type = "VIRTUAL"
    relation_to_vmm_domain = {
      domain_dn = module.vmm_domain.id
    }
  }
}

⚠️ relation_to_vmm_domain is a genuine nested block in the provider (not a typed attribute) — the referenced VMM domain must exist so the provider's DN validation passes.

6 · AVE switching mode on the VMM-domain relation
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  l4_l7_device = {
    name        = "adc-vm-cluster-01"
    device_type = "VIRTUAL"
    relation_to_vmm_domain = {
      domain_dn      = module.vmm_domain.id
      switching_mode = "AVE"
    }
  }
}

⚠️ switching_mode = "AVE" requires an AVE-enabled VMM domain — setting it against a non-AVE domain fails at apply. Leave the default "native" unless AVE is specifically provisioned.

7 · One concrete device (cluster member)
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn    = module.tenant.id
  l4_l7_device = { name = "fw-cluster-01" }

  concrete_devices = {
    "fw-node-a" = {}
  }
}

💡 concrete_devices is a map keyed by a stable natural key — here the member name itself doubles as the key and, since name is left null, the resource name too.

8 · Two concrete devices for an HA pair
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn    = module.tenant.id
  l4_l7_device = { name = "fw-cluster-01" }

  concrete_devices = {
    "fw-node-a" = {}
    "fw-node-b" = {}
  }
}

ℹ️ Each map entry becomes its own aci_concrete_device under the device via for_each — order-independent, and safe to add or remove members without touching the others.

9 · A virtual concrete device with VM identity
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  l4_l7_device = {
    name        = "adc-vm-cluster-01"
    device_type = "VIRTUAL"
  }

  concrete_devices = {
    "adc-vm-01" = {
      vm_name           = "adc-vm-01"
      vmm_controller_dn = module.vmm_domain.controller_dn
    }
  }
}

ℹ️ vm_name / vmm_controller_dn apply only to virtual devices — they tell APIC which vCenter VM and controller back this concrete device.

10 · Concrete interfaces attached to fabric paths
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn    = module.tenant.id
  l4_l7_device = { name = "fw-cluster-01" }

  concrete_devices = {
    "fw-node-a" = {}
  }

  concrete_interfaces = {
    "fw-node-a-consumer" = {
      concrete_device_key     = "fw-node-a"
      relation_to_fabric_path = "topology/pod-1/paths-101/pathep-[eth1/1]"
    }
    "fw-node-a-provider" = {
      concrete_device_key     = "fw-node-a"
      relation_to_fabric_path = "topology/pod-1/paths-101/pathep-[eth1/2]"
    }
  }
}

💡 concrete_device_key must match a key in concrete_devices — the module validates this at plan time so a typo surfaces before apply, not as a provider-side DN error.

11 · A logical interface grouping concrete interfaces
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn    = module.tenant.id
  l4_l7_device = { name = "fw-cluster-01" }

  concrete_devices = {
    "fw-node-a" = {}
  }

  concrete_interfaces = {
    "fw-node-a-consumer" = {
      concrete_device_key     = "fw-node-a"
      relation_to_fabric_path = "topology/pod-1/paths-101/pathep-[eth1/1]"
    }
  }

  logical_interfaces = {
    "consumer-lif" = {
      relation_to_concrete_interfaces = ["fw-node-a-consumer"]
    }
  }
}

ℹ️ A logical interface's relation_to_concrete_interfaces is a list of keys into this module's own concrete_interfaces map — resolved to DNs and wired onto relation_vns_rs_c_if_att_n.

12 · Encapsulation and enhanced LAG on a logical interface
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn    = module.tenant.id
  l4_l7_device = { name = "fw-cluster-01" }

  logical_interfaces = {
    "consumer-lif" = {
      encap                    = "vlan-100"
      enhanced_lag_policy_name = "enhanced-lacp"
    }
  }
}

ℹ️ enhanced_lag_policy_name names an enhanced LAG policy for port-channel/vPC-attached members; leave it null for single-homed attachments.

13 · Importing the device into another tenant
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn    = module.shared_services_tenant.id
  l4_l7_device = { name = "shared-fw-cluster" }

  imports = {
    "app-tenant" = {
      parent_dn = module.app_tenant.id
    }
  }
}

🔒 Creating an imports entry requires write privilege in the target tenant's security domain (here, module.app_tenant's), not just read on the device's own tenant — scope the caller's login accordingly.

14 · User metadata via the imports annotations and tags lists
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn    = module.tenant.id
  l4_l7_device = { name = "fw-cluster-01" }

  imports = {
    "app-tenant" = {
      parent_dn = module.app_tenant.id
      annotations = {
        "cost-center" = "CC-4021"
      }
      tags = {
        "tier" = "gold"
      }
    }
  }
}

ℹ️ annotations / tags on an import entry are given as ergonomic { key = value } maps and rendered as the ACI tagAnnotation / tagTag {key, value} lists.

15 · 🏗️ End-to-end composition — tenant → physical domain → L4-L7 device → cluster → service graph
provider "aci" {
  # configured + authenticated by the caller; insecure = false in production
}

# 1) The tenant — the root everything nests under.
module "tenant" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-tenant.git?ref=v1.0.0"
  tenant = { name = "core-prod", description = "Core production tenant" }
}

# 2) The physical domain the firewall's front/back interfaces attach through.
module "physical_domain" {
  source          = "git::https://github.com/microsoftexpert/terraform-aci-physical-domain.git?ref=v1.0.0"
  physical_domain = { name = "fw-phys-dom" }
}

# 3) This module: the L4-L7 device, its cluster member, and its interfaces.
module "l4_l7_device" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  l4_l7_device = {
    name                        = "fw-cluster-01"
    service_type                = "FW"
    relation_to_physical_domain = module.physical_domain.id
  }

  concrete_devices = {
    "fw-node-a" = {}
  }

  concrete_interfaces = {
    "fw-node-a-consumer" = {
      concrete_device_key     = "fw-node-a"
      relation_to_fabric_path = "topology/pod-1/paths-101/pathep-[eth1/1]"
    }
    "fw-node-a-provider" = {
      concrete_device_key     = "fw-node-a"
      relation_to_fabric_path = "topology/pod-1/paths-101/pathep-[eth1/2]"
    }
  }

  logical_interfaces = {
    "consumer-lif" = { relation_to_concrete_interfaces = ["fw-node-a-consumer"] }
    "provider-lif"  = { relation_to_concrete_interfaces = ["fw-node-a-provider"] }
  }
}

# 4) A service-graph template that selects this device for insertion.
module "service_graph" {
  source                      = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-service-graph-template.git?ref=v1.0.0"
  tenant_dn                   = module.tenant.id
  service_graph_template      = { name = "web-to-app-fw" }
  l4_l7_device_dn             = module.l4_l7_device.id
}

output "l4_l7_device_dn" { value = module.l4_l7_device.id }

🏗️ One tenant and one physical domain in; a firewall cluster with its interface topology out — the service-graph template binds to it purely by consuming module.l4_l7_device.id. This module is the service-insertion anchor between the fabric's physical connectivity and the service graph that redirects contract traffic through it.

📥 Inputs

Name Type Required Default Description
tenant_dn string ✅ — Parent tenant DN (uni/tn-{name}); wired to the device's tenant_dn.
l4_l7_device object({...}) ✅ — The device: name, cluster/context posture, metadata tail, and typed relations.
concrete_devices map(object({...})) ➖ {} Cluster members under the device, keyed by a stable natural key.
concrete_interfaces map(object({...})) ➖ {} Interfaces under a concrete device, referencing it by key.
logical_interfaces map(object({...})) ➖ {} Service-graph-facing interface groups, referencing concrete interfaces by key.
imports map(object({...})) ➖ {} Cross-tenant references to this device, keyed by a stable natural key.
Full input schema (from variables.tf)
variable "tenant_dn" {
  type = string
  # validation: must match ^uni/tn- (a tenant DN)
}

variable "l4_l7_device" {
  type = object({
    name             = string                                    # REQUIRED, immutable (force-new), 1-64 chars
    active           = optional(bool, false)                     # Active/active cluster mode.
    context_aware    = optional(string, "single-Context")        # single-Context | multi-Context.
    device_type      = optional(string, "PHYSICAL")               # CLOUD | PHYSICAL | VIRTUAL.
    function_type    = optional(string, "GoTo")                   # GoThrough | GoTo | L1 | L2 | None.
    is_copy          = optional(bool, false)                      # Mark as a copy (tap/SPAN) device.
    managed          = optional(bool, true)                       # Managed via device package.
    mode             = optional(string, null)                     # null | "legacy-Mode".
    promiscuous_mode = optional(bool, false)                      # Promiscuous port for the VM.
    service_type     = optional(string, "OTHERS")                 # ADC | COPY | FW | NATIVELB | OTHERS.
    trunking         = optional(bool, false)                      # 802.1Q trunking on the port group.
    annotation       = optional(string, "orchestrator:terraform") # ACI annotation marker.
    name_alias       = optional(string, null)                     # GUI display alias.
    description      = optional(string, null)                     # Free-form description.
    relation_to_physical_domain = optional(string, null)          # Flat relation, phys:DomP DN.
    relation_to_vmm_domain = optional(object({                    # Nested-block relation (vnsRsALDevToDomP).
      domain_dn      = string
      switching_mode = optional(string, "native")                 # native | AVE.
    }), null)
  })
  # validations: name length/charset; every closed enum; mode tri-state; switching_mode enum
}

variable "concrete_devices" {
  type = map(object({
    name              = optional(string, null)                    # Defaults to the map key when null.
    annotation        = optional(string, "orchestrator:terraform")
    name_alias        = optional(string, null)
    description       = optional(string, null)
    vm_name           = optional(string, null)                    # Virtual devices only.
    vmm_controller_dn = optional(string, null)                    # Virtual devices only.
  }))
  default = {}
}

variable "concrete_interfaces" {
  type = map(object({
    concrete_device_key     = string                              # REQUIRED. Key into concrete_devices.
    name                    = optional(string, null)              # Defaults to the map key when null.
    annotation              = optional(string, "orchestrator:terraform")
    name_alias              = optional(string, null)
    description             = optional(string, null)
    encap                   = optional(string, null)               # e.g. "vlan-100".
    vnic_name               = optional(string, null)               # Virtual devices only.
    relation_to_fabric_path = optional(string, null)               # fabric:PathEp DN.
  }))
  default = {}
  # validation: concrete_device_key must exist in concrete_devices
}

variable "logical_interfaces" {
  type = map(object({
    name                             = optional(string, null)      # Defaults to the map key when null.
    annotation                       = optional(string, "orchestrator:terraform")
    name_alias                       = optional(string, null)
    description                      = optional(string, null)
    encap                            = optional(string, null)      # e.g. "vlan-100".
    enhanced_lag_policy_name         = optional(string, null)
    relation_to_concrete_interfaces  = optional(list(string), [])  # Keys into concrete_interfaces.
  }))
  default = {}
  # validation: each key must exist in concrete_interfaces
}

variable "imports" {
  type = map(object({
    parent_dn   = string                                          # REQUIRED. Target tenant DN.
    name        = optional(string, null)                          # Defaults to the map key when null.
    annotation  = optional(string, "orchestrator:terraform")
    name_alias  = optional(string, null)
    description = optional(string, null)
    annotations = optional(map(string), {})
    tags        = optional(map(string), {})
  }))
  default = {}
  # validation: parent_dn must match ^uni/tn-
}

🧾 Outputs

Output Description Notes
id L4-L7 device Distinguished Name (uni/tn-{tenant}/lDevVip-{name}) Primary cross-module reference — pass to a service-graph template or as imports[*].logical_device.
name Device name For composition / audit.
concrete_device_dns Map of concrete_devices key → concrete device DN For downstream reference / audit.
concrete_interface_dns Map of concrete_interfaces key → concrete interface DN For downstream reference / audit.
logical_interface_dns Map of logical_interfaces key → logical interface DN For service-graph logical interface context wiring.
import_dns Map of imports key → imported logical device DN For cross-tenant service-graph consumers.

🧠 Architecture Notes

  • One keystone, four child resource types. aci_l4_l7_device.this is the keystone; concrete devices, concrete interfaces, logical interfaces, and imports are each for_each over their own typed map, because none of them is meaningful without the device.
  • Two-level hierarchy flattened to two maps. Concrete interfaces belong under a concrete device, but Terraform's for_each cannot nest one resource inside another's iteration. This module flattens that hierarchy into two independently-keyed maps joined by a caller-supplied concrete_device_key, and validates the reference at plan time.
  • Classic (SDKv2) parent wiring. aci_l4_l7_device's live schema exposes only tenant_dn — there is no parent_dn attribute to prefer here, unlike migrated resources such as aci_bridge_domain.
  • A real nested block, not a typed attribute. relation_to_vmm_domain renders through a dynamic block against relation_vns_rs_al_dev_to_dom_p (a TypeSet, max 1) — a different shape from the migrated relation_to_* attributes seen elsewhere in this suite, and confirmed by inspecting the live schema's nesting rather than assumed from convention.
  • Boolean ergonomics. active, is_copy, managed, promiscuous_mode, and trunking are all surfaced as Terraform bool and rendered to the provider's "yes"/"no" strings in main.tf.
  • mode is effectively single-valued today. Only null or "legacy-Mode" validates against the live provider — the module's validation reflects that rather than a larger enum implied by the schema's computed marking.
  • Null-when-empty for lists. annotations / tags on imports entries are passed as null (not an empty list) when the caller supplies none, so the module never fights provider-computed state or produces a spurious diff.
  • Secure by default. The minimal call yields a single-context, non-trunking, non-promiscuous, physical GoTo device with no forced domain relation — nothing permissive is created until explicitly opted in.

🧱 Design Principles

Concern Secure default How to opt out (deliberately)
l4_l7_device.context_aware single-Context — bound to one VRF context Set multi-Context only when the device genuinely spans multiple VRFs.
l4_l7_device.trunking false — no 802.1Q trunking on the port group Enable explicitly for appliances that require trunked interfaces.
l4_l7_device.promiscuous_mode false — the VM's port is not promiscuous Enable only for appliances/designs that require it (e.g. some transparent inspection modes).
l4_l7_device.active false — active/standby, not active/active Enable only when the appliance vendor supports active/active clustering.
l4_l7_device.is_copy false — not a copy/tap device Enable for SPAN/tap-style insertion.
relation_to_physical_domain / relation_to_vmm_domain null — no domain relation forced Set explicitly to bind a physical or VMM domain.
l4_l7_device.annotation orchestrator:terraform — Terraform-managed objects stay identifiable in APIC Extend the marker; do not blank it.
imports[*].annotations / imports[*].tags {} — no user metadata, no spurious diffs Populate the maps explicitly.
Transport (provider) This suite instructs callers to set insecure = false with CA trust The provider default is insecure = true; do not keep it as a steady state.
Secrets None accepted or emitted n/a — this module carries no secret material; credentials are provider config.

🚀 Runbook

# From the module directory (offline, no credentials, no backend):
terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module by immutable tag: ?ref=v1.0.0 — never a branch.
  • This module is plan-only from the library's perspective. A human runs terraform plan / apply against a sub-production APIC from their own pipeline, with a login scoped to the permissions above. No cloud apply happens here.

🧪 Testing

The offline proof gate for this module:

  • ✅ terraform validate — parses the module, resolves the l4_l7_device, concrete_devices, concrete_interfaces, logical_interfaces, and imports object types, runs the name/enum/tri-state/cross-map-key validations, and confirms every argument exists in the provider schema.
  • ✅ terraform fmt -check — canonical formatting.
  • ⛔ Not exercised offline (only a real plan / apply against an APIC covers these): DN validation of relation_to_physical_domain, relation_to_vmm_domain, and relation_to_fabric_path (server-side validate_relation_dn), APIC-side name-collision checks, cross-tenant RBAC enforcement on imports, and the computed DNs returned as id and the child DN maps.

💬 Example Output

$ terraform output
id                      = "uni/tn-core-prod/lDevVip-fw-cluster-01"
name                    = "fw-cluster-01"
concrete_device_dns     = {
  "fw-node-a" = "uni/tn-core-prod/lDevVip-fw-cluster-01/cDev-fw-node-a"
}
concrete_interface_dns  = {
  "fw-node-a-consumer" = "uni/tn-core-prod/lDevVip-fw-cluster-01/cDev-fw-node-a/cIf-[fw-node-a-consumer]"
  "fw-node-a-provider" = "uni/tn-core-prod/lDevVip-fw-cluster-01/cDev-fw-node-a/cIf-[fw-node-a-provider]"
}
logical_interface_dns   = {
  "consumer-lif" = "uni/tn-core-prod/lDevVip-fw-cluster-01/lIf-consumer-lif"
  "provider-lif" = "uni/tn-core-prod/lDevVip-fw-cluster-01/lIf-provider-lif"
}
import_dns              = {}

🔍 Troubleshooting

Symptom Cause Fix
l4_l7_device.name must be 1-64 characters Name is empty or too long Use a 1-64 character name.
l4_l7_device.name may contain only letters, digits, and the characters _ . : - Name has spaces or unsupported characters Remove spaces/special characters (ACI naming rules).
Changing l4_l7_device.name wants to destroy/recreate the device name is immutable (force-new) Treat a rename as a migration; expect the device and its children to be replaced.
... must be one of: PHYSICAL, VIRTUAL, CLOUD (or similar enum error) A posture field is outside its closed enum Use one of the documented values for that field.
l4_l7_device.mode must be null ... or "legacy-Mode" mode was set to an unsupported value Leave mode unset (null) or set exactly "legacy-Mode".
concrete_device_key must match a key present in var.concrete_devices A typo in concrete_interfaces[*].concrete_device_key Match the key exactly as defined in concrete_devices.
relation_to_concrete_interfaces entry must match a key present in var.concrete_interfaces A typo in logical_interfaces[*].relation_to_concrete_interfaces Match the key exactly as defined in concrete_interfaces.
Apply fails validating relation_to_physical_domain / relation_to_vmm_domain / relation_to_fabric_path The referenced domain or fabric path does not exist yet Create the referenced object first (or in the same apply); do not disable validate_relation_dn.
Apply fails creating an imports entry The caller's login lacks write privilege in the target tenant Scope the provider login's write privilege to the tenant named by imports[*].parent_dn, not just this device's tenant.
Post ... 401 / authentication error Provider not configured or wrong credentials Configure the aci provider with valid credentials and url; prefer signature auth for automation.

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."

About

Terraform module: terraform-aci-l4-l7-device

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages