terraform-bigip-ltm-datagroupmanages a singlebigip_ltm_datagroupβ an LTM data group (string/integer/address lookup table) β against theF5Networks/bigipTerraform provider~> 1.28, TMOS>= v12.1.1.
- π Manages one
bigip_ltm_datagroup.thisβ akey:= valuelookup table of typestring,ip, orinteger, consumed by full-path name from iRules and local traffic policies. - π§΅ Renders the provider's only nested/repeating block,
record, as adynamicblock over amap(object({ data = optional(string) }))keyed by each record's ownnameβ adding or removing one record never re-indexes its siblings in the plan (for_each, nevercount). - π Supports both internal data groups (
internal = true, the provider default β records managed inline viarecords) and external data groups (internal = falseβ records sourced from a file already staged on the target device viarecords_src). - π Requires
typeto be declared explicitly with no provider-side default β closed tostring/ip/integervia avalidation{}block, so a typo is caught atterraform validatetime rather than surfacing as a device-side API error. - π Emits
name(the practical full-path cross-reference key) andid, consumed byterraform-bigip-ltm-irule(TCLclass match/class lookupreferences) andterraform-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.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
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;
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.
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;
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.
| 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:
nameis full-path identity and is immutable β renaming the data group or moving it to a different partition forces destroy/recreate.recordusesnesting_mode = "set"in the live compiled schema β the published Argument Reference does not state whetherrecordis a list or a set block; this was confirmed viaterraform providers schema -jsonduring authoring, withnamerequired anddataoptional inside it.- Because
recordis schema-typed as a set, changing an existing record'sdatavalue (with itsnameunchanged) is not always shown as a clean in-place update interraform 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. typeapplies only to each record'snamefield βdataaccepts any string value regardless of the declaredtype.records(this module'srecorddynamic block) andrecords_srcare mutually exclusive by which value ofinternalthey pair with:recordsis meaningful only wheninternal = true;records_srconly wheninternal = false. This is a provider/device-side constraint, not a closed enum, so it is documented rather than enforced with avalidation{}block.- No provider-side default for
typeβ every data group must declare it explicitly (string/ip/integer); this module enforces the closed set viavalidation{}.
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.
- 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 byrecords_srcmust already be staged on the target BIG-IP device before apply β this module does not upload it.
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
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.
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 |
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"
}βΉοΈ
recordsdefaults to{},internaldefaults totrue(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'snamefield β every key inrecordsmust 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
nameoutput is referenced inside an iRule'sclass 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'stry(record.value.data, null)passesnullthrough 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"
}
β οΈ recordsis left at its default{}here β it is meaningless wheninternal = false. The file atrecords_srcmust already be staged on the target BIG-IP device beforeapply; 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 separatepartitionvariable β 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
datalets an iRule resolvepool [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 removingwestnever forces Terraform to toucheast'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 }
}
}βΉοΈ
datais a plain string field on the provider schema β passing a sibling module'snameoutput 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" = {}
}
}
β οΈ Changingtypeon an existing data group is not a safe in-place edit against real device state (astring-typed data group's existing record names are not guaranteed validintegerliterals) β plan and review carefully; treat atypechange 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.namereferences) enforces it automatically.
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. |
| 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 |
recordis the only nested structure, rendered viadynamic.main.tfiteratesvar.records(amap(object(...))keyed by each record's ownname) and emits onerecordblock per entry, withdata = try(record.value.data, null)so an omitteddatavalue never forces a placeholder.for_each, nevercount, 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
nameencodes 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_srcare not cross-validated againstinternal. The type system enforces the shape of each input independently; it does not enforce thatrecordsis empty wheninternal = false, or thatrecords_srcis null wheninternal = true. Passing both is not rejected atterraform validatetime β see SCOPE.md Provider gotchas.typeonly constrains recordname, notdata.dataisoptional(string)and accepts any string regardless of the data group's declared type.- No secret-shaped inputs.
bigip_ltm_datagrouphas nosensitive-worthy attribute; nothing in this module is markedsensitive = true. Per data-handling policy, do not place member PII, credentials, or account numbers indatavalues β this module has no mechanism to redact them once written to a data group record.
| 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 |
cd C:\GitHubCode\newf5modules\bigip\terraform-bigip-ltm-datagroup
terraform init -backend=false
terraform validate
terraform fmt -checkPin consumers to a released tag, e.g. ?ref=v1.0.0 β never to an unpinned branch.
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.
$ 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"
| 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 |
- F5 Terraform provider reference:
F5Networks/bigipbigip_ltm_datagroupresource, 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."