Manage a Cisco ACI L4-L7 device — a service appliance registered into the policy model for service-graph insertion (class
vnsLDevVip, DNuni/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 targetingCiscoDevNet/aci ~> 2.20.
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 Nameuni/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,
GoTofunction 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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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!
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;
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.
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;
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). |
| 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.nameis 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 throughtenant_dn, notparent_dn.aci_l4_l7_deviceis a classic (SDKv2) resource whose live schema exposes onlytenant_dn— there is noparent_dnattribute on this resource to prefer. This module wires the parent tenant throughtenant_dnprecisely because that is the current (not deprecated) attribute for this specific resource.- ℹ️ Boolean-style knobs are
yes/nostrings under the hood.active,is_copy,managed,promiscuous_mode, andtrunkingare surfaced asbooland rendered to the provider's string form inmain.tf. ⚠️ The VMM-domain relation is a genuine nested block, not a typed attribute.relation_vns_rs_al_dev_to_dom_pis aTypeSetblock (max 1 item) — this module renders it with adynamicblock 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.⚠️ modeaccepts only one live value. The provider's own validator confirmsmodemust benull(inherit the default) or"legacy-Mode"— no other value currently validates, despite the schema marking itcomputed.- ℹ️ Concrete interfaces and logical-interface bindings are cross-referenced by key, not by raw DN.
concrete_interfaces[*].concrete_device_keyandlogical_interfaces[*].relation_to_concrete_interfacesreference keys in this module's ownconcrete_devices/concrete_interfacesmaps — validated at plan time, resolved to DNs inmain.tf. ⚠️ validate_relation_dn(provider defaulttrue) fails apply ifrelation_to_physical_domain,relation_to_vmm_domain.domain_dn, or a concrete interface'srelation_to_fabric_pathpoints at an object that does not exist — create the referenced domain/path first, or in the same apply.
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-adminrole (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, andconcrete_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).
- A reachable Cisco APIC (
ACI_URL) whose version is compatible with the~> 2.20provider, with the provider configured and authenticated by the caller. In production, setinsecure = falsewith 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_domainis set, the referenced physical domain exists; ifrelation_to_vmm_domainis 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_dnalready exists.
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
# 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
}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 |
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
GoTodevice. 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_typedescribes the appliance's role to APIC (ADC,COPY,FW,NATIVELB, orOTHERS) — 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 = trueputs 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_domainis a flat DN relation to aphys:DomPobject — 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_domainis 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_devicesis a map keyed by a stable natural key — here the member name itself doubles as the key and, sincenameis 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_deviceunder the device viafor_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_dnapply 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_keymust match a key inconcrete_devices— the module validates this at plan time so a typo surfaces beforeapply, 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_interfacesis a list of keys into this module's ownconcrete_interfacesmap — resolved to DNs and wired ontorelation_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_namenames an enhanced LAG policy for port-channel/vPC-attached members; leave itnullfor 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
importsentry 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/tagson an import entry are given as ergonomic{ key = value }maps and rendered as the ACItagAnnotation/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.
| 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-
}| 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. |
- One keystone, four child resource types.
aci_l4_l7_device.thisis the keystone; concrete devices, concrete interfaces, logical interfaces, and imports are eachfor_eachover 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_eachcannot nest one resource inside another's iteration. This module flattens that hierarchy into two independently-keyed maps joined by a caller-suppliedconcrete_device_key, and validates the reference at plan time. - Classic (SDKv2) parent wiring.
aci_l4_l7_device's live schema exposes onlytenant_dn— there is noparent_dnattribute to prefer here, unlike migrated resources such asaci_bridge_domain. - A real nested block, not a typed attribute.
relation_to_vmm_domainrenders through adynamicblock againstrelation_vns_rs_al_dev_to_dom_p(aTypeSet, max 1) — a different shape from the migratedrelation_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, andtrunkingare all surfaced as Terraformbooland rendered to the provider's"yes"/"no"strings inmain.tf. modeis effectively single-valued today. Onlynullor"legacy-Mode"validates against the live provider — the module's validation reflects that rather than a larger enum implied by the schema'scomputedmarking.- Null-when-empty for lists.
annotations/tagsonimportsentries are passed asnull(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
GoTodevice with no forced domain relation — nothing permissive is created until explicitly opted in.
| 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. |
# 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/applyagainst a sub-production APIC from their own pipeline, with a login scoped to the permissions above. No cloud apply happens here.
The offline proof gate for this module:
- ✅
terraform validate— parses the module, resolves thel4_l7_device,concrete_devices,concrete_interfaces,logical_interfaces, andimportsobject 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/applyagainst an APIC covers these): DN validation ofrelation_to_physical_domain,relation_to_vmm_domain, andrelation_to_fabric_path(server-sidevalidate_relation_dn), APIC-side name-collision checks, cross-tenant RBAC enforcement onimports, and the computed DNs returned asidand the child DN maps.
$ 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 = {}
| 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. |
- Cisco ACI provider —
aci_l4_l7_device - Cisco ACI provider —
aci_concrete_device - Cisco ACI provider —
aci_concrete_interface - Cisco ACI provider —
aci_l4_l7_logical_interface - Cisco ACI provider —
aci_imported_logical_device - Cisco ACI provider — provider configuration & authentication
- Cisco APIC object model — classes
vnsLDevVip(L4-L7 device),vnsCDev(concrete device),vnsCIf(concrete interface),vnsLIf(logical interface), andvnsLDevIf(imported logical device). - Sibling modules:
terraform-aci-tenant,terraform-aci-physical-domain,terraform-aci-vmm-domain,terraform-aci-l4-l7-service-graph-template,terraform-aci-application-epg. - This module's
SCOPE.md— the cross-module contract.
💙 "Infrastructure as Code should be standardized, consistent, and secure."