Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ’™ Cisco ACI L4-L7 Redirect Terraform Module

Manage a Cisco ACI Policy-Based Redirect (PBR) service redirect policy β€” the L4-L7 traffic-steering construct (class vnsSvcRedirectPol, DN uni/tn-{tenant}/svcCont/svcRedirectPol-{name}) together with its backup policy, health groups, and L3 / L1-L2 destinations β€” as a typed, secure-by-default building block targeting CiscoDevNet/aci ~> 2.20.

Terraform Provider Module Version Type Resources

🧩 Overview

This module manages a Cisco ACI Policy-Based Redirect (PBR) configuration as one coherent, secure-by-default unit:

  • πŸ”€ The service redirect policy (aci_service_redirect_policy.this) β€” the PBR construct that steers matched traffic to a pool of service-node destinations, addressed by the Distinguished Name uni/tn-{tenant}/svcCont/svcRedirectPol-{name}.
  • 🧯 A paired backup policy (aci_service_redirect_backup_policy.this, for_each) β€” the fallback destination pool used when the primary pool's health drops.
  • 🩺 Redirect health groups (aci_l4_l7_redirect_health_group.this, for_each) β€” tracking objects that destinations bind to for up/down health monitoring.
  • 🎯 L3 destinations (aci_destination_of_redirected_traffic.this, for_each) β€” the IP-addressed service nodes traffic is redirected to.
  • πŸ”Œ L1/L2 destinations (aci_pbr_l1_l2_destination.this, for_each) β€” concrete-interface-attached destinations for transparent (L1/L2) service insertion, under either the primary or the backup policy.
  • 🏷️ The ACI metadata tail β€” annotation (preserved as orchestrator:terraform), name_alias, and description where the live schema exposes them.
  • πŸ”‘ Scope, not credentials β€” the policy 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: PBR is how ACI inserts firewalls, load balancers, and other L4-L7 devices into a traffic path without routing through them by default gateway β€” a contract subject or service graph template points its redirect relation at this policy's id, and everything downstream (destination health, backup failover, threshold behavior) is governed by what this module builds. A correctly-scoped, health-tracked redirect policy is the difference between transparent service insertion and a silent black hole when a service node fails.

❀️ 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
  redirect["terraform-aci-l4-l7-redirect (this module)"]:::this
  pol["aci_service_redirect_policy - class vnsSvcRedirectPol - DN uni/tn-{t}/svcCont/svcRedirectPol-{n}"]:::keystone
  device["terraform-aci-l4-l7-device"]:::sib
  ipsla["terraform-aci-ip-sla"]:::sib
  contract["terraform-aci-contract"]:::sib
  sgt["terraform-aci-l4-l7-service-graph-template"]:::sib

  tenant -->|"tenant_dn"| redirect
  redirect -->|"manages"| pol
  device -->|"concrete_interface_dn (l1_l2_destinations)"| redirect
  ipsla -->|"ip_sla_monitoring_policy_dn"| redirect
  redirect -->|"id (PBR policy DN)"| contract
  redirect -->|"id (PBR policy DN)"| sgt

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

The service redirect policy sits between the tenant (its parent, via tenant_dn) and the constructs that steer traffic into it. It optionally binds to a concrete interface (for L1/L2 destinations) and an IP SLA monitoring policy β€” each by DN β€” and emits its id (the policy DN) for a contract subject or service graph template to consume as the PBR redirect relation.

🧬 What this module builds

graph TD
  tdn["tenant_dn (required)"]:::in
  srp["service_redirect_policy object (PBR posture + metadata)"]:::in
  bp["backup_policies map (for_each)"]:::in
  hg["health_groups map (for_each)"]:::in
  dest["destinations map (for_each, L3)"]:::in
  l12["l1_l2_destinations map (for_each, concrete interface)"]:::in

  this["aci_service_redirect_policy.this (keystone, vnsSvcRedirectPol)"]:::this
  backup["aci_service_redirect_backup_policy.this (for_each, vnsBackupPol)"]:::this
  health["aci_l4_l7_redirect_health_group.this (for_each, vnsRedirectHealthGroup)"]:::this
  l3d["aci_destination_of_redirected_traffic.this (for_each, vnsRedirectDest)"]:::this
  l12d["aci_pbr_l1_l2_destination.this (for_each, vnsL1L2RedirectDest)"]:::this

  oid["output: id (policy DN)"]:::out
  obp["output: backup_policy_dns (map)"]:::out
  ohg["output: health_group_dns (map)"]:::out
  odest["output: destination_dns (map)"]:::out
  ol12["output: l1_l2_destination_dns (map)"]:::out

  tdn --> this
  srp --> this
  tdn --> backup
  bp --> backup
  tdn --> health
  hg --> health
  this --> l3d
  dest --> l3d
  health -.->|"health_group_key"| l3d
  this --> l12d
  backup -.->|"backup_policy_key"| l12d
  health -.->|"health_group_key"| l12d
  l12 --> l12d

  this --> oid
  backup --> obp
  health --> ohg
  l3d --> odest
  l12d --> ol12

  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_service_redirect_policy this 1 (keystone) The PBR service redirect policy (vnsSvcRedirectPol).
aci_service_redirect_backup_policy this 0..N (for_each over backup_policies) Paired PBR backup / fallback policy (vnsBackupPol).
aci_l4_l7_redirect_health_group this 0..N (for_each over health_groups) Health-tracking group for destinations (vnsRedirectHealthGroup).
aci_destination_of_redirected_traffic this 0..N (for_each over destinations) L3 redirect destination (vnsRedirectDest).
aci_pbr_l1_l2_destination this 0..N (for_each over l1_l2_destinations) L1/L2 redirect destination (vnsL1L2RedirectDest).

βœ… Provider / Versions

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

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

  • πŸ”’ service_redirect_policy.name is immutable. Changing it forces replacement of the policy β€” and every destination and relation under its DN. Treat renames as migrations.
  • ⚠️ The parent is wired through tenant_dn, not a parent_dn attribute. Every resource in this module (aci_service_redirect_policy, aci_service_redirect_backup_policy, aci_l4_l7_redirect_health_group) is a classic (SDKv2) resource whose only parent-wiring attribute on the live schema is tenant_dn β€” there is no parent_dn alternative for these resources, so tenant_dn is the current attribute here, unlike migrated resources (e.g. aci_bridge_domain) where parent_dn supersedes a deprecated tenant_dn.
  • ℹ️ Every yes/no-style knob is a plain string on the wire. anycast_enabled, program_local_pod_only, resilient_hash_enabled, and threshold_enable are "yes"/"no" strings in the provider; this module surfaces them as bool and renders the string form in main.tf. The threshold percentages are also strings under the hood; this module surfaces them as number.
  • ⚠️ anycast_enabled and program_local_pod_only are mutually exclusive. The provider rejects both being "yes" simultaneously; this module enforces that as a plan-time validation so the conflict surfaces before apply, not as an APIC fault.
  • ℹ️ Health-group and IP-SLA relations are classic flat attributes. relation_vns_rs_ipsla_monitoring_pol, relation_vns_rs_redirect_health_group, and relation_vns_rs_l1_l2_redirect_health_group each take a raw DN string β€” unlike the typed relation_to_* "by name" objects seen on migrated resources elsewhere in this suite. This module wires the two health-group relations internally from health_group_key, resolving to the DN of a health group created in the same call.
  • ⚠️ l1_l2_destinations[*].concrete_interface_dn (the provider's relation_vns_rs_to_c_if) is REQUIRED on the live provider schema, even though the published resource documentation describes it as optional β€” the live schema wins, and this module requires it accordingly.
  • ℹ️ An L1/L2 destination's parent can be either policy type. aci_pbr_l1_l2_destination accepts a policy_based_redirect_dn pointing at either a service redirect policy or a service redirect backup policy; this module exposes that choice via l1_l2_destinations[*].backup_policy_key instead of requiring a second, near-duplicate module.
  • ⚠️ validate_relation_dn (provider default true) fails the apply if a referenced concrete interface or IP SLA monitoring policy DN does not exist β€” fix the missing object rather than disabling validation. Mistyped health_group_key / backup_policy_key values are instead caught at plan time by this module's own validation blocks.

πŸ”‘ Required APIC Roles & Privileges

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

  • Create / modify the redirect policy, its backup policy, health groups, and destinations: the tenant-admin role (or a custom role with tenant-l4-l7-services write privilege), scoped to the tenant's own security domain.
  • Referenced concrete interfaces and IP SLA monitoring policy: read privilege on each object named in l1_l2_destinations[*].concrete_interface_dn and service_redirect_policy.ip_sla_monitoring_policy_dn.

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.
  • Any concrete interface named in l1_l2_destinations[*].concrete_interface_dn already exists (typically created by an L4-L7 logical device module) so the provider's DN validation passes.
  • Any IP SLA monitoring policy named in service_redirect_policy.ip_sla_monitoring_policy_dn already exists.
  • Redirecting traffic through this policy requires a consuming contract subject or service graph template to point its PBR relation at this module's id β€” that wiring is the consumer's responsibility, not this module's.

πŸ“ Module Structure

terraform-aci-l4-l7-redirect/
β”œβ”€β”€ providers.tf     # terraform{} + required_providers (aci ~> 2.20); no provider block
β”œβ”€β”€ variables.tf     # tenant_dn + service_redirect_policy object + backup_policies / health_groups / destinations / l1_l2_destinations maps
β”œβ”€β”€ main.tf          # aci_service_redirect_policy.this (keystone) + 4 for_each siblings/children
β”œβ”€β”€ outputs.tf       # id (the policy DN) first, then name and per-collection 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 "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn                = module.tenant.id # from terraform-aci-tenant
  service_redirect_policy  = { name = "fw-pbr" }

  destinations = {
    "10.10.1.10" = { ip = "10.10.1.10" }
  }
}

output "redirect_policy_dn" {
  value = module.redirect.id # pass this as the PBR relation target on a contract subject
}

πŸ”Œ Cross-Module Contract

Consumes

Input Type Typical source
tenant_dn string (DN) terraform-aci-tenant
service_redirect_policy object({...}) caller (name + PBR posture + metadata tail + IP SLA relation DN)
backup_policies map(object({...})) caller
health_groups map(object({...})) caller
destinations map(object({...})) caller
l1_l2_destinations[*].concrete_interface_dn string (DN) terraform-aci-l4-l7-device

Emits

Output Description Consumed by
id Service redirect policy DN (uni/tn-{tenant}/svcCont/svcRedirectPol-{name}) β€” primary reference contract subjects / service graph templates (PBR redirect relation)
name Service redirect policy name composition / audit
backup_policy_dns Map of backup_policies key β†’ backup policy DN audits / downstream reference
health_group_dns Map of health_groups key β†’ health group DN audits / downstream reference
destination_dns Map of destinations key β†’ L3 destination DN audits / downstream reference
l1_l2_destination_dns Map of l1_l2_destinations key β†’ L1/L2 destination DN audits / downstream reference

πŸ“š Example Library

1 Β· Minimal β€” an L3 redirect policy with secure defaults
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn               = module.tenant.id
  service_redirect_policy = { name = "fw-pbr" }
}

πŸ’‘ The minimal call creates only the policy: destination type L3, hashing on the full source/destination/protocol tuple, threshold monitoring disabled, and resilient hashing / anycast / pod-local programming all off. No destinations exist yet.

2 Β· Choosing a different hashing algorithm
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  service_redirect_policy = {
    name              = "fw-pbr"
    hashing_algorithm = "sip"
  }
}

ℹ️ hashing_algorithm must be one of sip, dip, sip-dip-prototype. The default (sip-dip-prototype) load-balances on the fullest available tuple; narrowing to sip or dip pins flows to a destination by only the source or destination address.

3 Β· Threshold monitoring with a deny-on-down posture
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  service_redirect_policy = {
    name                   = "fw-pbr"
    threshold_enable       = true
    min_threshold_percent  = 50
    max_threshold_percent  = 100
    threshold_down_action  = "deny"
  }
}

⚠️ threshold_down_action = "deny" drops traffic outright when the destination pool falls below min_threshold_percent healthy members β€” a deliberate fail-closed posture. The default "permit" instead forwards normally (bypassing PBR) when the pool is unhealthy.

4 Β· Resilient hashing for flow stability across pool changes
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  service_redirect_policy = {
    name                   = "fw-pbr"
    resilient_hash_enabled = true
  }
}

ℹ️ resilient_hash_enabled = true keeps existing flows pinned to their current destination when the pool's membership changes, instead of rehashing the entire flow table β€” reduces mid-session disruption when adding or removing a service node.

5 Β· Anycast-addressed destination pool (opt-in)
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  service_redirect_policy = {
    name            = "fw-pbr-anycast"
    anycast_enabled = true
  }
}

⚠️ anycast_enabled and program_local_pod_only are mutually exclusive β€” setting both true fails plan-time validation. Anycast addressing lets destinations share a single virtual address across pods; pod-local programming instead confines redirect to the local pod's leaf switches.

6 Β· A single L3 destination
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn               = module.tenant.id
  service_redirect_policy = { name = "fw-pbr" }

  destinations = {
    "10.10.1.10" = { ip = "10.10.1.10", mac = "00:50:56:AA:BB:CC" }
  }
}

πŸ’‘ destinations is a map keyed by a stable natural key β€” here the destination IP itself β€” so adding a second destination later never disturbs this one's address in state.

7 Β· Multiple L3 destinations (an active/active pool)
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn               = module.tenant.id
  service_redirect_policy = { name = "fw-pbr" }

  destinations = {
    "10.10.1.10" = { ip = "10.10.1.10", mac = "00:50:56:AA:BB:CC", dest_name = "fw-node-1" }
    "10.10.1.11" = { ip = "10.10.1.11", mac = "00:50:56:AA:BB:CD", dest_name = "fw-node-2" }
  }
}

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

8 Β· A redirect health group bound to a destination
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn               = module.tenant.id
  service_redirect_policy = { name = "fw-pbr" }

  health_groups = {
    "fw-health" = { name = "fw-health" }
  }

  destinations = {
    "10.10.1.10" = {
      ip               = "10.10.1.10"
      health_group_key = "fw-health"
    }
  }
}

ℹ️ health_group_key references a key in health_groups and is wired internally to the destination's DN-based health-group relation β€” no manual DN plumbing required. A destination that stops responding to health tracking is pulled from the active pool.

9 Β· A paired PBR backup policy
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn               = module.tenant.id
  service_redirect_policy = { name = "fw-pbr" }

  backup_policies = {
    "fw-pbr-backup" = { name = "fw-pbr-backup" }
  }
}

ℹ️ A backup policy is a tenant-scoped sibling of the primary policy, not nested under its DN. Its own L1/L2 destinations are attached separately via l1_l2_destinations[*].backup_policy_key β€” see example 11.

10 Β· An L1/L2 destination bound to a concrete interface
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn               = module.tenant.id
  service_redirect_policy = { name = "transparent-fw-pbr" }

  l1_l2_destinations = {
    "fw-l1l2-1" = {
      destination_name      = "fw-l1l2-1"
      concrete_interface_dn = module.l4l7_device.concrete_interface_dns["fw-node-1-if1"]
    }
  }
}

⚠️ concrete_interface_dn is required on the live provider schema for every L1/L2 destination β€” it is what makes transparent (non-routed) service insertion possible.

11 Β· An L1/L2 destination under the backup policy instead of the primary
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn               = module.tenant.id
  service_redirect_policy = { name = "transparent-fw-pbr" }

  backup_policies = {
    "fw-pbr-backup" = { name = "fw-pbr-backup" }
  }

  l1_l2_destinations = {
    "fw-l1l2-backup-1" = {
      destination_name      = "fw-l1l2-backup-1"
      concrete_interface_dn = module.l4l7_device.concrete_interface_dns["fw-node-2-if1"]
      backup_policy_key     = "fw-pbr-backup"
    }
  }
}

ℹ️ Setting backup_policy_key creates this L1/L2 destination under the named backup policy's DN instead of the primary keystone's β€” the key must match an entry in backup_policies, validated at plan time.

12 Β· Binding to an IP SLA monitoring policy
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  service_redirect_policy = {
    name                        = "fw-pbr"
    ip_sla_monitoring_policy_dn = module.ip_sla.id
  }
}

ℹ️ ip_sla_monitoring_policy_dn is a classic flat relation β€” a raw DN, unlike the "by name" typed relations on migrated resources elsewhere in this suite. The referenced IP SLA monitoring policy must exist so the provider's DN validation passes.

13 Β· A custom annotation and GUI alias
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  service_redirect_policy = {
    name       = "fw-pbr"
    name_alias = "Firewall-PBR"
    annotation = "orchestrator:terraform:platform-team"
  }
}

πŸ”’ Keep the orchestrator:terraform prefix so Terraform-managed objects stay identifiable in APIC. This suite defaults annotation to orchestrator:terraform; override it only to extend, not to erase, that marker.

14 Β· A fully-specified PBR configuration (all pieces combined)
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn = module.tenant.id
  service_redirect_policy = {
    name                   = "fw-pbr"
    description            = "Firewall PBR β€” production web tier"
    hashing_algorithm      = "sip-dip-prototype"
    resilient_hash_enabled = true
    threshold_enable       = true
    min_threshold_percent  = 50
    max_threshold_percent  = 100
    threshold_down_action  = "bypass"
  }

  backup_policies = {
    "fw-pbr-backup" = { name = "fw-pbr-backup" }
  }

  health_groups = {
    "fw-health" = { name = "fw-health" }
  }

  destinations = {
    "10.10.1.10" = { ip = "10.10.1.10", dest_name = "fw-node-1", health_group_key = "fw-health" }
    "10.10.1.11" = { ip = "10.10.1.11", dest_name = "fw-node-2", health_group_key = "fw-health" }
  }
}
15 Β· Many redirect policies from one definition (caller-side for_each)
locals {
  redirect_policies = {
    "fw-pbr" = {
      service_redirect_policy = { name = "fw-pbr" }
      destinations             = { "10.10.1.10" = { ip = "10.10.1.10" } }
    }
    "lb-pbr" = {
      service_redirect_policy = { name = "lb-pbr" }
      destinations             = { "10.10.2.10" = { ip = "10.10.2.10" } }
    }
  }
}

module "redirects" {
  source   = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"
  for_each = local.redirect_policies

  tenant_dn               = module.tenant.id
  service_redirect_policy = each.value.service_redirect_policy
  destinations            = each.value.destinations
}

output "redirect_policy_dns" {
  value = { for k, m in module.redirects : k => m.id }
}

πŸ’‘ Instantiate the module with for_each at the caller level to manage a fleet of redirect policies (one per service tier or device pair) from a single, auditable map.

16 Β· πŸ—οΈ End-to-end composition β€” tenant β†’ L4-L7 device β†’ redirect policy β†’ contract subject
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) An L4-L7 logical device exposing concrete interfaces for L1/L2 insertion.
module "l4l7_device" {
  source    = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-device.git?ref=v1.0.0"
  tenant_dn = module.tenant.id
  device    = { name = "fw-cluster" }
}

# 3) This module: a redirect policy with a health group and an L3 destination.
module "redirect" {
  source = "git::https://github.com/microsoftexpert/terraform-aci-l4-l7-redirect.git?ref=v1.0.0"

  tenant_dn               = module.tenant.id
  service_redirect_policy = { name = "fw-pbr", resilient_hash_enabled = true }

  health_groups = {
    "fw-health" = { name = "fw-health" }
  }

  destinations = {
    "10.10.1.10" = { ip = "10.10.1.10", dest_name = "fw-node-1", health_group_key = "fw-health" }
  }
}

# 4) A contract whose subject points its PBR relation at this policy's DN.
module "contract" {
  source    = "git::https://github.com/microsoftexpert/terraform-aci-contract.git?ref=v1.0.0"
  tenant_dn = module.tenant.id
  contract  = { name = "web-to-app" }

  subjects = {
    "web-to-fw" = {
      name              = "web-to-fw"
      redirect_policy_dn = module.redirect.id
    }
  }
}

output "redirect_policy_dn" { value = module.redirect.id }

πŸ—οΈ One tenant, one L4-L7 device, and one redirect policy in; a fully health-tracked PBR path out β€” the contract subject binds to it purely by consuming module.redirect.id. This module is the traffic-steering anchor between the device's data plane and every contract that redirects through it.

πŸ“₯ Inputs

Name Type Required Default Description
tenant_dn string βœ… β€” Parent tenant DN (uni/tn-{name}); wired to every resource's tenant_dn.
service_redirect_policy object({...}) βœ… β€” The redirect policy: name, PBR posture, metadata tail, and IP SLA relation DN.
backup_policies map(object({...})) βž– {} Paired PBR backup policies, keyed by a stable natural key.
health_groups map(object({...})) βž– {} Redirect health groups, keyed by a stable natural key.
destinations map(object({...})) βž– {} L3 destinations under the policy, keyed by destination IP.
l1_l2_destinations map(object({...})) βž– {} L1/L2 destinations, keyed by destination name.
Full input schema (from variables.tf)
variable "tenant_dn" {
  type = string
  # validation: must match ^uni/tn- (a tenant DN)
}

variable "service_redirect_policy" {
  type = object({
    name                        = string                                     # REQUIRED, immutable (force-new), 1-64 chars
    description                 = optional(string, null)
    dest_type                   = optional(string, "L3")                     # L1 | L2 | L3
    hashing_algorithm           = optional(string, "sip-dip-prototype")      # sip | dip | sip-dip-prototype
    threshold_down_action       = optional(string, "permit")                 # bypass | deny | permit
    anycast_enabled             = optional(bool, false)                     # mutually exclusive with program_local_pod_only
    program_local_pod_only      = optional(bool, false)                     # mutually exclusive with anycast_enabled
    resilient_hash_enabled      = optional(bool, false)
    threshold_enable            = optional(bool, false)
    min_threshold_percent       = optional(number, null)                    # 0-100
    max_threshold_percent       = optional(number, null)                    # 0-100
    annotation                  = optional(string, "orchestrator:terraform")
    name_alias                  = optional(string, null)
    ip_sla_monitoring_policy_dn = optional(string, null)                    # classic flat relation, raw DN
  })
  # validations: name length/charset; dest_type/hashing_algorithm/threshold_down_action enums;
  #              anycast_enabled/program_local_pod_only mutual exclusion; threshold percent ranges
}

variable "backup_policies" {
  type = map(object({
    name       = string                                     # REQUIRED, immutable, 1-64 chars
    annotation = optional(string, "orchestrator:terraform")
    name_alias = optional(string, null)
  }))
  default = {}
  # validations: name length/charset
}

variable "health_groups" {
  type = map(object({
    name        = string                                     # REQUIRED, immutable, 1-64 chars
    description = optional(string, null)
    annotation  = optional(string, "orchestrator:terraform")
    name_alias  = optional(string, null)
  }))
  default = {}
  # validations: name length/charset
}

variable "destinations" {
  type = map(object({
    ip               = string                                # REQUIRED
    ip2              = optional(string, null)
    mac              = optional(string, null)                # required by APIC releases before 5.2
    dest_name        = optional(string, null)
    pod_id           = optional(number, 1)                    # 1-255
    health_group_key = optional(string, null)                 # key into var.health_groups
    description      = optional(string, null)
    annotation       = optional(string, "orchestrator:terraform")
    name_alias       = optional(string, null)
  }))
  default = {}
  # validations: pod_id range; health_group_key must exist in var.health_groups
}

variable "l1_l2_destinations" {
  type = map(object({
    destination_name      = string                            # REQUIRED
    name                  = optional(string, null)
    mac                   = optional(string, null)
    pod_id                = optional(number, 1)                # 1-255
    concrete_interface_dn = string                             # REQUIRED on the live provider schema
    health_group_key      = optional(string, null)             # key into var.health_groups
    backup_policy_key     = optional(string, null)             # key into var.backup_policies
    description           = optional(string, null)
    annotation            = optional(string, "orchestrator:terraform")
    name_alias            = optional(string, null)
  }))
  default = {}
  # validations: destination_name length; pod_id range; health_group_key / backup_policy_key existence
}

🧾 Outputs

Output Description Notes
id Service redirect policy Distinguished Name (uni/tn-{tenant}/svcCont/svcRedirectPol-{name}) Primary cross-module reference β€” pass as the PBR relation target on a contract subject / service graph template.
name Service redirect policy name For composition / audit.
backup_policy_dns Map of backup_policies key β†’ backup policy DN For downstream reference / audit.
health_group_dns Map of health_groups key β†’ health group DN For downstream reference / audit.
destination_dns Map of destinations key β†’ L3 destination DN For downstream reference / audit.
l1_l2_destination_dns Map of l1_l2_destinations key β†’ L1/L2 destination DN For downstream reference / audit.

🧠 Architecture Notes

  • One keystone, four for_each collections. aci_service_redirect_policy.this is the keystone; aci_service_redirect_backup_policy.this and aci_l4_l7_redirect_health_group.this are tenant-scoped siblings (parented by tenant_dn, not the keystone's DN), while aci_destination_of_redirected_traffic.this and aci_pbr_l1_l2_destination.this are true children (parented by the keystone's or a backup policy's DN).
  • All five resources are classic (SDKv2). None expose a parent_dn alternative; every parent-wiring attribute (tenant_dn, service_redirect_policy_dn, policy_based_redirect_dn) is the current, non-deprecated attribute for its resource.
  • Internal by-key relation wiring. destinations[*].health_group_key, l1_l2_destinations[*].health_group_key, and l1_l2_destinations[*].backup_policy_key reference natural keys in this module's own health_groups / backup_policies maps; main.tf resolves each to the sibling resource's DN with a conditional expression, and variables.tf validates the key exists before plan reaches the resource graph.
  • Boolean and numeric ergonomics. Every yes/no-style knob and both threshold percentages are provider strings; this module surfaces them as bool / number and renders the string form (tostring(), ? "yes" : "no") in main.tf.
  • Stable for_each keys. Every collection is keyed by a stable natural key (policy name, health-group name, destination IP, or destination name) β€” never count β€” so inserting or removing one entry never disturbs another's address in state.
  • Secure by default. The minimal call yields destination type L3, full-tuple hashing, threshold monitoring disabled, and every permissive flag (anycast_enabled, program_local_pod_only, resilient_hash_enabled, threshold_enable) off.

🧱 Design Principles

Concern Secure default How to opt out (deliberately)
service_redirect_policy.dest_type L3 β€” routed redirect Set L1 / L2 for transparent service insertion via l1_l2_destinations.
service_redirect_policy.threshold_enable false β€” no threshold-based bypass Set true with min_threshold_percent / max_threshold_percent to fail predictably when the pool degrades.
service_redirect_policy.anycast_enabled / program_local_pod_only false / false Set exactly one true for anycast or pod-local programming; both true is rejected at plan time.
service_redirect_policy.resilient_hash_enabled false Set true to preserve existing flows' hash bucket across pool membership changes.
service_redirect_policy.annotation orchestrator:terraform β€” Terraform-managed objects stay identifiable in APIC Extend the marker; do not blank it.
service_redirect_policy.ip_sla_monitoring_policy_dn null β€” no relation forced Set explicitly to bind IP SLA monitoring.
destinations[*].health_group_key / l1_l2_destinations[*].health_group_key null β€” no health tracking forced Set to a key in health_groups to bind health tracking.
Transport (provider) This suite instructs callers to set insecure = false with CA trust The provider default is insecure = true; do not keep it as a steady state.
Secrets None accepted or emitted n/a β€” this module carries no secret material; credentials are provider config.

πŸš€ Runbook

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

πŸ§ͺ Testing

The offline proof gate for this module:

  • βœ… terraform validate β€” parses the module, resolves the service_redirect_policy and every collection's object type, runs the name/enum/mutual-exclusion/range/key-existence 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 concrete_interface_dn and ip_sla_monitoring_policy_dn (server-side validate_relation_dn), APIC-side name-collision checks, health-group up/down state, and the computed DNs returned as id / the per-collection DN maps.

πŸ’¬ Example Output

$ terraform output
id                     = "uni/tn-core-prod/svcCont/svcRedirectPol-fw-pbr"
name                   = "fw-pbr"
backup_policy_dns      = {}
health_group_dns       = {
  "fw-health" = "uni/tn-core-prod/svcCont/redirectHealthGroup-fw-health"
}
destination_dns        = {
  "10.10.1.10" = "uni/tn-core-prod/svcCont/svcRedirectPol-fw-pbr/RedirectDest_ip-[10.10.1.10]"
}
l1_l2_destination_dns  = {}

πŸ” Troubleshooting

Symptom Cause Fix
service_redirect_policy.name must be 1-64 characters Name is empty or too long Use a 1-64 character name.
service_redirect_policy.name may contain only letters, digits, and the characters _ . : - Name has spaces or unsupported characters Remove spaces/special characters (ACI naming rules).
Changing service_redirect_policy.name wants to destroy/recreate the policy name is immutable (force-new) Treat a rename as a migration; expect the policy and its destinations to be replaced.
anycast_enabled and program_local_pod_only cannot both be true Both flags set true in the same call Set only one, matching the intended addressing model.
... must be one of: L1, L2, L3 (or similar enum error) A posture field is outside its closed enum Use one of the documented values for that field.
health_group_key must be null or a key present in var.health_groups Typo, or the health group wasn't added to health_groups Add the health group entry, or correct the key.
backup_policy_key must be null or a key present in var.backup_policies Typo, or the backup policy wasn't added to backup_policies Add the backup policy entry, or correct the key.
Apply fails validating concrete_interface_dn / ip_sla_monitoring_policy_dn The referenced object does not exist yet Create the referenced object first (or in the same apply); do not disable validate_relation_dn.
Traffic isn't being redirected at all No consumer (contract subject / service graph template) points its PBR relation at this module's id Wire module.redirect.id into the consuming subject's redirect relation.
Post ... 401 / authentication error Provider not configured or wrong credentials Configure the aci provider with valid credentials and url; prefer signature auth for automation.

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."

About

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

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages