Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🔴 F5 BIG-IP LTM Node Terraform Module

terraform-bigip-ltm-node manages a single bigip_ltm_node object -- one addressable server IP (or FQDN-resolved node) on a BIG-IP TMOS >= v12.1.1 device, via the F5Networks/bigip Terraform provider ~> 1.28.

Terraform Provider Module Type Resources Posture


🧩 Overview

  • 🖥️ Creates one bigip_ltm_node -- a static IP/hostname node, or an FQDN-resolved node when var.fqdn is set.
  • 🧵 Renders the single-item fqdn nested block as a dynamic block so a null (the default) never forces a placeholder into the plan.
  • 🏷️ Emits name (full path) as the primary cross-reference key, alongside id, address, monitor, state, and session.
  • 🚫 Owns nothing else -- no pool membership, no monitor. Both are consumed by full-path name from sibling modules.
  • 🔓 Deliberately partition-agnostic in its own inputs: partition is folded into name's full-path convention (/Partition/name), never a separate always-present variable.

💡 Why it matters: a BIG-IP node is the one LTM object most likely to be shared across several pools at once -- a single application server backing both an HTTP pool and a management pool, for example. Modeling it as its own standalone module (rather than an inline attribute of the pool that first references it) means the node's lifecycle -- and its administrative state (user-up/user-down) -- is never accidentally tied to any one pool's terraform apply.


❤️ 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

flowchart LR
 MON["terraform-bigip-ltm-monitor"]
 NODE["terraform-bigip-ltm-node (this module)"]
 RES["bigip_ltm_node.this"]
 POOL["terraform-bigip-ltm-pool"]
 VS["terraform-bigip-ltm-virtual-server"]

 MON -->|"monitor full-path name, optional"| NODE
 NODE -->|"renders"| RES
 RES -->|"name and address consumed by member map"| POOL
 POOL -->|"pool name consumed by"| VS

 style NODE fill:#E4002B,color:#ffffff
 style RES fill:#000000,color:#ffffff
 style MON fill:#ECECEC,color:#000000
 style POOL fill:#ECECEC,color:#000000
 style VS fill:#ECECEC,color:#000000
Loading

terraform-bigip-ltm-monitor is an optional upstream sibling -- most designs in this catalog put health checking on the pool rather than the node, so the monitor edge above is frequently unused. terraform-bigip-ltm-pool is the primary downstream consumer, referencing this module's name/address outputs from its member map. terraform-bigip-ltm-virtual-server is shown for full-chain context; it never references a node directly.


🧬 What this builds

flowchart TB
 subgraph Inputs["Inputs"]
 NAME["var.name"]
 ADDR["var.address"]
 DESC["var.description"]
 CONN["var.connection_limit"]
 DYN["var.dynamic_ratio"]
 RATIO["var.ratio"]
 MONV["var.monitor"]
 RATE["var.rate_limit"]
 STATE["var.state"]
 SESSION["var.session"]
 FQDN["var.fqdn, optional block"]
 end

 RES["bigip_ltm_node.this"]

 NAME --> RES
 ADDR --> RES
 DESC --> RES
 CONN --> RES
 DYN --> RES
 RATIO --> RES
 MONV --> RES
 RATE --> RES
 STATE --> RES
 SESSION --> RES
 FQDN -->|"dynamic block, zero or one"| RES

 subgraph Outputs["Outputs"]
 ONAME["name"]
 OID["id"]
 OADDR["address"]
 OMON["monitor"]
 OSTATE["state"]
 OSESSION["session"]
 end

 RES --> ONAME
 RES --> OID
 RES --> OADDR
 RES --> OMON
 RES --> OSTATE
 RES --> OSESSION

 style RES fill:#E4002B,color:#ffffff
Loading

Resource inventory (1 resource, no children):

Resource Count Notes
bigip_ltm_node.this 1 (single keystone) The only resource this module manages. The fqdn nested block is rendered via dynamic, not a separate resource.

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
F5Networks/bigip provider ~> 1.28 (re-verify against the Registry before each new module wave)
Provider block None -- the caller's root module configures address/username/password/token_value
BIG-IP TMOS >= v12.1.1 (provider floor)

Schema notes that bite:

  • address is required: true on the live schema unconditionally -- including for FQDN-based nodes. Do not assume it can be omitted when fqdn is set; BIG-IP overwrites the resolved address at runtime, but Terraform still requires a value at plan time.
  • fqdn.address_family and fqdn.autopopulate defaults disagree between the prose Argument Reference and the live provider schema (the schema documents "unspecified" for address_family where the prose says "all"/"ipv4"/"ipv6", and does not confirm an accepted value set for autopopulate). This module leaves both null unless the caller supplies them, so the provider's own computed default applies rather than this module guessing wrong.
  • ratio is optional, computed on the live schema but absent from the prose Argument Reference; confirmed present and defaulting to 1.
  • rate_limit takes the literal string "disabled" as its off-state, not a boolean or 0 -- passing a number as a string is required to set an actual limit.
  • state and session are each closed to exactly two values (user-up/user-down and user-enabled/user-disabled respectively) -- enforced here via variable validation blocks, not left to the provider to reject at apply time.
  • Full-path identity (/Partition/name) is partition-specific -- moving a node between partitions is a destroy/recreate, not an in-place rename.

🔑 Required BIG-IP User Role / Partition Access

Manager role scoped to the target partition is sufficient; Administrator is not required for this application-layer LTM object.


F5 BIG-IP Prerequisites

  • iControl REST enabled and reachable on the target device.
  • TMOS >= v12.1.1 (provider floor).
  • Target partition must already exist.
  • If fqdn is set, the device must be able to resolve the configured fqdn.name via its configured DNS resolver for the node to populate a live address.

📁 Module Structure

File Role
providers.tf required_version >= 1.12.0, pinned F5Networks/bigip ~> 1.28 -- no provider {} block
variables.tf 1:1 bigip_ltm_node argument schema, including the fqdn object block, verified against the live provider schema
main.tf Single keystone resource bigip_ltm_node.this; fqdn rendered as a dynamic block
outputs.tf name, id, address, monitor, state, session
SCOPE.md Lightweight standalone scope: consumed-by-name relationships, required role, F5 prerequisites
README.md This document

⚙️ Quick Start

# Caller's root module configures the provider once -- never inside this module.
# provider "bigip" {
# address = var.bigip_address
# username = var.bigip_username
# password = var.bigip_password
# }

module "web01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/web01-node"
  address = "10.20.30.11"
}

🔌 Cross-Module Contract

Consumes:

Input Type Source module
monitor (optional) string (full-path name) terraform-bigip-ltm-monitor

Emits:

Output Description Consumed by
name Full-path node name (e.g. /Common/my-node) terraform-bigip-ltm-pool member map
id Provider-internal id (rarely consumed directly)
address IP address or hostname configured for the node terraform-bigip-ltm-pool member map (paired with name)
monitor Full-path name of the node-level monitor, if any (diagnostics / reporting only)
state Administrative state (user-up / user-down) (diagnostics / reporting only)
session Session-enablement state (user-enabled / user-disabled) (diagnostics / reporting only)

📚 Example Library

1 · Minimal static node

The smallest real call -- only the two required arguments.

module "app01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/app01-node"
  address = "10.20.30.11"
}

ℹ️ Every other argument falls back to the provider's own default (connection_limit = 0, rate_limit = "disabled", state = "user-up", session = "user-enabled").

2 · Node with a description
module "app01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name        = "/Common/app01-node"
  address     = "10.20.30.11"
  description = "App01 -- order-processing tier, rack 4"
}
3 · Explicit connection limit (secure-by-default guardrail)
module "app01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name             = "/Common/app01-node"
  address          = "10.20.30.11"
  connection_limit = 25000
}

⚠️ connection_limit = 0 (unlimited) is the provider's own default and this module's default -- per this module suite's secure-by-default guidance, that default is accepted but surfaced as an explicit, guardrail-visible input. Set a real ceiling for production nodes rather than relying on the default.

4 · Dynamic-ratio load balancing weight
module "app01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name          = "/Common/app01-node"
  address       = "10.20.30.11"
  dynamic_ratio = 3
}

💡 Only relevant when a pool consuming this node uses a dynamic-ratio load-balancing mode.

5 · Static ratio weight
module "app02_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/app02-node"
  address = "10.20.30.12"
  ratio   = 2
}

💡 Only relevant when a pool consuming this node uses a ratio load-balancing mode.

6 · Node-level health monitor
module "app01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/app01-node"
  address = "10.20.30.11"
  monitor = module.icmp_monitor.name # from terraform-bigip-ltm-monitor
}

ℹ️ Node-level monitoring is optional and, in this catalog's design, typically secondary to pool-level monitors lists -- one node commonly backs several pools, each with its own monitor.

7 · Explicit connections-per-second rate limit
module "app01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name       = "/Common/app01-node"
  address    = "10.20.30.11"
  rate_limit = "1000"
}

⚠️ rate_limit is a string field -- the provider's off-state sentinel is the literal string "disabled" (this module's default), not a boolean or 0.

8 · Administratively disabling a node
module "app01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/app01-node"
  address = "10.20.30.11"
  state   = "user-down"
}

🔒 Sets the node administratively down without removing it from the configuration -- pools consuming it stop sending new traffic to it, existing connections are handled per the pool's own connection-draining behavior.

9 · Disabling new sessions only
module "app01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/app01-node"
  address = "10.20.30.11"
  session = "user-disabled"
}

ℹ️ Unlike state = "user-down", session = "user-disabled" keeps the node "up" for monitor/health purposes while refusing new connections -- a softer drain than administratively disabling it.

10 · Minimal FQDN-resolved node
module "db_primary_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/db-primary-node"
  address = "255.255.255.254" # placeholder; overwritten once BIG-IP resolves fqdn.name

  fqdn = {
    name = "db-primary.internal.example.org"
  }
}

⚠️ address is still required at plan time even for FQDN nodes -- the live schema requires it unconditionally. BIG-IP populates/overwrites the resolved address once DNS resolution occurs.

11 · FQDN node with a custom resolution interval
module "db_primary_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/db-primary-node"
  address = "255.255.255.254"

  fqdn = {
    name     = "db-primary.internal.example.org"
    interval = "300"
  }
}

💡 interval (in seconds) defaults to "3600"; shorten it for records that change frequently (e.g. a database failover CNAME).

12 · FQDN node with a custom down-detection threshold
module "db_primary_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/db-primary-node"
  address = "255.255.255.254"

  fqdn = {
    name         = "db-primary.internal.example.org"
    downinterval = 10
  }
}

ℹ️ downinterval is the number of failed resolution attempts before BIG-IP considers the FQDN name down; provider default is 5.

13 · FQDN node with an explicit address family override
module "db_primary_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name    = "/Common/db-primary-node"
  address = "255.255.255.254"

  fqdn = {
    name           = "db-primary.internal.example.org"
    address_family = "ipv4"
  }
}

⚠️ Left null by default because the prose Argument Reference and the live schema disagree on the provider's own default value -- only set this explicitly when the caller needs to force a specific address family.

14 · Multiple nodes via root-level `for_each`

This module models exactly one node per call (there is no in-module child collection to for_each over), so a caller creating several nodes wraps the module call itself at the root, keyed on a stable identifier -- never count.

locals {
  nodes = {
    "app01" = { address = "10.20.30.11" }
    "app02" = { address = "10.20.30.12" }
    "app03" = { address = "10.20.30.13" }
  }
}

module "app_nodes" {
  source   = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"
  for_each = local.nodes

  name    = "/Common/${each.key}-node"
  address = each.value.address
}

💡 Keying on each.key (the stable node name) rather than a list index means removing app02 never forces Terraform to touch app01's or app03's state.

15 · 🏗️ End-to-end composition

A node feeding a monitor-backed pool feeding a virtual server -- the full chain this module participates in.

module "icmp_monitor" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-monitor.git?ref=v1.0.0"

  name = "/Common/app-icmp-monitor"
  #... monitor-specific arguments
}

module "app01_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name             = "/Common/app01-node"
  address          = "10.20.30.11"
  connection_limit = 25000
}

module "app02_node" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-node.git?ref=v1.0.0"

  name             = "/Common/app02-node"
  address          = "10.20.30.12"
  connection_limit = 25000
}

module "app_pool" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-pool.git?ref=v1.0.0"

  name     = "/Common/app-pool"
  monitors = [module.icmp_monitor.name]

  members = {
    "${module.app01_node.address}:8080" = {
      node_name = module.app01_node.name
      port      = 8080
    }
    "${module.app02_node.address}:8080" = {
      node_name = module.app02_node.name
      port      = 8080
    }
  }
}

module "app_virtual_server" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-virtual-server.git?ref=v1.0.0"

  name        = "/Common/app-vs"
  destination = "10.20.30.100:443"
  pool_name   = module.app_pool.name
  #... profile/persistence references
}

🏗️ This module's contribution to the chain is narrow and intentional: it owns exactly the node record and nothing downstream. The pool decides how the node is health-checked (via its own monitors list) and load-balanced; the virtual server decides how traffic reaches the pool. Ordering matters -- nodes and monitors before the pool, the pool before the virtual server -- and Terraform's implicit dependency graph (via module.X.name references) enforces it automatically.


📥 Inputs

Summary:

Variable Type Default Required
name string -- ✅
address string -- ✅
description string null --
connection_limit number 0 --
dynamic_ratio number 1 --
ratio number 1 --
monitor string null --
rate_limit string "disabled" --
state string "user-up" --
session string "user-enabled" --
fqdn object(...) null --
Full object schema -- var.fqdn
variable "fqdn" {
  type = object({
    name           = optional(string)
    interval       = optional(string, "3600")
    address_family = optional(string)
    autopopulate   = optional(string)
    downinterval   = optional(number, 5)
  })
  default = null
}
Field Type Default Notes
name string unset FQDN of the node.
interval string "3600" Seconds between DNS queries.
address_family string null (provider-computed) Prose docs and live schema disagree on the provider default -- left unset unless supplied.
autopopulate string null (provider-computed) Accepted value set not confirmed against the live schema -- no closed-enum validation applied; left unset unless supplied.
downinterval number 5 Resolution attempts before the name is considered down.

🧾 Outputs

Output Description Sensitive
name Full-path name of the LTM node (e.g. /Common/my-node) -- primary cross-reference key No
id Provider-internal id of the node resource No
address IP address or hostname configured for the node No
monitor Full-path name of the node-level monitor, if any No
state Administrative state (user-up / user-down) No
session Session-enablement state (user-enabled / user-disabled) No

🧠 Architecture Notes

  • No child collections. bigip_ltm_node has no for_each-able sub-resource -- the only nested structure is the single-item fqdn block, rendered as a dynamic block so an absent var.fqdn never emits a null-block placeholder into the plan.
  • address is unconditionally required, including for FQDN nodes -- do not build calling code that tries to omit it when fqdn is set.
  • monitor is node-level and optional, distinct from a pool's required monitors list -- this catalog's default design puts health checking on the pool, since one node commonly backs several pools with different monitoring needs.
  • state and session are closed two-value enums, enforced via variable validation blocks in variables.tf rather than left for the provider to reject at apply time -- a typo surfaces at terraform validate, before any HTTP call reaches the device.
  • Full-path identity is partition-specific. name in the form /Partition/name is the practical cross-reference key sibling modules use; moving a node between partitions is a destroy/recreate operation, not an in-place update.
  • for_each, never count, at the call site. Because this module models one node per call, a caller creating multiple nodes wraps the module invocation itself with for_each over a keyed map (see Example 14) -- never count, so removing one node never re-indexes its siblings' state.

🧱 Design Principles

Concern Secure default in this module Opt-out (caller must type extra)
Connection limits connection_limit = 0 (unlimited) matches the provider's own default, but is surfaced as an explicit, guardrail-visible input rather than silently inherited Caller explicitly sets 0 to accept unlimited; production nodes should set a real ceiling
Health monitoring Node-level monitor defaults to null -- by design, health checking is expected to live on the consuming pool's required monitors list, not silently assumed absent everywhere Caller sets monitor explicitly for node-level checks in addition to (or instead of) pool-level monitoring
Administrative state state = "user-up" / session = "user-enabled" -- new nodes are live by default, matching the provider's own default; both are closed-enum validated Caller sets "user-down" / "user-disabled" explicitly to stage a node before it takes traffic
Partition scope No implicit partition is invented -- partition is folded into the full-path name the caller supplies Caller includes the target partition segment in name for any non-/Common object
Secrets Not applicable -- bigip_ltm_node has no secret-shaped attributes N/A

🚀 Runbook

cd C:\GitHubCode\newf5modules\bigip\terraform-bigip-ltm-node
terraform init -backend=false
terraform validate
terraform fmt -check
Remove-Item -Recurse -Force.terraform,.terraform.lock.hcl -ErrorAction SilentlyContinue

Pin consuming module calls to ?ref=v1.0.0 (or the current tagged release) rather than an unpinned branch reference.


🧪 Testing

This module's local proof gate is plan-only, schema-level validation -- terraform init -backend=false && terraform validate && terraform fmt -check confirm the HCL is syntactically valid, every required argument is present, and the fqdn object shape matches what main.tf expects. It does not exercise real device state: it cannot confirm the target partition exists, that fqdn.name actually resolves, or that the supplied address is reachable. A human runs terraform plan/apply against an actual BIG-IP from CI, never from this authoring process.


💬 Example Output

module.app01_node.bigip_ltm_node.this: Creating...
module.app01_node.bigip_ltm_node.this: Creation complete after 1s [id=/Common/app01-node]

Outputs:

address = "10.20.30.11"
id = "/Common/app01-node"
monitor = ""
name = "/Common/app01-node"
session = "user-enabled"
state = "user-up"

🔍 Troubleshooting

Symptom Cause Fix
terraform validate fails with a missing-argument error on address fqdn was set without also supplying address, on the assumption the provider computes it Supply an explicit address value even for FQDN nodes -- the live schema requires it unconditionally at plan time
Plan shows a full replace of the node instead of an in-place update name's partition segment changed (e.g. /Common/x -> /Tenant-A/x) Expected -- full-path identity is partition-specific; moving partitions is a destroy/recreate, not a rename
state/session rejected at terraform validate A value outside the two accepted enum members was supplied Use exactly "user-up"/"user-down" for state and "user-enabled"/"user-disabled" for session
Node stays reachable in monitor status after setting state = "user-down" state and session are independent controls -- user-down affects administrative/traffic state, not necessarily monitor-visible health Confirm which control the operational runbook actually needs; use session = "user-disabled" for a traffic-only drain
Pool member plan shows an unexpected replace, not update, after changing this node's address The consuming pool's member map is keyed on ip:port (see terraform-bigip-ltm-pool) -- changing the node's address changes that key Expected per that module's for_each design; not a bug in this module
connection refused on first apply against a cloud-deployed BIG-IP Management interface reachable only on a non-443 port (common on single-NIC AWS/Azure/GCP deployments) Set the provider's port argument (env BIGIP_PORT) at the root module -- not a concern of this module

🔗 Related Docs

Releases

Packages

Contributors

Languages