Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ”΄ F5 BIG-IP LTM Data Group Terraform Module

terraform-bigip-ltm-datagroup manages a single bigip_ltm_datagroup β€” an LTM data group (string/integer/address lookup table) β€” against the F5Networks/bigip Terraform provider ~> 1.28, TMOS >= v12.1.1.

Terraform Provider Module Version Module Type Resources


🧩 Overview

  • πŸ“‡ Manages one bigip_ltm_datagroup.this β€” a key:= value lookup table of type string, ip, or integer, consumed by full-path name from iRules and local traffic policies.
  • 🧡 Renders the provider's only nested/repeating block, record, as a dynamic block over a map(object({ data = optional(string) })) keyed by each record's own name β€” adding or removing one record never re-indexes its siblings in the plan (for_each, never count).
  • πŸ”€ Supports both internal data groups (internal = true, the provider default β€” records managed inline via records) and external data groups (internal = false β€” records sourced from a file already staged on the target device via records_src).
  • πŸ”’ Requires type to be declared explicitly with no provider-side default β€” closed to string/ip/integer via a validation{} block, so a typo is caught at terraform validate time rather than surfacing as a device-side API error.
  • πŸ”— Emits name (the practical full-path cross-reference key) and id, consumed by terraform-bigip-ltm-irule (TCL class match/class lookup references) and terraform-bigip-ltm-policy (rule condition/action references) β€” this module never creates or references either.

πŸ’‘ Why it matters: A data group turns a lookup table that would otherwise be hardcoded inline inside an iRule's TCL body, or scattered across several policy rules, into one independently versioned Terraform object. Changing an allow-list, a routing table, or a per-tenant config map becomes a diff against var.records, not a search-and-replace inside script text.


❀️ 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
 DG["terraform-bigip-ltm-datagroup"]:::this
 IRULE["terraform-bigip-ltm-irule"]:::target
 POLICY["terraform-bigip-ltm-policy"]:::target
 VS["terraform-bigip-ltm-virtual-server"]:::sibling

 DG -- "name (full path, class match/lookup in TCL)" --> IRULE
 DG -- "name (full path, rule condition/action)" --> POLICY
 IRULE -- "name (irules list)" --> VS
 POLICY -- "name (policies list)" --> VS

 classDef this fill:#E4002B,color:#ffffff,stroke:#7a0016,stroke-width:1px;
 classDef target fill:#000000,color:#ffffff,stroke:#000000,stroke-width:1px;
 classDef sibling fill:#D9D9D9,color:#000000,stroke:#999999,stroke-width:1px;
Loading

This module (red) has no upstream dependency of its own β€” name, type, and records are all caller-supplied literals. It is consumed by full-path name from two direct, equally-primary targets (black): terraform-bigip-ltm-irule (a data group referenced inside a TCL class command) and terraform-bigip-ltm-policy (a data group referenced from a rule condition or action). Neither target is owned by this module β€” both consume the name output as a plain string. terraform-bigip-ltm-virtual-server is shown for full-chain context (it attaches the iRule/policy that in turn references this data group); it never references a data group directly.


🧬 What this builds

flowchart TD
 subgraph "Identity"
 NAME["name, type"]
 end
 subgraph "Record Source"
 SRC["internal, records_src"]
 end
 subgraph "Records (for_each)"
 REC["record (dynamic block, keyed by record name)"]
 end

 RES(["bigip_ltm_datagroup.this"]):::keystone

 NAME --> RES
 SRC --> RES
 REC -- "for_each over var.records" --> RES

 RES --> OUT_NAME["output: name"]
 RES --> OUT_ID["output: id"]
 RES --> OUT_TYPE["output: type"]
 RES --> OUT_INTERNAL["output: internal"]
 RES --> OUT_RECNAMES["output: record_names"]

 classDef keystone fill:#000000,color:#ffffff,stroke:#000000,stroke-width:1px;
Loading

Resource inventory: exactly one resource, bigip_ltm_datagroup.this. No child resources; the single record argument is rendered as a dynamic block over var.records, not a separate managed resource.


βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
Provider F5Networks/bigip ~> 1.28 (re-verify against the live Terraform Registry listing before each new module wave β€” F5 ships frequent minor releases)
Provider block None β€” the caller's root module configures provider "bigip" {}; this module assumes a single already-authenticated provider instance is in scope
TMOS floor >= v12.1.1

Schema notes that bite:

  • name is full-path identity and is immutable β€” renaming the data group or moving it to a different partition forces destroy/recreate.
  • record uses nesting_mode = "set" in the live compiled schema β€” the published Argument Reference does not state whether record is a list or a set block; this was confirmed via terraform providers schema -json during authoring, with name required and data optional inside it.
  • Because record is schema-typed as a set, changing an existing record's data value (with its name unchanged) is not always shown as a clean in-place update in terraform plan β€” set element identity can hash the whole nested object, so the diff may present as remove-old/add-new for that one record rather than a modify. This is expected set semantics, not a module bug, and it does not affect other records in the same data group.
  • type applies only to each record's name field β€” data accepts any string value regardless of the declared type.
  • records (this module's record dynamic block) and records_src are mutually exclusive by which value of internal they pair with: records is meaningful only when internal = true; records_src only when internal = false. This is a provider/device-side constraint, not a closed enum, so it is documented rather than enforced with a validation{} block.
  • No provider-side default for type β€” every data group must declare it explicitly (string/ip/integer); this module enforces the closed set via validation{}.

πŸ”‘ 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. See SCOPE.md for the full cross-module contract this was derived from.

F5 BIG-IP Prerequisites

  • iControl REST enabled and reachable on the target device.
  • TMOS >= v12.1.1.
  • Target partition already exists (this module does not create partitions).
  • For external data groups (internal = false), the file referenced by records_src must already be staged on the target BIG-IP device before apply β€” this module does not upload it.

πŸ“ Module Structure

terraform-bigip-ltm-datagroup/
β”œβ”€β”€ providers.tf # required_version >= 1.12.0, F5Networks/bigip ~> 1.28, no provider {} block
β”œβ”€β”€ variables.tf # name, type, internal, records_src, records (map(object) for the record block)
β”œβ”€β”€ main.tf # bigip_ltm_datagroup.this β€” dynamic "record" block over var.records
β”œβ”€β”€ outputs.tf # name (primary cross-reference key), id, type, internal, record_names
β”œβ”€β”€ SCOPE.md # cross-module contract (this module's lightweight SCOPE.md)
└── README.md # this file

βš™οΈ Quick Start

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

  name = "/Common/http-error-codes"
  type = "integer"

  records = {
    "500" = {}
    "502" = {}
    "503" = {}
  }
}

The caller's root module configures the bigip provider (address/username/password or token_value) β€” this module never declares its own provider {} block and accepts no auth variables.


πŸ”Œ Cross-Module Contract

Consumes: none β€” this module has no cross-module inputs; name, type, internal, records_src, and records are all caller-supplied literals.

Input Type Source module
(none) β€” β€”

Emits:

Output Description Consumed by
name Full-path name of the LTM data group (e.g. /Common/my-datagroup) β€” the practical cross-reference key terraform-bigip-ltm-irule, terraform-bigip-ltm-policy (referenced by full path)
id Provider-internal id β€” identical value to name for this resource rarely consumed directly
type Data-group type declared for this data group's records diagnostics / reporting only
internal Whether this data group is internal vs. external diagnostics / reporting only
record_names Record names configured on this data group (the keys of var.records) diagnostics / reporting only

πŸ“š Example Library

1 Β· Minimal string data group

The smallest real call β€” a string-typed data group with no records yet, populated later as requirements emerge.

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

  name = "/Common/reserved-usernames"
  type = "string"
}

ℹ️ records defaults to {}, internal defaults to true (the provider default) β€” this produces a valid, empty internal data group ready for records to be added in a later change.

2 Β· String data group with inline records
module "reserved_usernames" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-datagroup.git?ref=v1.0.0"

  name = "/Common/reserved-usernames"
  type = "string"

  records = {
    "admin" = { data = "reserved" }
    "root"  = { data = "reserved" }
    "test"  = { data = "reserved" }
  }
}
3 Β· Integer data group (HTTP status allow-list)
module "http_error_codes" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-datagroup.git?ref=v1.0.0"

  name = "/Common/http-error-codes"
  type = "integer"

  records = {
    "500" = {}
    "502" = {}
    "503" = {}
    "504" = {}
  }
}

ℹ️ type = "integer" applies to each record's name field β€” every key in records must be a valid integer literal expressed as a string map key.

4 Β· IP/address data group (source allow-list)
module "trusted_source_ips" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-datagroup.git?ref=v1.0.0"

  name = "/Common/trusted-source-ips"
  type = "ip"

  records = {
    "10.10.0.0/16"  = { data = "corp-network" }
    "192.168.50.10" = { data = "vpn-jump-host" }
  }
}

πŸ’‘ A common pairing: this data group's name output is referenced inside an iRule's class match [IP::client_addr] equals <this-name> check for an allow-list gate.

5 Β· Key-only membership records (no `data` value)

data is optional per the live schema β€” a record can exist purely as a membership key.

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

  name = "/Common/maintenance-mode-nodes"
  type = "string"

  records = {
    "app01" = {}
    "app02" = {}
  }
}

ℹ️ Omitting data (leaving the object {}) is valid β€” main.tf's try(record.value.data, null) passes null through rather than requiring a placeholder value.

6 Β· External data group sourced from a device-staged file
module "ext_string_dg" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-datagroup.git?ref=v1.0.0"

  name        = "/Common/ext-allow-list"
  type        = "string"
  internal    = false
  records_src = "/var/config/rest/downloads/ext_dg_string.txt"
}

⚠️ records is left at its default {} here β€” it is meaningless when internal = false. The file at records_src must already be staged on the target BIG-IP device before apply; this module does not upload it (see F5 BIG-IP Prerequisites above).

7 Β· Non-`/Common` partition data group
module "tenant_a_routes" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-datagroup.git?ref=v1.0.0"

  name = "/Tenant-A/backend-routes"
  type = "string"

  records = {
    "eu" = { data = "10.40.0.10" }
    "na" = { data = "10.40.0.20" }
  }
}

πŸ’‘ Partition is folded into name's full-path convention (/Tenant-A/...), never a separate partition variable β€” per this module suite's design decision log, this module invents no implicit partition default.

8 Β· Per-environment backend selection table

A string-typed lookup used by an iRule to route requests to a different pool name per header value.

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

  name = "/Common/env-backend-map"
  type = "string"

  records = {
    "staging"    = { data = "/Common/pool-staging" }
    "canary"     = { data = "/Common/pool-canary" }
    "production" = { data = "/Common/pool-production" }
  }
}

πŸ’‘ Storing a full-path pool name as data lets an iRule resolve pool [class lookup [HTTP::header value X-Env] $::env_backend_map] without hardcoding pool names inside the TCL body itself.

9 Β· Building `records` from a `locals` list transformation

Useful when the record set is generated from another list-shaped input rather than typed by hand.

locals {
  allow_listed_ips = ["10.1.1.1", "10.1.1.2", "10.1.1.3"]
}

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

  name = "/Common/generated-allow-list"
  type = "ip"

  records = { for ip in local.allow_listed_ips : ip => {} }
}

ℹ️ The resulting map is still keyed by each record's own value (its natural, already-unique key), preserving the for_each-never-count guarantee even though the source data started as a list.

10 Β· Multiple data groups via root-level `for_each`

This module models exactly one data group per call, so a caller creating several wraps the module invocation itself at the root, keyed on a stable identifier β€” never count.

locals {
  region_datagroups = {
    "east" = { name = "/Common/routes-east", type = "string" }
    "west" = { name = "/Common/routes-west", type = "string" }
  }
}

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

  name = each.value.name
  type = each.value.type
}

πŸ’‘ Keying on each.key (a stable region identifier) rather than a list index means removing west never forces Terraform to touch east's state.

11 Β· Record `data` reused as a pool cross-reference
module "backend_by_tenant" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-datagroup.git?ref=v1.0.0"

  name = "/Common/backend-by-tenant"
  type = "string"

  records = {
    "tenant-a" = { data = module.tenant_a_pool.name }
    "tenant-b" = { data = module.tenant_b_pool.name }
  }
}

ℹ️ data is a plain string field on the provider schema β€” passing a sibling module's name output works, but this module does not track that reference as a formal cross-module input; it is only visible to Terraform as an implicit dependency via the referenced module's output.

12 Β· Data group consumed by an iRule (illustrative wiring)
module "trusted_source_ips" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-datagroup.git?ref=v1.0.0"

  name = "/Common/trusted-source-ips"
  type = "ip"

  records = {
    "10.10.0.0/16" = { data = "corp-network" }
  }
}

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

  name  = "/Common/source-ip-acl"
  irule = <<-TCL
 when CLIENT_ACCEPTED {
 if { [class match [IP::client_addr] equals ${module.trusted_source_ips.name}] } {
 # allow
 } else {
 reject
 }
 }
 TCL
}

πŸ’‘ This module never creates or owns the iRule β€” it only emits name, which the iRule's TCL body references by full path. The dependency Terraform sees is implicit, via the ${module.trusted_source_ips.name} interpolation.

13 Β· Reworking an existing data group's type (destroy/recreate)
module "reworked_dg" {
  source = "git::https://github.com/microsoftexpert/terraform-bigip-ltm-datagroup.git?ref=v1.0.0"

  name = "/Common/status-codes"
  type = "integer" # was "string" in a prior revision

  records = {
    "200" = {}
    "404" = {}
  }
}

⚠️ Changing type on an existing data group is not a safe in-place edit against real device state (a string-typed data group's existing record names are not guaranteed valid integer literals) β€” plan and review carefully; treat a type change as equivalent in risk to a rename.

14 Β· πŸ—οΈ End-to-end composition

A trusted-source-IP data group feeding an iRule, attached to a monitor-backed virtual server β€” the full chain this module participates in.

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

  name = "/Common/trusted-source-ips"
  type = "ip"

  records = {
    "10.10.0.0/16"  = { data = "corp-network" }
    "192.168.50.10" = { data = "vpn-jump-host" }
  }
}

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

  name  = "/Common/source-ip-acl"
  irule = <<-TCL
 when CLIENT_ACCEPTED {
 if { not ([class match [IP::client_addr] equals ${module.trusted_source_ips.name}]) } {
 reject
 }
 }
 TCL
}

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

  name = "/Common/app-pool"
  #...monitor/member wiring per terraform-bigip-ltm-pool's own README
}

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.40:443"
  pool        = module.app_pool.name
  irules      = [module.acl_irule.name]
  #...remaining virtual-server wiring per terraform-bigip-ltm-virtual-server's own README
}

πŸ—οΈ This module's contribution to the chain is narrow and intentional: it owns exactly the lookup table and nothing downstream. The iRule decides how the data group is evaluated at traffic time; the virtual server decides which iRules attach to which traffic. Ordering matters β€” the data group before the iRule, the iRule and pool before the virtual server β€” and Terraform's implicit dependency graph (via module.X.name references) enforces it automatically.


πŸ“₯ Inputs

Grouped summary (5 inputs total; name and type are required):

Group Variables
Identity name (required), type (required)
Record source internal, records_src
Records (for_each) records
Full variable reference
Variable Type Default Notes
name string β€” (required) Full-path data group name. Immutable β€” rename/repartition forces destroy/recreate.
type string β€” (required) Closed set: string, ip, integer. No provider-side default β€” every data group must declare it.
internal bool true Whether this is an internal (inline-managed) data group. false pairs with records_src.
records_src string null Path to a device-staged file with key:= value records, one per line. Meaningful only when internal = false.
records map(object({ data = optional(string) })) {} Internal records, keyed by each record's own name. Meaningful only when internal = true.

🧾 Outputs

Output Description Sensitive
name Full-path name of the LTM data group (e.g. /Common/my-datagroup) β€” the practical cross-reference key for sibling modules No
id Provider-internal id β€” identical value to name for this resource (no separate numeric id) No
type Data-group type declared for this data group's records (string/ip/integer) No
internal Whether this data group is internal (Terraform-managed via records) vs. external (file-sourced via records_src) No
record_names Record names configured on this data group (the keys of var.records) β€” diagnostics/reporting only, not a provider-computed attribute No

🧠 Architecture Notes

  • record is the only nested structure, rendered via dynamic. main.tf iterates var.records (a map(object(...)) keyed by each record's own name) and emits one record block per entry, with data = try(record.value.data, null) so an omitted data value never forces a placeholder.
  • for_each, never count, on records. Because the map is keyed by the record's own name (a naturally unique key on a BIG-IP data group), adding or removing one record never re-indexes any other record's plan entry.
  • Full-path identity drives replacement, not update. Because name encodes partition inline, renaming the data group or moving it to a different partition is a destroy/recreate β€” any sibling module (iRule TCL body, policy rule) still referencing the old full path will fail to resolve it until repointed.
  • records/records_src are not cross-validated against internal. The type system enforces the shape of each input independently; it does not enforce that records is empty when internal = false, or that records_src is null when internal = true. Passing both is not rejected at terraform validate time β€” see SCOPE.md Provider gotchas.
  • type only constrains record name, not data. data is optional(string) and accepts any string regardless of the data group's declared type.
  • No secret-shaped inputs. bigip_ltm_datagroup has no sensitive-worthy attribute; nothing in this module is marked sensitive = true. Per data-handling policy, do not place member PII, credentials, or account numbers in data values β€” this module has no mechanism to redact them once written to a data group record.

🧱 Design Principles

Concern This module's default Opt-out (caller must type extra)
Data-group type No default β€” var.type has no provider-side default and must be declared explicitly as string/ip/integer, enforced via validation{} N/A β€” hard requirement, not a toggle
Partition scope No invented default β€” name must carry the full path (partition + name) explicitly; this module never assumes /Common Caller always supplies the full path in name
Internal vs. external records internal = true (Terraform-managed inline records via records), matching the provider's own default Caller sets internal = false and supplies records_src to source records from a device-staged file instead
Record identity Records are keyed by their own name (map key), never a list index β€” adding/removing one record never re-indexes its siblings, per this module suite's for_each-never-count rule N/A β€” structural, not a toggle
Secrets Not applicable β€” bigip_ltm_datagroup has no secret-shaped attributes; data values are arbitrary lookup data, not credentials N/A β€” do not store PII/credentials in data regardless

πŸš€ Runbook

cd C:\GitHubCode\newf5modules\bigip\terraform-bigip-ltm-datagroup
terraform init -backend=false
terraform validate
terraform fmt -check

Pin consumers to a released tag, e.g. ?ref=v1.0.0 β€” never to an unpinned branch.


πŸ§ͺ Testing

This is an offline proof gate β€” it confirms the module is internally consistent, not that a real BIG-IP device accepts the configuration:

Command Proves Does NOT prove
terraform init -backend=false Provider requirement resolves, no backend needed for a child module Reachability of any real BIG-IP device
terraform validate Types, required arguments, and the type validation{} block are internally consistent That a string-typed record name is meaningful, or that records_src names a file that actually exists on the device
terraform fmt -check Canonical HCL formatting Anything about runtime behavior

Only a real terraform plan/apply against an authenticated bigip provider instance exercises whether the target device actually accepts the record set, honors the internal/records_src pairing, or resolves an external file β€” that step is a root-module/pipeline concern, not something this module's own offline gate can cover.


πŸ’¬ Example Output

$ terraform apply

module.reserved_usernames.bigip_ltm_datagroup.this: Creating...
module.reserved_usernames.bigip_ltm_datagroup.this: Creation complete after 1s [id=/Common/reserved-usernames]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

id = "/Common/reserved-usernames"
internal = true
name = "/Common/reserved-usernames"
record_names = [
 "admin",
 "root",
 "test",
]
type = "string"

πŸ” Troubleshooting

Symptom Cause Fix
terraform validate fails with must be one of: string, ip, integer var.type was set to an unsupported value Use exactly "string", "ip", or "integer"
BIG-IP API error rejecting a record's name A record's map key isn't a valid literal of the declared type (e.g. a non-numeric key with type = "integer") Confirm every key in records matches the declared type's expected format
Changing one record's data value shows as a remove+add instead of an in-place update record is schema-typed as a set β€” element identity can hash the whole nested object, not just name Expected set semantics for this resource; the diff still only touches the one changed record, not its siblings
records_src file not found at apply The referenced file was never staged on the target BIG-IP device, or the path is wrong Stage the file on the device first (out of band); this module does not upload it
Setting records seems to have no effect internal = false is set β€” records is only meaningful when internal = true Set internal = true to manage records inline, or use records_src for external data groups
Terraform wants to destroy and recreate the data group on an otherwise-small change name (full path) changed β€” partition or data group name edited Full-path identity is immutable; treat any rename as a deliberate destroy/recreate, and update every sibling module's reference (iRule TCL, policy rule) in the same change
iRule/policy fails to resolve the data group after a rename The consuming iRule's TCL body or policy rule action still references the old full path Update the referencing iRule/policy in the same change that renames this data group

πŸ”— Related Docs

  • F5 Terraform provider reference: F5Networks/bigip bigip_ltm_datagroup resource, Terraform Registry.
  • F5 clouddocs: LTM data group role/permission model at clouddocs.f5.com.
  • Sibling modules: terraform-bigip-ltm-irule, terraform-bigip-ltm-policy, terraform-bigip-ltm-virtual-server.
  • This module's own SCOPE.md (cross-module contract).

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

Releases

Packages

Contributors

Languages