Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

💙 Cisco ACI OSPF Policies Terraform Module

Manage a Cisco ACI tenant's OSPF protocol-policy set — the OSPF interface policy (class ospfIfPol), OSPF timers policy (class ospfCtxPol), OSPF route-summarization policy (class ospfRtSummPol), and the VRF-to-timers binding (class fvRsCtxToOspfCtxPol) — as a typed, secure-by-default building block targeting CiscoDevNet/aci ~> 2.20.

Terraform Provider Module Version Type Resources

🧩 Overview

This module manages a tenant's OSPF protocol-policy templates and the relationship that binds one of them to a VRF, as one coherent, secure-by-default unit:

  • 🛣️ The OSPF interface policy (aci_ospf_interface_policy.this) — the keystone; a tenant-scoped template of per-interface OSPF timers and controls (hello/dead intervals, network type, cost, priority) referenced by L3Out interface profiles, addressed by uni/tn-{tenant}/ospfIfPol-{name}.
  • ⏱️ OSPF timers policies (aci_ospf_timers.this, for_each) — sibling tenant-scoped templates tuning VRF-wide OSPF process behavior (SPF/LSA pacing, max-ECMP, administrative distance, graceful restart, max-LSA protection), keyed by a stable natural key.
  • 🧮 OSPF route-summarization policies (aci_ospf_route_summarization.this, for_each) — sibling tenant-scoped templates referenced from an L3Out's OSPF area configuration to summarize inter-area or external routes.
  • 🔗 A VRF-to-timers binding (aci_relation_from_vrf_to_address_family_ospf_timers.this, for_each) — binds a VRF's OSPF address family (ipv4-ucast / ipv6-ucast) to one of this module's own timers policies, consuming the VRF by DN.
  • 🏷️ The ACI metadata tail — annotation (preserved as orchestrator:terraform), name_alias, and description on every policy.
  • 🔑 Scope, not credentials — every tenant-scoped policy takes the parent tenant DN (tenant_dn) as a required input; the VRF binding takes the VRF's own DN by reference. Authentication and the APIC URL are the caller's provider concern and are never module variables.

💡 Why it matters: OSPF's interface behavior, VRF-wide process tuning, and route summarization are configured as three independent tenant-scoped templates in the ACI object model, not as nested children of one another — but they are almost always authored, reviewed, and rolled out together as a tenant's OSPF posture. Bundling them (plus the VRF binding that activates the timers policy) in one module keeps that whole surface auditable as a single change, while still emitting each policy's DN for L3Out modules to reference independently.

❤️ 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
  ospf["terraform-aci-ospf-policies (this module)"]:::this
  ifpol["aci_ospf_interface_policy - class ospfIfPol - DN uni/tn-{t}/ospfIfPol-{n}"]:::keystone
  timers["aci_ospf_timers - class ospfCtxPol (for_each)"]:::keystone
  rtsumm["aci_ospf_route_summarization - class ospfRtSummPol (for_each)"]:::keystone
  rel["aci_relation_from_vrf_to_address_family_ospf_timers - class fvRsCtxToOspfCtxPol (for_each)"]:::keystone
  vrf["terraform-aci-vrf"]:::sib
  l3out["terraform-aci-l3-outside"]:::sib
  tenant -->|"tenant_dn"| ospf
  ospf -->|"manages"| ifpol
  ospf -->|"for_each ospf_timers"| timers
  ospf -->|"for_each ospf_route_summarization"| rtsumm
  ospf -->|"for_each vrf_ospf_timer_relations"| rel
  vrf -->|"vrf_dn (relation parent_dn)"| rel
  ospf -->|"id (interface policy DN, by name)"| l3out
  rtsumm -->|"by name"| l3out
  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

This module sits beside the VRF and the L3Out in a tenant's routing stack: it takes the tenant (tenant_dn) as its parent, consumes a VRF's DN only to bind that VRF's OSPF address family to one of its own timers policies, and emits the interface policy DN and the route-summarization DNs for the L3Out module's OSPF interface profiles and area configuration to reference.

🧬 What this module builds

graph TD
  tdn["tenant_dn (required)"]:::in
  ifp["ospf_interface_policy object"]:::in
  tim["ospf_timers map (for_each)"]:::in
  rts["ospf_route_summarization map (for_each)"]:::in
  vrl["vrf_ospf_timer_relations map (for_each)"]:::in
  this["aci_ospf_interface_policy.this (keystone, ospfIfPol)"]:::this
  t["aci_ospf_timers.this (for_each, ospfCtxPol)"]:::this
  r["aci_ospf_route_summarization.this (for_each, ospfRtSummPol)"]:::this
  rel["aci_relation_from_vrf_to_address_family_ospf_timers.this (for_each, fvRsCtxToOspfCtxPol)"]:::this
  oid["output: id (interface policy DN)"]:::out
  otd["output: ospf_timers_dns (map)"]:::out
  ord["output: ospf_route_summarization_dns (map)"]:::out
  orl["output: vrf_ospf_timer_relation_dns (map)"]:::out

  tdn --> this
  ifp --> this
  tdn --> t
  tim --> t
  tdn --> r
  rts --> r
  vrl --> rel

  this --> oid
  t --> otd
  r --> ord
  rel --> orl

  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_ospf_interface_policy this 1 (keystone) The OSPF interface policy — per-interface timers/controls (ospfIfPol).
aci_ospf_timers this 0..N (for_each over ospf_timers) OSPF timers (VRF context) policies (ospfCtxPol).
aci_ospf_route_summarization this 0..N (for_each over ospf_route_summarization) OSPF route-summarization policies (ospfRtSummPol).
aci_relation_from_vrf_to_address_family_ospf_timers this 0..N (for_each over vrf_ospf_timer_relations) Binds a VRF's OSPF address family to a timers policy (fvRsCtxToOspfCtxPol).

✅ 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 policies require a parent tenant_dn; the VRF binding requires the VRF's own DN (vrf_dn).

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

  • ⚠️ aci_ospf_interface_policy, aci_ospf_timers, and aci_ospf_route_summarization are classic (SDKv2) resources. None of the three exposes a parent_dn attribute in this provider line — the parent tenant is wired through tenant_dn, which is correctly the current attribute here, not a deprecated one. Do not confuse this with migrated resources (like the bridge domain) where tenant_dn is deprecated in favor of parent_dn.
  • ℹ️ aci_relation_from_vrf_to_address_family_ospf_timers is the one migrated (plugin-framework) resource here. Its parent is the VRF, wired through parent_dn (not tenant_dn — a VRF DN, not a tenant DN). It also exposes typed annotations / tags nested attributes, unused in this module beyond the single annotation marker.
  • 🔒 name is immutable on all three tenant-scoped policies. Changing it forces replacement of that policy — and, for the OSPF timers policy, breaks any vrf_ospf_timer_relations entry that references it by name until updated.
  • ℹ️ These three policy objects are DN siblings, not a parent/child chain. The OSPF interface policy, OSPF timers policy, and route-summarization policy do not nest under each other in the MIT — each is created directly under the tenant. This module bundles them because they are typically authored together, not because one contains another.
  • ⚠️ Numeric fields are strings with documented ranges. Every OSPF timer/control field (cost, dead_intvl, hello_intvl, prio, rexmit_intvl, xmit_delay, bw_ref, dist, the lsa_* / spf_* intervals, max_ecmp, the max_lsa_* fields, and route-summarization cost) is a string in the provider schema even though it carries a numeric range; this module validates each at plan time against the range confirmed in the live provider schema and leaves it null (provider computed default) when unset.
  • ⚠️ ctrl is a distinct closed enum on each resource. unspecified | passive | mtu-ignore | advert-subnet | bfd on the interface policy vs. name-lookup | pfx-suppress on the timers policy — confirmed live; do not reuse one list on the other.
  • ℹ️ inter_area_enabled on the route-summarization policy is a yes/no string in the provider; this module surfaces it as bool and renders it.
  • ⚠️ validate_relation_dn (provider default true) fails apply if vrf_ospf_timer_relations[*].vrf_dn does not exist, or if ospf_timers_name does not resolve to an OSPF timers policy in the same tenant — create the missing VRF/policy first rather than disabling validation.

🔑 Required APIC Roles & Privileges

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

  • Create / modify the OSPF interface, timers, and route-summarization policies: the tenant-admin role (or a custom role with tenant-networking / protocol-policy write privilege), scoped to the tenant's own security domain.
  • Create the VRF-to-timers binding: write privilege scoped to the target VRF's security domain (the relation is created under the VRF's own DN).
  • Referenced VRF: read privilege on any VRF named in vrf_ospf_timer_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).

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 vrf_ospf_timer_relations is set, the referenced VRF exists (or is created in the same apply) so the provider's DN validation passes, and the referenced ospf_timers_name policy exists in the same tenant — either in this module's own ospf_timers map or from another instance of this module.

📁 Module Structure

terraform-aci-ospf-policies/
├── providers.tf     # terraform{} + required_providers (aci ~> 2.20); no provider block
├── variables.tf     # tenant_dn + ospf_interface_policy object + ospf_timers / ospf_route_summarization / vrf_ospf_timer_relations maps
├── main.tf          # aci_ospf_interface_policy.this (keystone) + three for_each collections
├── outputs.tf       # id (interface policy DN) first, then name and three 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 "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn             = module.tenant.id # from terraform-aci-tenant
  ospf_interface_policy = { name = "ospf-if-default" }
}

output "ospf_interface_policy_dn" {
  value = module.ospf_policies.id # reference from an L3Out interface profile
}

🔌 Cross-Module Contract

Consumes

Input Type Typical source
tenant_dn string (DN) terraform-aci-tenant
ospf_interface_policy object({...}) caller (name + per-interface timers/controls + metadata)
ospf_timers map(object({...})) caller
ospf_route_summarization map(object({...})) caller
vrf_ospf_timer_relations map(object({ vrf_dn, address_family, ospf_timers_name, annotation })) caller; vrf_dn from terraform-aci-vrf

Emits

Output Description Consumed by
id OSPF interface policy DN (uni/tn-{tenant}/ospfIfPol-{name}) — primary reference L3Out logical interface profiles (interface-policy binding)
name OSPF interface policy name composition / audit
ospf_timers_dns Map of ospf_timers key → timers policy DN audits / downstream reference
ospf_route_summarization_dns Map of ospf_route_summarization key → route-summarization policy DN L3Out OSPF area configuration (summarization binding)
vrf_ospf_timer_relation_dns Map of vrf_ospf_timer_relations key → relation DN audits / downstream reference

📚 Example Library

1 · Minimal — an OSPF interface policy with secure defaults
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn              = module.tenant.id
  ospf_interface_policy  = { name = "ospf-if-default" }
}

💡 The minimal call leaves every timer and control at the provider's own computed default (hello/dead intervals, priority, cost, network type all inherit the ACI defaults). No timers policy, route-summarization policy, or VRF binding is created yet.

2 · Passive interfaces with BFD-style fast-failure detection
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  ospf_interface_policy = {
    name = "ospf-if-passive-bfd"
    ctrl = ["passive", "bfd"]
  }
}

ℹ️ passive suppresses OSPF hellos on the interface (routes are still advertised); bfd ties OSPF neighbor liveness to a BFD session for sub-second failure detection.

3 · Point-to-point network type for a routed sub-interface
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  ospf_interface_policy = {
    name = "ospf-if-p2p"
    nw_t = "p2p"
  }
}

ℹ️ nw_t = "p2p" avoids DR/BDR election on point-to-point transit links — the common choice for routed sub-interfaces and L3Out numbered links.

4 · Custom cost, priority, and timers
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  ospf_interface_policy = {
    name         = "ospf-if-tuned"
    cost         = "50"
    prio         = "10"
    hello_intvl  = "5"
    dead_intvl   = "20"
    rexmit_intvl = "3"
  }
}

⚠️ hello_intvl and dead_intvl must match (or be compatible with) the neighbor's OSPF interface policy — a mismatch prevents adjacency from forming. Every numeric field here is validated at plan time against the documented range.

5 · A single OSPF timers (VRF context) policy
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn              = module.tenant.id
  ospf_interface_policy  = { name = "ospf-if-default" }

  ospf_timers = {
    "core-vrf-timers" = { name = "core-vrf-timers" }
  }
}

💡 ospf_timers is a map keyed by a stable natural key so adding a second policy later never disturbs this one in state. The minimal entry leaves every SPF/LSA/graceful-restart knob at the provider's own default.

6 · Multiple OSPF timers policies (for_each)
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn              = module.tenant.id
  ospf_interface_policy  = { name = "ospf-if-default" }

  ospf_timers = {
    "core-vrf-timers"  = { name = "core-vrf-timers" }
    "dmz-vrf-timers"   = { name = "dmz-vrf-timers", max_ecmp = "4" }
  }
}

ℹ️ Each map entry becomes its own aci_ospf_timers resource via for_each — order-independent, and safe to add or remove entries without touching the others.

7 · Tuning SPF and LSA pacing on a timers policy
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn              = module.tenant.id
  ospf_interface_policy  = { name = "ospf-if-default" }

  ospf_timers = {
    "fast-converge" = {
      name             = "fast-converge"
      spf_init_intvl   = "50"
      spf_hold_intvl   = "200"
      spf_max_intvl    = "1000"
      lsa_start_intvl  = "0"
      lsa_hold_intvl   = "1000"
    }
  }
}

⚠️ Lowering SPF/LSA intervals speeds convergence but increases CPU load on affected nodes during instability — validate against the fabric's scale before applying broadly.

8 · Disabling graceful restart on a timers policy
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn              = module.tenant.id
  ospf_interface_policy  = { name = "ospf-if-default" }

  ospf_timers = {
    "no-gr" = { name = "no-gr", gr_ctrl = "" }
  }
}

ℹ️ gr_ctrl = "" explicitly disables OSPF graceful restart for this VRF context; leave gr_ctrl unset (null) to keep the provider's own default (helper).

9 · A route-summarization policy (inter-area enabled)
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn              = module.tenant.id
  ospf_interface_policy  = { name = "ospf-if-default" }

  ospf_route_summarization = {
    "branch-summary" = {
      name               = "branch-summary"
      inter_area_enabled = true
      cost               = "100"
    }
  }
}

💡 inter_area_enabled defaults to false; set it explicitly to opt in to inter-area summarization for this policy. Reference the resulting DN from an L3Out's OSPF area configuration.

10 · Multiple route-summarization policies
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn              = module.tenant.id
  ospf_interface_policy  = { name = "ospf-if-default" }

  ospf_route_summarization = {
    "branch-summary" = { name = "branch-summary", inter_area_enabled = true }
    "dr-summary"     = { name = "dr-summary", inter_area_enabled = true, tag = "100" }
  }
}

ℹ️ Like ospf_timers, ospf_route_summarization is a for_each map — each entry is an independent tenant-scoped policy, not a child of the other.

11 · Binding a VRF's OSPF address family to a timers policy
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn              = module.tenant.id
  ospf_interface_policy  = { name = "ospf-if-default" }

  ospf_timers = {
    "core-vrf-timers" = { name = "core-vrf-timers" }
  }

  vrf_ospf_timer_relations = {
    "core-vrf-ipv4" = {
      vrf_dn           = module.vrf.id
      address_family   = "ipv4-ucast"
      ospf_timers_name = "core-vrf-timers"
    }
  }
}

⚠️ ospf_timers_name must reference a policy that exists in the same tenant — either in this module's own ospf_timers map or from another instance of this module — or the provider's validate_relation_dn check fails the apply. vrf_dn must be a VRF DN (uni/tn-{name}/ctx-{name}), typically module.vrf.id.

12 · Dual-stack VRF bindings (ipv4-ucast and ipv6-ucast)
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn              = module.tenant.id
  ospf_interface_policy  = { name = "ospf-if-default" }

  ospf_timers = {
    "core-vrf-timers" = { name = "core-vrf-timers" }
  }

  vrf_ospf_timer_relations = {
    "core-vrf-ipv4" = { vrf_dn = module.vrf.id, address_family = "ipv4-ucast", ospf_timers_name = "core-vrf-timers" }
    "core-vrf-ipv6" = { vrf_dn = module.vrf.id, address_family = "ipv6-ucast", ospf_timers_name = "core-vrf-timers" }
  }
}

ℹ️ A VRF can bind the same or different timers policies per address family — address_family is validated to ipv4-ucast or ipv6-ucast at plan time.

13 · User metadata via the annotation marker
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  ospf_interface_policy = {
    name       = "ospf-if-default"
    annotation = "orchestrator:terraform:network-platform"
  }
}

🔒 Keep the orchestrator:terraform prefix so Terraform-managed objects stay identifiable in APIC. This suite defaults annotation to orchestrator:terraform on every policy; override it only to extend, not to erase, that marker.

14 · A full tenant OSPF policy bundle (all four objects together)
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn = module.tenant.id

  ospf_interface_policy = {
    name        = "ospf-if-core"
    nw_t        = "p2p"
    hello_intvl = "5"
    dead_intvl  = "20"
  }

  ospf_timers = {
    "core-vrf-timers" = { name = "core-vrf-timers", max_ecmp = "16" }
  }

  ospf_route_summarization = {
    "branch-summary" = { name = "branch-summary", inter_area_enabled = true }
  }

  vrf_ospf_timer_relations = {
    "core-vrf-ipv4" = { vrf_dn = module.vrf.id, address_family = "ipv4-ucast", ospf_timers_name = "core-vrf-timers" }
  }
}
15 · 🏗️ End-to-end composition — tenant → VRF → OSPF policies → L3Out
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) A VRF in the tenant — the OSPF process context this module binds timers to.
module "vrf" {
  source    = "git::https://github.com/microsoftexpert/terraform-aci-vrf.git?ref=v1.0.0"
  tenant_dn = module.tenant.id
  vrf       = { name = "core-vrf" }
}

# 3) This module: interface policy + timers policy + route-summarization + VRF binding.
module "ospf_policies" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-ospf-policies.git?ref=v1.0.0"

  tenant_dn = module.tenant.id

  ospf_interface_policy = {
    name = "ospf-if-core"
    nw_t = "p2p"
  }

  ospf_timers = {
    "core-vrf-timers" = { name = "core-vrf-timers" }
  }

  ospf_route_summarization = {
    "branch-summary" = { name = "branch-summary", inter_area_enabled = true }
  }

  vrf_ospf_timer_relations = {
    "core-vrf-ipv4" = {
      vrf_dn           = module.vrf.id
      address_family   = "ipv4-ucast"
      ospf_timers_name = "core-vrf-timers"
    }
  }
}

# 4) An L3Out consumes the interface policy and route-summarization DNs by reference.
module "l3out" {
  source    = "git::https://github.com/microsoftexpert/terraform-aci-l3-outside.git?ref=v1.0.0"
  tenant_dn = module.tenant.id
  vrf_dn    = module.vrf.id
  l3_outside = {
    name                          = "internet-l3out"
    ospf_interface_policy_name    = module.ospf_policies.name
    ospf_route_summarization_name = "branch-summary"
  }
}

output "ospf_interface_policy_dn" { value = module.ospf_policies.id }

🏗️ One tenant and one VRF in; a fully-tuned OSPF policy surface out. The L3Out module references the interface policy and route-summarization policy by name within the tenant, while the VRF's OSPF address family is bound directly to the timers policy through this module's own relation resource.

📥 Inputs

Name Type Required Default Description
tenant_dn string ✅ — Parent tenant DN (uni/tn-{name}); wired to every tenant-scoped policy's tenant_dn.
ospf_interface_policy object({...}) ✅ — The OSPF interface policy: name, per-interface timers/controls, and metadata tail.
ospf_timers map(object({...})) ➖ {} OSPF timers (VRF context) policies, keyed by a stable natural key.
ospf_route_summarization map(object({...})) ➖ {} OSPF route-summarization policies, keyed by a stable natural key.
vrf_ospf_timer_relations map(object({...})) ➖ {} VRF-to-timers bindings, 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 "ospf_interface_policy" {
  type = object({
    name         = string                                     # REQUIRED, immutable (force-new), 1-64 chars
    annotation   = optional(string, "orchestrator:terraform") # ACI annotation marker.
    name_alias   = optional(string, null)                     # GUI display alias.
    description  = optional(string, null)                     # Free-form description.
    cost         = optional(string, null)                     # "unspecified" or "0"-"65535".
    ctrl         = optional(list(string), [])                 # Subset of: unspecified | passive | mtu-ignore | advert-subnet | bfd.
    dead_intvl   = optional(string, null)                     # "1"-"65535" seconds.
    hello_intvl  = optional(string, null)                     # "1"-"65535" seconds.
    nw_t         = optional(string, null)                     # unspecified | p2p | bcast.
    pfx_suppress = optional(string, null)                     # inherit | enable | disable.
    prio         = optional(string, null)                     # "0"-"255".
    rexmit_intvl = optional(string, null)                     # "1"-"65535" seconds.
    xmit_delay   = optional(string, null)                     # "1"-"450" seconds.
  })
  # validations: name length/charset; ctrl subset; nw_t/pfx_suppress enums; every numeric range
}

variable "ospf_timers" {
  type = map(object({
    name                = string                                     # REQUIRED, immutable, 1-64 chars
    annotation          = optional(string, "orchestrator:terraform")
    name_alias          = optional(string, null)
    description         = optional(string, null)
    bw_ref              = optional(string, null)      # "1"-"4000000"
    ctrl                = optional(list(string), [])  # Subset of: name-lookup | pfx-suppress.
    dist                = optional(string, null)      # "1"-"255"
    gr_ctrl             = optional(string, null)      # "helper" | "" (disabled)
    lsa_arrival_intvl   = optional(string, null)      # "10"-"600000"
    lsa_gp_pacing_intvl = optional(string, null)      # "1"-"1800"
    lsa_hold_intvl      = optional(string, null)      # "50"-"30000"
    lsa_max_intvl       = optional(string, null)      # "50"-"30000"
    lsa_start_intvl     = optional(string, null)      # "0"-"5000"
    max_ecmp            = optional(string, null)      # "1"-"64"
    max_lsa_action      = optional(string, null)      # reject | log | restart
    max_lsa_num         = optional(string, null)      # "1"-"4294967295"
    max_lsa_reset_intvl = optional(string, null)      # "1"-"1440"
    max_lsa_sleep_cnt   = optional(string, null)      # "1"-"4294967295"
    max_lsa_sleep_intvl = optional(string, null)      # "1"-"1440"
    max_lsa_thresh      = optional(string, null)      # "1"-"100"
    spf_hold_intvl      = optional(string, null)      # "1"-"600000"
    spf_init_intvl      = optional(string, null)      # "1"-"600000"
    spf_max_intvl       = optional(string, null)      # "1"-"600000"
  }))
  default = {}
  # validations: name length/charset; ctrl subset; gr_ctrl/max_lsa_action enums
}

variable "ospf_route_summarization" {
  type = map(object({
    name               = string                                     # REQUIRED, immutable, 1-64 chars
    annotation         = optional(string, "orchestrator:terraform")
    name_alias         = optional(string, null)
    description        = optional(string, null)
    cost               = optional(string, null)  # "unspecified" or "0"-"16777215"
    inter_area_enabled = optional(bool, false)   # rendered to yes/no
    tag                = optional(string, null)
  }))
  default = {}
  # validations: name length/charset; cost range
}

variable "vrf_ospf_timer_relations" {
  type = map(object({
    vrf_dn           = string                                     # REQUIRED. VRF DN, e.g. module.vrf.id.
    address_family   = string                                     # REQUIRED. ipv4-ucast | ipv6-ucast.
    ospf_timers_name = string                                     # REQUIRED. Name of an ospf_timers policy in this tenant.
    annotation       = optional(string, "orchestrator:terraform")
  }))
  default = {}
  # validations: vrf_dn DN format; address_family enum; ospf_timers_name length
}

🧾 Outputs

Output Description Notes
id OSPF interface policy Distinguished Name (uni/tn-{tenant}/ospfIfPol-{name}) Primary cross-module reference — reference from an L3Out interface profile.
name OSPF interface policy name For composition / audit.
ospf_timers_dns Map of ospf_timers key → timers policy DN For downstream reference / audit.
ospf_route_summarization_dns Map of ospf_route_summarization key → route-summarization policy DN For downstream reference / audit.
vrf_ospf_timer_relation_dns Map of vrf_ospf_timer_relations key → relation DN For downstream reference / audit.

🧠 Architecture Notes

  • Four resources, one keystone. aci_ospf_interface_policy.this is the keystone; aci_ospf_timers.this, aci_ospf_route_summarization.this, and aci_relation_from_vrf_to_address_family_ospf_timers.this are each iterated with for_each over their own typed map — never count — so inserting or removing one policy never churns the others' state addresses.
  • Sibling policies, not nested children. The OSPF interface, timers, and route-summarization policies are all created directly under the tenant DN — none is a DN child of another, unlike a bridge domain's subnets. The module bundles them for authoring/review convenience, not because the ACI object model nests them.
  • Classic resources, tenant_dn is correct here. aci_ospf_interface_policy, aci_ospf_timers, and aci_ospf_route_summarization have no parent_dn attribute in the live provider schema — tenant_dn is the only (and current) way to set their parent. Only the VRF-binding relation is migrated and uses parent_dn.
  • The VRF binding reaches across modules. aci_relation_from_vrf_to_address_family_ospf_timers is created under the VRF's DN (owned by terraform-aci-vrf), not the tenant, and references this module's own timers policies by name — the one place this module writes into another module's object.
  • Numeric-as-string fields, validated at plan time. Every OSPF timer/control field is a string in the provider schema but carries a documented numeric range; this module validates each range (and the cost fields' "unspecified" sentinel) with can(tonumber(...)) checks so out-of-range values fail at terraform validate, not at apply.
  • Null-when-empty for ctrl. Each resource's ctrl list is passed as null (not an empty list) when the caller supplies none, so the module never fights provider-computed state.
  • Boolean ergonomics. ospf_route_summarization[*].inter_area_enabled is surfaced as bool and rendered to the provider's "yes"/"no" string in main.tf.
  • Secure by default. The minimal call for each policy leaves every timer/control at the provider's own computed default, and no VRF binding, timers policy, or summarization policy is created unless explicitly supplied.

🧱 Design Principles

Concern Secure default How to opt out (deliberately)
ospf_interface_policy.* numeric/enum fields null — inherit the provider's own computed default Set explicitly per the documented range/enum.
ospf_timers[*].gr_ctrl null — inherit the provider default (helper, graceful restart on) Set "" to explicitly disable graceful restart.
ospf_route_summarization[*].inter_area_enabled false — no inter-area summarization Set true to opt in per policy.
ospf_timers / ospf_route_summarization / vrf_ospf_timer_relations {} — no sibling policies or bindings created Populate the maps explicitly.
annotation (every policy) orchestrator:terraform — Terraform-managed objects stay identifiable in APIC Extend the marker; do not blank it.
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 — these policies carry 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 ospf_interface_policy, ospf_timers, ospf_route_summarization, and vrf_ospf_timer_relations object types, runs the name/enum/numeric-range 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 vrf_ospf_timer_relations[*].vrf_dn and ospf_timers_name (server-side validate_relation_dn), APIC-side name-collision checks, OSPF neighbor-adjacency behavior from the configured timers, and the computed DNs returned as id / the three DN maps.

💬 Example Output

$ terraform output
id                           = "uni/tn-core-prod/ospfIfPol-ospf-if-core"
name                         = "ospf-if-core"
ospf_timers_dns = {
  "core-vrf-timers" = "uni/tn-core-prod/ospfCtxPol-core-vrf-timers"
}
ospf_route_summarization_dns = {
  "branch-summary" = "uni/tn-core-prod/ospfRtSummPol-branch-summary"
}
vrf_ospf_timer_relation_dns = {
  "core-vrf-ipv4" = "uni/tn-core-prod/ctx-core-vrf/rsctxToOspfCtxPol-[core-vrf-timers]-ipv4-ucast"
}

🔍 Troubleshooting

Symptom Cause Fix
ospf_interface_policy.name must be 1-64 characters (or the timers/route-summarization equivalent) Name is empty or too long Use a 1-64 character name.
... may contain only letters, digits, and the characters _ . : - Name has spaces or unsupported characters Remove spaces/special characters (ACI naming rules).
... must be null or a number (range error) A numeric OSPF field is outside its documented range Use a value within the documented range, or leave it null for the provider default.
... must be one of: ... (enum error) ctrl, nw_t, pfx_suppress, gr_ctrl, max_lsa_action, or address_family is outside its closed enum Use one of the documented values for that field.
Changing a policy's name wants to destroy/recreate it name is immutable (force-new) on all three tenant-scoped policies Treat a rename as a migration; update any vrf_ospf_timer_relations[*].ospf_timers_name that referenced the old name.
Apply fails validating vrf_ospf_timer_relations[*].vrf_dn The referenced VRF does not exist yet Create the VRF first (or in the same apply); do not disable validate_relation_dn.
Apply fails validating ospf_timers_name The named aci_ospf_timers policy does not exist in the same tenant Add it to this module's ospf_timers map (or confirm the other module instance that manages it applies first).
OSPF neighbors never form an adjacency hello_intvl / dead_intvl mismatch between neighbors, or ctrl = ["passive"] set unintentionally Align interface-policy timers across the link; remove passive if hellos should be sent.
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-ospf-policies

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages