Manage a Cisco ACI leaf (switch) profile — the fabric access-policy object (class
infraNodeP, DNuni/infra/nprof-{name}) that groups the leaf selectors mapping physical switches to switch policy groups — together with its leaf selectors and node blocks, as a typed, secure-by-default building block targetingCiscoDevNet/aci ~> 2.20.
This module manages an ACI leaf (switch) profile together with its leaf selectors and their nested node blocks as one coherent, secure-by-default unit:
- 🗄️ The leaf profile (
aci_leaf_profile.this) — the fabric access-policy object, addressed by the Distinguished Nameuni/infra/nprof-{name}, that groups the leaf selectors covering a set of fabric leaf switches. - 🎯 Leaf selectors (switch associations) (
aci_leaf_selector.this) — one per entry in a typedleaf_selectorsmap, each declaring how its member nodes are matched (ALL,range, orALL_IN_POD) and, optionally, the switch policy group that configures them. - 🔢 Node blocks (node-ID ranges) (
aci_node_block.this) — nested under each selector, flattened into a stablefor_eachmap so a fabric's node membership (e.g. leaf IDs 101-104) is declared per selector without churning the rest of the set. - 🔗 Classic flat relations —
relation_infra_rs_acc_port_p/relation_infra_rs_acc_card_pon the keystone bind the profile to leaf interface / access-card profiles by DN;relation_infra_rs_acc_node_p_grpon each selector binds it to a switch policy group by DN. - 🏷️ The ACI metadata tail —
annotation(preserved asorchestrator:terraform),name_alias, anddescriptionon every object this module manages. - 🔑 Scope, not credentials — the leaf profile is a fabric access-policy object with no parent DN; authentication and the APIC URL are the caller's provider concern and are never module variables.
💡 Why it matters: the leaf profile is how ACI answers "which physical switches does this policy apply to?" Getting node-ID ranges and switch-policy-group bindings right — and keeping them declarative, typed, and individually addressable — is what lets interface and switch policy actually reach the correct hardware.
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
apic["Cisco APIC fabric (provider auth, out of band)"]:::ext
lsp["terraform-aci-leaf-switch-profile (this module)"]:::this
infra["aci_leaf_profile - class infraNodeP - DN uni/infra/nprof-{name}"]:::keystone
spg["terraform-aci-access-switch-policy-group"]:::sib
lip["terraform-aci-leaf-interface-profile"]:::sib
ipg["leaf interface policy group modules"]:::sib
apic -->|"provider configured by caller"| lsp
lsp -->|"manages"| infra
spg -->|"switch policy group DN via relation_to_switch_policy_group_dn"| lsp
lip -->|"leaf interface profile DN via relation_to_leaf_interface_profile_dns"| lsp
ipg -->|"interface selectors resolved through the interface profile"| lip
classDef this fill:#00BCEB,color:#fff,stroke:#00BCEB;
classDef keystone fill:#0D274D,color:#fff,stroke:#0D274D;
classDef sib fill:#f5f5f5,color:#333,stroke:#cccccc;
classDef ext fill:#eeeeff,color:#333,stroke:#9999ff;
The leaf profile is the node-membership half of the access-policy model: it declares which leaf switches
(by node-ID range or ALL/ALL_IN_POD) a switch policy group applies to. It takes no parent DN — it
composes directly under uni/infra — and consumes a switch policy group DN and, optionally, leaf interface
/ access-card profile DNs from sibling modules by reference.
graph TD
n["leaf_profile.name (required, immutable)"]:::in
meta["annotation / name_alias / description"]:::in
rel["relation_to_leaf_interface_profile_dns / relation_to_access_card_profile_dns"]:::in
this["aci_leaf_profile.this (keystone, infraNodeP)"]:::this
sel_in["leaf_selectors (typed map)"]:::in
sel["aci_leaf_selector.this (for_each, infraLeafS)"]:::child
blk_in["leaf_selectors[*].node_blocks (nested map)"]:::in
blk["aci_node_block.this (for_each, infraNodeBlk)"]:::child
oid["output: id (DN uni/infra/nprof-{name})"]:::out
onm["output: name"]:::out
osel["output: leaf_selector_dns"]:::out
oblk["output: node_block_dns"]:::out
n --> this
meta --> this
rel --> this
this --> oid
this --> onm
sel_in --> sel
this --> sel
sel --> osel
blk_in --> blk
sel --> blk
blk --> oblk
classDef this fill:#00BCEB,color:#fff,stroke:#00BCEB;
classDef child 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_leaf_profile |
this |
1 (keystone) | The leaf profile (infraNodeP), with its classic flat interface/access-card-profile relations. |
aci_leaf_selector |
this |
0-N (for_each) |
Leaf selectors / switch associations (infraLeafS) under the profile. |
aci_node_block |
this |
0-N (for_each, flattened) |
Node-ID range blocks (infraNodeBlk) nested under each selector. |
| 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 | None — the leaf profile is a fabric access-policy object under uni/infra (no parent DN). |
Schema notes that bite (verified against the live provider schema):
- 🔒
leaf_profile.nameis immutable. Changing it forces replacement — a brand-new profile, and every selector and node block nested under its DN goes with it. - ℹ️ All three resources are classic (SDKv2) — none is migrated to the plugin-framework in this provider line. There is no typed
relation_to_*alternative for any relation here:relation_infra_rs_acc_port_p/relation_infra_rs_acc_card_pon the keystone are flatset(string)DN lists, andrelation_infra_rs_acc_node_p_grpon each selector is a flatstringDN. ⚠️ The keystone also exposes inlineleaf_selector { node_block { ... } }blocks as an alternative shape for the identical child objects this module manages as dedicatedaci_leaf_selector/aci_node_blockresources. This module manages selectors and node blocks only through the dedicated child resources — do not add inlineleaf_selectorblocks to anaci_leaf_profile.thisthis module manages, or the two will fight over the same child objects.- ℹ️
switch_association_typeis a closed enum —ALL,range,ALL_IN_POD— confirmed against the live provider's own validator. It also encodes into the selector's DN (.../leaves-{selector}-typ-{type}), so changing it changes the selector's identity. - ℹ️
node_block.from_/to_are provider strings, not numbers, holding a node-ID range (1-16000, default"1"); this module validates the numeric range at plan time. - ℹ️ Node blocks are nested under
leaf_selectors[*].node_blocks, not a separate top-level variable — this avoids any dangling cross-map reference and mirrors the ACI hierarchy (a node block is meaningless without its owning selector). The module flattens the nesting internally into a stable"{selector_key}.{block_key}"for_eachkey. - ℹ️
idis the DN (uni/infra/nprof-{name}), computed by APIC at create. ⚠️ validate_relation_dn(provider defaulttrue) meansrelation_to_switch_policy_group_dnor any keystone relation pointing at a DN that does not yet exist will fail at apply — create the target first, or wire it by reference.
Scope the caller's APIC login to the least privilege this module needs:
- Create / modify a leaf profile, its selectors, and node blocks: the
access-adminrole (or a custom role with fabric access-policy write privilege), typically scoped to theallsecurity domain — access policies are fabric-wide constructs. - Referenced switch policy group: read on the group targeted by
relation_to_switch_policy_group_dn. - Referenced leaf interface / access-card profiles: read on any profile targeted by the keystone's flat relations.
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, the provider should be configured with
insecure = falseand proper CA trust — the provider's own default (insecure = true, skip TLS verification) is not a safe steady state. - Any switch policy group referenced by
relation_to_switch_policy_group_dnmust exist (or be created in the same configuration) so the provider's DN validation passes; likewise for any referenced leaf interface / access-card profiles. - No parent object is required — the leaf profile is created directly under
uni/infra. - The physical leaf switches a
rangeorALL_IN_PODselector's node blocks reference are already discovered and registered in the fabric — this module associates already-known node IDs with policy; it does not perform fabric membership/registration.
terraform-aci-leaf-switch-profile/
├── providers.tf # terraform{} + required_providers (aci ~> 2.20); no provider block
├── variables.tf # leaf_profile + leaf_selectors (with nested node_blocks) — typed, secure defaults
├── main.tf # aci_leaf_profile.this (keystone) + aci_leaf_selector.this + aci_node_block.this
├── outputs.tf # id (the DN) first, then name, leaf_selector_dns, node_block_dns
├── 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 "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
description = "Leaf profile for compute-rack switches 101-104"
}
leaf_selectors = {
"compute-rack" = {
switch_association_type = "range"
node_blocks = {
"101-104" = { from_ = "101", to_ = "104" }
}
}
}
}
output "leaf_profile_dn" {
value = module.leaf_switch_profile.id
}Consumes
| Input | Type | Typical source |
|---|---|---|
leaf_profile |
object({...}) |
caller (name + metadata tail + classic flat relations, by DN) |
leaf_selectors |
map(object({...})) |
caller (selector name + type + switch-policy-group DN + nested node_blocks) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Leaf profile DN (uni/infra/nprof-{name}) — primary reference |
downstream access-policy composition, audits |
name |
Leaf profile name | composition / audit |
leaf_selector_dns |
Map of selector key (name) → selector DN | audits / downstream reference |
node_block_dns |
Map of node block key ("{selector_key}.{block_key}") → node block DN |
audits / downstream reference |
1 · Minimal — a leaf profile with secure defaults
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
}
}💡 The minimal call creates only the leaf profile.
annotationis preserved asorchestrator:terraform, no selectors are created, and no relation is forced — nothing permissive by default.
2 · Description and GUI alias
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
name_alias = "Compute Rack Leafs"
description = "Covers leaf switches 101-104 in the compute rack"
}
}ℹ️
name_aliasis a display alias shown in the APIC GUI;nameremains the immutable identity encoded in the DN.
3 · A custom annotation marker
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
annotation = "orchestrator:terraform:platform-team"
}
}🔒 Keep the
orchestrator:terraformprefix so Terraform-managed objects stay identifiable in APIC. This suite defaultsannotationtoorchestrator:terraform; override it only to extend, not to erase, that marker.
4 · A single leaf selector with a node-ID range
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
}
leaf_selectors = {
"compute-rack" = {
switch_association_type = "range"
node_blocks = {
"101-104" = { from_ = "101", to_ = "104" }
}
}
}
}💡
leaf_selectorsis keyed by selector name; each selector'snode_blocksis a nested map keyed by block name, so the selector'snameand each block'snameare both derived from their map keys.
5 · Multiple disjoint node-ID ranges under one selector
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-storage"
}
leaf_selectors = {
"storage-nodes" = {
switch_association_type = "range"
node_blocks = {
"block-a" = { from_ = "201", to_ = "202" }
"block-b" = { from_ = "210", to_ = "212" }
}
}
}
}ℹ️ A single selector can cover several non-contiguous node-ID ranges by adding more entries to its
node_blocksmap.
6 · Multiple leaf selectors in one profile
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-dc1"
}
leaf_selectors = {
"compute-rack" = {
switch_association_type = "range"
node_blocks = { "101-104" = { from_ = "101", to_ = "104" } }
}
"storage-rack" = {
switch_association_type = "range"
node_blocks = { "201-202" = { from_ = "201", to_ = "202" } }
}
}
}💡 Each map entry is an independently addressable
aci_leaf_selector.this[...]resource — adding or removing a selector never churns the others.
7 · Selecting every leaf switch in a pod (ALL_IN_POD)
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-pod1-all"
}
leaf_selectors = {
"pod1-all-leafs" = {
switch_association_type = "ALL_IN_POD"
}
}
}
⚠️ ALL_IN_PODmatches every leaf switch in the pod without an explicit node-ID range —node_blocksmay be omitted. Prefer explicitrangeselectors for tightly-scoped policy; useALL_IN_PODdeliberately for fabric-wide policy.
8 · Selecting every leaf switch in the fabric (ALL)
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-fabric-wide"
}
leaf_selectors = {
"all-leafs" = {
switch_association_type = "ALL"
}
}
}
⚠️ ALLis the broadest possible match — every leaf switch in the fabric. Reserve it for genuinely fabric-wide policy (e.g. a baseline monitoring or NTP switch policy group); preferrangefor anything workload-specific.
9 · Binding a selector to a switch policy group
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
}
leaf_selectors = {
"compute-rack" = {
switch_association_type = "range"
relation_to_switch_policy_group_dn = "uni/infra/funcprof/accnodepgrp-compute-leaf-pg"
node_blocks = {
"101-104" = { from_ = "101", to_ = "104" }
}
}
}
}
⚠️ The referenced switch policy group must exist (or be created in the same apply). With the provider'svalidate_relation_dndefault oftrue, a dangling relation fails at apply — fix the missing group rather than disabling validation.
10 · Wiring a switch-policy-group module's DN into a selector
module "access_switch_policy_group" {
source = "git::https://github.com/microsoftexpert/terraform-aci-access-switch-policy-group.git?ref=v1.0.0"
access_switch_policy_group = { name = "compute-leaf-pg" }
}
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
}
leaf_selectors = {
"compute-rack" = {
switch_association_type = "range"
relation_to_switch_policy_group_dn = module.access_switch_policy_group.id
node_blocks = {
"101-104" = { from_ = "101", to_ = "104" }
}
}
}
}💡 Reference a sibling switch-policy-group module's
idoutput directly rather than hardcoding the DN.
11 · Binding the profile to a leaf interface profile
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
relation_to_leaf_interface_profile_dns = [
"uni/infra/accportprof-compute-leaf-ifprof"
]
}
leaf_selectors = {
"compute-rack" = {
switch_association_type = "range"
node_blocks = { "101-104" = { from_ = "101", to_ = "104" } }
}
}
}ℹ️
relation_to_leaf_interface_profile_dnsbinds the profile to one or more leaf interface profiles (infraAccPortP, class ofterraform-aci-leaf-interface-profile) — this is the classic flat relation; there is no typed alternative on this resource.
12 · Fully-annotated selector and node block (all metadata)
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
name_alias = "Compute Rack Leafs"
description = "Covers leaf switches 101-104 in the compute rack"
}
leaf_selectors = {
"compute-rack" = {
switch_association_type = "range"
name_alias = "Compute Rack Selector"
description = "Nodes 101-104"
relation_to_switch_policy_group_dn = "uni/infra/funcprof/accnodepgrp-compute-leaf-pg"
node_blocks = {
"101-104" = {
from_ = "101"
to_ = "104"
name_alias = "Compute rack block"
description = "Contiguous range 101-104"
}
}
}
}
}13 · Least-privilege operating model (documentation variant)
# Configure the provider with a login scoped to access-policy management only —
# not a fabric-wide admin — for day-2 changes to an existing leaf profile.
provider "aci" {
# username = "svc-access-admin" # an access-admin role
# private_key = var.apic_private_key # signature auth avoids login-rate limits
# cert_name = "terraform-cert"
# url = "https://apic.example.com"
# insecure = false
}
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = { name = "leaf-101-104" }
}🔒 Access policies are fabric-scoped; scope automation logins to the
access-adminrole rather than a fabric-wideadminaccount. Prefer signature-based (X.509) auth for automation to avoid APIC login-rate thresholds.
14 · Reading outputs for downstream wiring
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = { name = "leaf-101-104" }
leaf_selectors = {
"compute-rack" = {
switch_association_type = "range"
node_blocks = { "101-104" = { from_ = "101", to_ = "104" } }
}
}
}
output "leaf_profile_dn" { value = module.leaf_switch_profile.id } # uni/infra/nprof-leaf-101-104
output "leaf_selector_dns" { value = module.leaf_switch_profile.leaf_selector_dns }
output "node_block_dns" { value = module.leaf_switch_profile.node_block_dns }15 · 🏗️ End-to-end composition — switch policy group + interface profile → leaf profile
provider "aci" {
# configured + authenticated by the caller; insecure = false in production
}
# 1) A switch policy group that carries the switch-level policy (NTP, monitoring, etc.).
module "access_switch_policy_group" {
source = "git::https://github.com/microsoftexpert/terraform-aci-access-switch-policy-group.git?ref=v1.0.0"
access_switch_policy_group = { name = "compute-leaf-pg" }
}
# 2) A leaf interface profile carrying the interface selectors for these switches.
module "leaf_interface_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-interface-profile.git?ref=v1.0.0"
leaf_interface_profile = { name = "compute-leaf-ifprof" }
}
# 3) The keystone leaf profile, binding both by DN and declaring the node-ID range
# it covers via a range selector.
module "leaf_switch_profile" {
source = "git::https://github.com/microsoftexpert/terraform-aci-leaf-switch-profile.git?ref=v1.0.0"
leaf_profile = {
name = "leaf-101-104"
relation_to_leaf_interface_profile_dns = [
module.leaf_interface_profile.id
]
}
leaf_selectors = {
"compute-rack" = {
switch_association_type = "range"
relation_to_switch_policy_group_dn = module.access_switch_policy_group.id
node_blocks = {
"101-104" = { from_ = "101", to_ = "104" }
}
}
}
}
output "leaf_profile_dn" { value = module.leaf_switch_profile.id }🏗️ The leaf profile is where node membership (this module's
leaf_selectors/node_blocks) meets switch policy (access-switch-policy-group) and interface policy (leaf-interface-profile) — every access-policy association APIC needs to configure these physical switches and their ports converges on this module's keystone DN.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
leaf_profile |
object({...}) |
✅ | — | The leaf profile: name (required, immutable) plus the metadata tail and the classic flat interface/access-card-profile relations. |
leaf_selectors |
map(object({...})) |
➖ | {} |
Leaf selectors keyed by name, each with a nested node_blocks map keyed by block name. Empty creates no selectors. |
Full input schema (from variables.tf)
variable "leaf_profile" {
type = object({
name = string # REQUIRED, immutable (force-new), 1-64 chars
annotation = optional(string, "orchestrator:terraform") # ACI annotation marker (kept identifiable)
name_alias = optional(string, null) # GUI display alias
description = optional(string, null) # free-form description
relation_to_leaf_interface_profile_dns = optional(list(string), []) # classic flat relation infraRsAccPortP, by DN
relation_to_access_card_profile_dns = optional(list(string), []) # classic flat relation infraRsAccCardP, by DN
})
# validation: name is 1-64 chars, matches ^[a-zA-Z0-9_.:-]+$, and every
# relation DN entry is non-empty
}
variable "leaf_selectors" {
type = map(object({
switch_association_type = string # REQUIRED. ALL | range | ALL_IN_POD
annotation = optional(string, "orchestrator:terraform")
name_alias = optional(string, null)
description = optional(string, null)
relation_to_switch_policy_group_dn = optional(string, null) # classic flat relation infraRsAccNodePGrp, by DN
node_blocks = optional(map(object({
from_ = optional(string, "1") # node-ID range start, 1-16000
to_ = optional(string, "1") # node-ID range end, 1-16000
annotation = optional(string, "orchestrator:terraform")
name_alias = optional(string, null)
description = optional(string, null)
})), {})
}))
default = {}
# validation: switch_association_type is one of ALL, range, ALL_IN_POD;
# every node_blocks[*].from_ / to_ is within 1-16000
}| Output | Description | Notes |
|---|---|---|
id |
Leaf profile Distinguished Name (uni/infra/nprof-{name}) |
Primary cross-module reference. |
name |
Leaf profile name | For composition / audit. |
leaf_selector_dns |
Map of selector key (name) → selector DN | Empty map when leaf_selectors is {}. |
node_block_dns |
Map of node block key ("{selector_key}.{block_key}") → node block DN |
Empty map when no selector declares node_blocks. |
- One keystone, two levels of
for_eachchildren.aci_leaf_profile.thisowns identity and the classic flat interface/access-card-profile relations;aci_leaf_selector.thisiterates the top-levelleaf_selectorsmap;aci_node_block.thisiterates alocal.node_blocksmap flattened from each selector's nestednode_blocks— nevercountat any level, so inserting or removing a selector or a single node block never churns the rest of the set. - Node blocks nest inside their selector in the input, not a parallel top-level map. This mirrors the ACI hierarchy (a node block cannot exist without its owning selector) and avoids needing a cross-map "does this key exist" validation that plain Terraform
validation {}blocks cannot express reliably at this Terraform floor. - Classic (SDKv2) shape throughout. All three resources predate the plugin-framework migration: relations are flat
relation_infra_rs_*attributes (aset(string)on the keystone, a singlestringon the selector), not typedrelation_to_*nested objects. This module surfaces them as ergonomic DN lists/strings and renders them directly. - Dedicated child resources, not inline blocks.
aci_leaf_profile's live schema also supports inlineleaf_selector { node_block { ... } }blocks on the keystone itself; this module never uses them, so there is exactly one place that manages any given selector or node block. - Immutable identity.
leaf_profile.nameis force-new: a rename destroys and recreates the profile (and, server-side, everything nested under its DN). Thevalidationblocks reject names that violate the ACI length/character rules, invalidswitch_association_typevalues, and out-of-range node IDs at plan time, not apply time. - Secure by omission. The minimal call preserves the
orchestrator:terraformannotation, creates no selectors, and forces no relations — nothing permissive is created by default.
| Concern | Secure default | How to opt out (deliberately) |
|---|---|---|
leaf_profile.annotation |
orchestrator:terraform — Terraform-managed objects stay identifiable in APIC |
Extend the marker (e.g. add a team suffix); do not blank it. |
leaf_selectors |
{} — no selector created by default |
Add entries keyed by selector name. |
relation_to_leaf_interface_profile_dns / relation_to_access_card_profile_dns |
[] — no interface/access-card profile bound |
Add DNs explicitly. |
leaf_selectors[*].relation_to_switch_policy_group_dn |
null — no switch policy group bound |
Set it to bind a switch policy group by DN. |
switch_association_type scope |
Caller must choose explicitly (ALL / range / ALL_IN_POD) — no implicit default toward the broadest match |
Choose range for tightly-scoped policy; reserve ALL / ALL_IN_POD for deliberate fabric-/pod-wide policy. |
| 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 — the leaf profile 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 theleaf_profileandleaf_selectorsobject types (including the nestednode_blocksmap), runs the name/enum/range 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_switch_policy_group_dnand the keystone's flat relations (server-sidevalidate_relation_dn), APIC-side name-collision and node-ID-conflict checks, and the computed DNs returned asid/leaf_selector_dns/node_block_dns.
$ terraform output
id = "uni/infra/nprof-leaf-101-104"
name = "leaf-101-104"
leaf_selector_dns = {
"compute-rack" = "uni/infra/nprof-leaf-101-104/leaves-compute-rack-typ-range"
}
node_block_dns = {
"compute-rack.101-104" = "uni/infra/nprof-leaf-101-104/leaves-compute-rack-typ-range/nodeblk-101-104"
}
| Symptom | Cause | Fix |
|---|---|---|
leaf_profile.name must be 1-64 characters |
Name is empty or too long | Use a 1-64 character name. |
leaf_profile.name may contain only letters, digits, and the characters _ . : - |
Name has spaces or unsupported characters | Remove spaces/special characters (ACI naming rules). |
Every leaf_selectors[*].switch_association_type must be one of: ALL, range, ALL_IN_POD |
An unsupported value was passed | Use one of the three allowed values (confirmed against the live provider validator). |
Every leaf_selectors[*].node_blocks[*].from_ must be between 1 and 16000 (or to_) |
A node-ID string outside the valid range | Use a numeric string between "1" and "16000". |
Changing leaf_profile.name wants to destroy/recreate the profile |
leaf_profile.name is immutable (force-new) |
Treat a rename as a migration; expect the profile and its selectors/node blocks to be replaced. |
| Plan shows a conflicting change to the same selector/node-block object | Inline leaf_selector { node_block { ... } } blocks were added to aci_leaf_profile.this alongside this module's leaf_selectors |
Manage selectors/node blocks through leaf_selectors only; never add inline blocks to a profile this module manages. |
Apply fails validating relation_to_switch_policy_group_dn |
The referenced switch policy group does not exist | Create the switch policy group first (or in the same apply); do not disable validate_relation_dn. |
Post ... 401 / authentication error |
Provider not configured or wrong credentials | Configure the aci provider with valid credentials and url; prefer signature auth for automation. |
| TLS verification error against the APIC | insecure = false (correct) but no CA trust |
Install the APIC's CA chain in the caller's trust store rather than reverting to insecure = true. |
- Cisco ACI provider —
aci_leaf_profile - Cisco ACI provider —
aci_leaf_selector - Cisco ACI provider —
aci_node_block - Cisco ACI provider — provider configuration & authentication
- Cisco APIC object model — class
infraNodeP(the leaf profile),infraLeafS(leaf selector / switch association), andinfraNodeBlk(node block). - Sibling modules:
terraform-aci-access-switch-policy-group,terraform-aci-leaf-interface-profile,terraform-aci-spine-access-port-policy-group. - This module's
SCOPE.md— the cross-module contract.
💙 "Infrastructure as Code should be standardized, consistent, and secure."