A reusable Azure IP Group β a named set of IP addresses and CIDR ranges that Azure Firewall and Firewall Policy rules reference by ID. Define the address set once, reference it from many rules, and change it in one place. Targets
hashicorp/azurerm ~> 4.0.
- ποΈ Creates a single
azurerm_ip_groupβ a named, reusable collection of IP addresses and CIDR ranges. - π Carries the address set inline via
cidrs, which updates in place β editing the set never replaces the group and never re-creates the firewall rules that reference it. - π΄ Do not also use
terraform-azurerm-ip-group-cidrfor the same group. Azure offers two ways to populate an IP Group and the provider warns that using both "will cause a conflict and CIDRS will be removed" β data loss, not a plan error. Leavingcidrsempty does not make them safe to combine, because the field is not computed and an unset input is an enforced empty set. Pick one method per group. - π§± Centralizes address ranges so a change is made once and applies everywhere the group's
idis referenced. - π Emits the Resource
idfirst, so Azure Firewall and Firewall Policy rule modules can consume it directly. - π·οΈ Applies
tagsand supports configurabletimeoutsβ the universal module tail. - π Empty call is safe: it produces a valid, empty group with no exposure knobs to get wrong.
π‘ Why it matters: Firewall rule sets drift when the same address list is copied into rule after rule. An IP Group turns that list into one addressable object. Update the ranges here and every consuming rule sees the change at once β fewer edits, fewer mistakes, one place to audit.
If this module saves you time, please consider supporting its continued development:
- β Star the repository on GitHub
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
rg["terraform-azurerm-resource-group"]
ipg["terraform-azurerm-ip-group"]
ipgres["azurerm_ip_group"]
fw["Azure Firewall rule (sibling)"]
fwp["Firewall Policy rule collection (sibling)"]
cidrmod["terraform-azurerm-ip-group-cidr: the standalone way to populate a group, and MUTUALLY EXCLUSIVE with this module's cidrs input. Using both removes CIDRS."]
rg -->|"resource_group_name"| ipg
ipg -->|"creates"| ipgres
ipgres -->|"id referenced by"| fw
ipgres -->|"id referenced by"| fwp
cidrmod -->|"do NOT combine with cidrs on the same group"| ipg
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef keystone fill:#004578,stroke:#002a4d,color:#ffffff;
classDef sib fill:#f0f0f0,stroke:#c8c8c8,color:#333333;
class ipg me;
class ipgres keystone;
class cidrmod keystone;
class rg,fw,fwp sib;
The module sits downstream of a resource group and upstream of the firewall rules that reference the group. It owns the address set; the firewall and firewall-policy modules own the rules and consume this module's id.
flowchart LR
subgraph in["Inputs"]
n["name (force-new)"]
rgn["resource_group_name (force-new)"]
loc["location (force-new)"]
c["cidrs (set, updates in place)"]
t["tags"]
end
this["azurerm_ip_group.this"]
subgraph out["Outputs"]
oid["id (first)"]
oname["name"]
ocidrs["cidrs"]
ofw["firewall_ids / firewall_policy_ids (computed)"]
end
in -->|"configure"| this
this -->|"emit"| out
classDef keystone fill:#004578,stroke:#002a4d,color:#ffffff;
classDef io fill:#f0f0f0,stroke:#c8c8c8,color:#333333;
class this keystone;
class in,out io;
Resource inventory
| Resource | Role | Cardinality |
|---|---|---|
azurerm_ip_group.this |
The keystone IP Group; holds the centralized cidrs set |
single |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| azurerm provider | ~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, auth, and subscription |
Schema notes that bite (verified against the live provider schema):
β οΈ nameis force-new β renaming the group replaces it, and any rule that referenced the old ID must be reconciled.β οΈ resource_group_nameis force-new β moving the group between resource groups replaces it.β οΈ locationis force-new β an IP Group cannot be moved between regions in place, and it must share the region of the firewall/policy that consumes it.- β
cidrsupdates in place β adding or removing ranges is a non-destructive update that does not touch the consuming rules. - βΉοΈ
firewall_idsandfirewall_policy_idsare computed (read-only) β Azure populates them from the rules that reference the group; they are outputs, never inputs.
Network Contributoron the target resource group, or a custom role grantingMicrosoft.Network/ipGroups/*scoped to the resource group.
Least-privilege at the smallest scope that works; grant at the resource-group scope, not the subscription, unless a broader scope is genuinely required.
- An existing resource group in a supported US Azure region.
- The
Microsoft.Networkresource provider registered on the target subscription. - The IP Group must be created in the same region as any Azure Firewall or Firewall Policy that will reference it.
- The caller configures the
provider "azurerm" { features {} }block, authentication, and the target subscription β the module declares none of these.
terraform-azurerm-ip-group/
βββ providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
βββ variables.tf # deeply-typed inputs: name, resource_group_name, location, cidrs, tags, timeouts
βββ main.tf # keystone azurerm_ip_group.this + dynamic timeouts
βββ outputs.tf # id first, then name, cidrs, location, resource_group_name, firewall_* maps
βββ README.md # this document
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT, Copyright (c) 2026 Casey Wood
βββ .gitignore # the canonical library ignore set
provider "azurerm" {
features {}
# auth + subscription come from the environment (ARM_* / az login / OIDC)
}
module "onprem_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "onprem-datacenters"
resource_group_name = "rg-network-prod"
location = "eastus"
cidrs = [
"10.10.0.0/16",
"10.20.0.0/16",
]
tags = {
environment = "prod"
owner = "network-team"
}
}βΉοΈ The caller owns the provider. This module never declares a
provider {}block or afeatures {}block β pin?ref=v1.0.0, never a branch.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group (name) |
location |
string |
caller / terraform-azurerm-resource-group (location) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
IP Group Resource ID (first) | Azure Firewall / Firewall Policy rule modules |
name |
IP Group name | diagnostics, tagging |
cidrs |
the centralized IP/CIDR set | documentation, IPAM β π΄ not to be combined with terraform-azurerm-ip-group-cidr |
location |
region the group is deployed in | siblings needing region parity |
resource_group_name |
the containing resource group | downstream modules |
firewall_ids |
Azure Firewall IDs referencing the group (computed) | audit / reporting |
firewall_policy_ids |
Firewall Policy IDs referencing the group (computed) | audit / reporting |
1 Β· Minimal call (empty group)
module "placeholder" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "placeholder"
resource_group_name = "rg-network-prod"
location = "eastus"
}π The empty call is safe:
cidrsdefaults to an empty set, producing a valid, empty group with nothing exposed.
2 Β· On-prem datacenter ranges
module "onprem" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "onprem-datacenters"
resource_group_name = "rg-network-prod"
location = "eastus"
cidrs = [
"10.10.0.0/16", # primary DC
"10.20.0.0/16", # DR DC
]
}π‘ Reference this group's
idfrom every firewall rule that trusts the corporate network β update the ranges here when a datacenter subnet changes and every rule follows.
3 Β· Partner / SaaS allowlist
module "partner_allowlist" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "partner-saas-allowlist"
resource_group_name = "rg-network-prod"
location = "eastus"
cidrs = [
"203.0.113.0/24", # partner egress range
"198.51.100.42/32", # single SaaS webhook source
]
tags = { purpose = "partner-integration" }
}
β οΈ Keep the set minimal and least-privilege β every range here widens each firewall rule that references the group.
4 Β· Empty-then-populate pattern
# Phase 1: create the group and wire its id into firewall rules first.
module "app_egress" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "app-egress-targets"
resource_group_name = "rg-network-prod"
location = "eastus"
# cidrs omitted β starts empty
}
# Phase 2 (a later change): add ranges. Because cidrs updates in place,
# the group is not replaced and the referencing rules are untouched.βΉοΈ Create the addressable object first, wire it everywhere, then fill it. The group's
idis stable across the populate step.
5 Β· Mixed IPv4 and IPv6 ranges
module "dual_stack" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "dual-stack-trusted"
resource_group_name = "rg-network-prod"
location = "eastus2"
cidrs = [
"10.0.0.0/8",
"2001:db8:1234::/48",
]
}π‘ A single group can hold both IPv4 and IPv6 CIDRs; reference one group instead of maintaining parallel v4/v6 rule lists.
6 Β· Governance tagging
module "tagged" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "regulated-data-sources"
resource_group_name = "rg-network-prod"
location = "centralus"
cidrs = ["172.16.0.0/12"]
tags = {
environment = "prod"
data_class = "restricted"
cost_center = "cc-4471"
managed_by = "terraform"
}
}βΉοΈ Tags flow only onto the IP Group. They aid inventory and cost attribution; they do not affect which addresses the group contains.
7 Β· Individual host addresses
module "jump_hosts" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "admin-jump-hosts"
resource_group_name = "rg-network-prod"
location = "eastus"
cidrs = [
"10.50.1.10/32",
"10.50.1.11/32",
"10.50.1.12/32",
]
}π Use
/32(IPv4) or/128(IPv6) for single hosts so the intent β exactly these addresses β is explicit and reviewable.
8 Β· Per-environment groups via for_each
variable "env_ranges" {
type = map(set(string))
default = {
dev = ["10.60.0.0/16"]
test = ["10.61.0.0/16"]
prod = ["10.62.0.0/16"]
}
}
module "env_ip_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
for_each = var.env_ranges
name = "trusted-${each.key}"
resource_group_name = "rg-network-${each.key}"
location = "eastus"
cidrs = each.value
tags = { environment = each.key }
}π‘ One definition, three groups. Because each module instance is keyed by environment name, adding or removing an environment never re-indexes the others.
9 Β· Multiple purpose-built groups via for_each
locals {
ip_groups = {
onprem = { name = "onprem", cidrs = ["10.0.0.0/8"] }
azure = { name = "azure-services", cidrs = ["20.0.0.0/8"] }
partner = { name = "partner", cidrs = ["203.0.113.0/24"] }
}
}
module "ip_groups" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
for_each = local.ip_groups
name = each.value.name
resource_group_name = "rg-network-prod"
location = "eastus"
cidrs = each.value.cidrs
}βΉοΈ Reference
module.ip_groups["onprem"].id,module.ip_groups["partner"].id, and so on from your firewall rules.
10 Β· Custom operation timeouts
module "slow_region" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "bulk-ranges"
resource_group_name = "rg-network-prod"
location = "westus2"
cidrs = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
timeouts = {
create = "30m"
delete = "30m"
}
}βΉοΈ Timeouts are rarely needed for an IP Group, but the universal tail is available for regions or subscriptions with slow control-plane behavior.
11 Β· Referenced from an Azure Firewall network rule
module "onprem" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "onprem-datacenters"
resource_group_name = "rg-network-prod"
location = "eastus"
cidrs = ["10.10.0.0/16"]
}
resource "azurerm_firewall_network_rule_collection" "example" {
name = "allow-onprem"
azure_firewall_name = "afw-hub"
resource_group_name = "rg-network-prod"
priority = 100
action = "Allow"
rule {
name = "onprem-to-app"
source_ip_groups = [module.onprem.id]
destination_addresses = ["10.100.0.0/16"]
destination_ports = ["443"]
protocols = ["TCP"]
}
}π‘ The rule references the group by
id. Change the group'scidrsand this rule's effective source set changes with it β no edit to the rule required.
12 Β· Referenced from a Firewall Policy rule collection
module "partner_allowlist" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "partner-saas-allowlist"
resource_group_name = "rg-network-prod"
location = "eastus"
cidrs = ["203.0.113.0/24"]
}
resource "azurerm_firewall_policy_rule_collection_group" "example" {
name = "partner-rcg"
firewall_policy_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod/providers/Microsoft.Network/firewallPolicies/afwp-hub"
priority = 200
network_rule_collection {
name = "allow-partner"
priority = 100
action = "Allow"
rule {
name = "partner-inbound"
source_ip_groups = [module.partner_allowlist.id]
destination_addresses = ["10.100.5.0/24"]
destination_ports = ["443"]
protocols = ["TCP"]
}
}
}π Firewall Policy is the modern, centrally-managed path. The same IP Group
idis reusable across many policy rule collections.
13 Β· Consuming resource-group module outputs
module "network_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-network-prod"
location = "eastus"
}
module "onprem" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "onprem-datacenters"
resource_group_name = module.network_rg.name
location = module.network_rg.location
cidrs = ["10.10.0.0/16"]
}βΉοΈ Sourcing
locationfrom the resource group keeps the group in the same region as the rest of the network stack β a requirement for the firewall that will consume it.
14 Β· ποΈ End-to-end composition
provider "azurerm" {
features {}
}
# 1) The resource group that owns the network stack.
module "network_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-network-prod"
location = "eastus"
}
# 2) The centralized address set.
module "onprem" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-ip-group.git?ref=v1.0.0"
name = "onprem-datacenters"
resource_group_name = module.network_rg.name
location = module.network_rg.location
cidrs = [
"10.10.0.0/16",
"10.20.0.0/16",
]
tags = { environment = "prod", owner = "network-team" }
}
# 3) A Firewall Policy rule collection that references the group by id.
resource "azurerm_firewall_policy_rule_collection_group" "hub" {
name = "hub-rcg"
firewall_policy_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod/providers/Microsoft.Network/firewallPolicies/afwp-hub"
priority = 300
network_rule_collection {
name = "allow-onprem-to-app"
priority = 100
action = "Allow"
rule {
name = "onprem-https"
source_ip_groups = [module.onprem.id]
destination_addresses = ["10.100.0.0/16"]
destination_ports = ["443"]
protocols = ["TCP"]
}
}
}π‘ The resource group's outputs feed the IP Group; the IP Group's
idfeeds the firewall policy rule. When the corporate ranges change, editmodule.onprem.cidrsonce β the rule collection needs no change at all.
Required
| Name | Type | Description |
|---|---|---|
name |
string |
Name of the IP Group (force-new). |
resource_group_name |
string |
Existing resource group to contain the group (force-new). |
location |
string |
Azure region (force-new). |
Optional
| Name | Type | Default | Description |
|---|---|---|---|
cidrs |
set(string) |
[] |
IP/CIDR ranges centralized by the group; updates in place. π΄ Mutually exclusive with terraform-azurerm-ip-group-cidr for the same group. |
tags |
map(string) |
{} |
Tags applied to the IP Group. |
timeouts |
object(...) |
null |
Optional create/read/update/delete timeouts. |
Full input schemas
variable "name" {
type = string
# Immutable: changing forces replacement of the IP Group.
}
variable "resource_group_name" {
type = string
# Existing group; this module does not create it. Immutable (force-new).
}
variable "location" {
type = string
# US regions e.g. eastus / eastus2 / westus2 / centralus. Immutable (force-new).
}
variable "cidrs" {
type = set(string)
default = []
# Set of IP addresses ("10.0.0.1") or CIDR ranges ("10.0.0.0/24",
# "2001:db8::/48"). Updates in place; does not replace the group or
# re-create consuming firewall rules. Validated as IP/CIDR-shaped.
}
variable "tags" {
type = map(string)
default = {}
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Notes |
|---|---|---|
id |
IP Group Resource ID | emitted first; reference from firewall rules |
name |
IP Group name | β |
cidrs |
the centralized IP/CIDR set | β |
location |
region the group is deployed in | β |
resource_group_name |
the containing resource group | β |
firewall_ids |
Azure Firewall IDs referencing the group | computed by Azure |
firewall_policy_ids |
Firewall Policy IDs referencing the group | computed by Azure |
- Single owned resource. Per this module suite's single-primary-resource rule, the module owns exactly one resource,
azurerm_ip_group.this, and emits itsidfirst. cidrsis the whole point. The address set is carried inline on the keystone. It updates in place, so the group'sidis stable while its contents change β this is what lets many firewall rules track one address list.- Force-new fields bite.
name,resource_group_name, andlocationeach force replacement. A replacement changes the Resource ID, which breaks every rule that referenced the old ID until they are reconciled β treat these three as effectively immutable once the group is referenced. - Do not double-manage the address set. Managing the same group's ranges both here (inline
cidrs) and through a separate per-CIDR resource elsewhere conflicts and produces perpetual diffs. This module owns the set inline. - Computed reference outputs.
firewall_ids/firewall_policy_idsreflect the rules Azure sees referencing the group; they are read-only and are useful for audit, not for wiring. features {}is the caller's. The module declares no provider block. If it appears not to initialize in isolation, the cause is a missing caller-sideprovider "azurerm" { features {} }block β that is expected.
An IP Group is a reusability and consistency control, not an exposure control β it carries no public-access, TLS, or encryption knobs. The relevant secure-by-default posture:
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Address set | cidrs = [] β a valid, empty group; nothing is trusted implicitly |
add explicit ranges |
| Least privilege | narrow ranges recommended; /32 and /128 for single hosts |
supply broader CIDRs deliberately |
| Change surface | one group referenced by many rules; edit ranges in one place | copy addresses into individual rules instead (discouraged) |
| Tagging | tags = {} |
supply governance tags |
Rule of thumb: the empty call produces the safe, empty object; every widening of the trusted address set is an explicit, reviewable caller change.
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0β never a branch. - This library is plan-only during authoring. A human runs
terraform plan/applyfrom CI against real credentials. - The caller supplies
provider "azurerm" { features {} }and authentication (Azure CLI, Managed Identity, or OIDC).
The offline proof gate is what this module ships green against:
| Gate | What it proves | What it does not do |
|---|---|---|
terraform init -backend=false |
providers resolve against the ~> 4.0 pin |
no backend, no cloud |
terraform validate |
the configuration is internally consistent and type-correct against the pinned schema | does not call Azure |
terraform fmt -check |
canonical formatting | β |
Only terraform plan (run by a human from CI, against real credentials) exercises the ARM API. validate and fmt never reach Azure.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod/providers/Microsoft.Network/ipGroups/onprem-datacenters"
name = "onprem-datacenters"
cidrs = toset([
"10.10.0.0/16",
"10.20.0.0/16",
])
location = "eastus"
resource_group_name = "rg-network-prod"
firewall_ids = []
firewall_policy_ids = [
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod/providers/Microsoft.Network/firewallPolicies/afwp-hub",
]
| Symptom | Cause | Fix |
|---|---|---|
| Plan shows the group being destroyed and re-created after a rename | name is force-new |
Keep the name stable; if a rename is required, reconcile every rule that referenced the old id. |
| A firewall rule stops matching after moving the group | location or resource_group_name changed, replacing the group and changing its id |
Recreate/reference the new id; keep the group in the firewall's region. |
Perpetual diff on cidrs |
The same group's ranges are managed both here and elsewhere | Manage the address set in exactly one place β this module's inline cidrs. |
provider "azurerm" ... features initialization error |
Caller root module has no features {} block |
Add provider "azurerm" { features {} } to the caller; the module never carries it. |
Error: Invalid value for variable on cidrs |
An entry is not a valid IP/CIDR | Use a bare address (10.0.0.1) or CIDR (10.0.0.0/24, 2001:db8::/48). |
| Group appears with no referencing rules | firewall_ids/firewall_policy_ids are empty |
Expected until a rule references the group's id; these outputs are computed by Azure. |
azurerm_ip_groupresource- Azure Firewall network rule collection
- Azure Firewall Policy rule collection group
- Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-role-assignments,terraform-azurerm-monitor-diagnostic-setting,terraform-azurerm-ip-group-cidr(β οΈ mutually exclusive with this module'scidrsinput β pick one method per IP Group) - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."