Creates a named set of CIDR blocks inside a Cloud NGFW local rulestack, for rules to match on. Targets
hashicorp/azurerm ~> 4.0.
- π Creates one
azurerm_palo_alto_local_rulestack_prefix_listβ a reusable, named group of CIDR blocks that rules match against by ID. - π΄ Reports the trap that costs the most time here: the name cannot be changed, and the provider does not stop you trying. It is not force-new, the update never sends it, and the read restores it β so a rename applies successfully, changes nothing, and returns on every later plan.
- β Reports the good news too: create, update and delete all commit the rulestack, so removing a prefix list takes effect immediately β unlike removing a rule.
- π― Emits the shape of the address set β
covers_every_ipv4_address,broadest_prefix_length,duplicate_prefix_count,every_prefix_is_a_single_addressβ so a review reads the list rather than counting it. β οΈ Explains the difference from a rule: the literal"any"is rejected here, and a bare IP needs an explicit/32.
π‘ Why it matters: a prefix list is how a rulestack stops repeating itself β one named object, referenced by many rules on either side of the match. That also makes it the quiet blast radius: widening a prefix list widens every rule that references it, in one apply, with no rule appearing in the plan. Twenty derived outputs exist so the widening is visible before it happens.
If this module saves you time:
- β Star the repository β it helps others find it.
- πΌ Connect on LinkedIn β linkedin.com/in/microsoftexpert
- β Buy me a coffee β buymeacoffee.com/microsoftexpert
flowchart TB
RG["terraform-azurerm-resource-group"]
VNET["terraform-azurerm-virtual-network: the VNet a VNET-deployed firewall is placed into, with its trusted and untrusted subnets"]
VWAN["terraform-azurerm-virtual-wan"]
VHUB["terraform-azurerm-virtual-hub: the Virtual WAN hub. It supplies BOTH the resource group and the region of anything registered against it"]
PUBIP["terraform-azurerm-public-ip: the addresses a firewall presents, and its egress NAT addresses. Passed by Resource ID"]
KV["terraform-azurerm-key-vault: holds the certificate a non-self-signed rulestack certificate points at, and its private key"]
RULESTACK["terraform-azurerm-palo-alto-local-rulestack: six threat-prevention engines and nothing else. Creating it does NOT commit it"]
RULE["terraform-azurerm-palo-alto-local-rulestack-rule: the traffic policy. PRIORITY is its identity and is force-new. Creating or updating a rule COMMITS the whole rulestack -- deleting one does NOT"]
PREFIXLIST["terraform-azurerm-palo-alto-local-rulestack-prefix-list: a named set of CIDR blocks, every entry validated as a real CIDR. Usable from BOTH sides of a rule's match"]
FQDNLIST["terraform-azurerm-palo-alto-local-rulestack-fqdn-list: a named set of domain names. The provider validates only that each entry is NOT EMPTY, and the list is usable from a rule's DESTINATION ONLY"]
CERTIFICATE["terraform-azurerm-palo-alto-local-rulestack-certificate: either self-signed or a Key Vault reference, and THREE of its arguments are force-new. Its DELETE does not commit"]
TRUSTASSOC["terraform-azurerm-palo-alto-local-rulestack-outbound-trust-certificate-association: a SINGLETON that sets one property on the rulestack"]
UNTRUSTASSOC["terraform-azurerm-palo-alto-local-rulestack-outbound-untrust-certificate-association: the independent twin, and a true clone of it"]
FWVNET["terraform-azurerm-palo-alto-next-generation-firewall-virtual-network-local-rulestack: THE RESOURCE THAT MAKES ALL OF THE ABOVE INSPECT TRAFFIC. Takes its REGION from the rulestack, carries no policy of its own, and takes HOURS to create"]
FWVHUB["terraform-azurerm-palo-alto-next-generation-firewall-virtual-hub-local-rulestack: the Virtual WAN twin of the VNet firewall, and NOT a clone of it. It REQUIRES an appliance slot, and Azure -- not the caller -- assigns its trusted and untrusted subnets. FIVE force-new fields against the VNet variant's three"]
APPLIANCE["terraform-azurerm-palo-alto-virtual-network-appliance: the family's odd one out. It creates a MICROSOFT.NETWORK resource, not a Cloud NGFW one, and the only thing making it Palo Alto is a delegation string the provider hardcodes. No locks, no commits, no rulestack"]
FWVHUBPANO["terraform-azurerm-palo-alto-next-generation-firewall-virtual-hub-panorama: the same Virtual WAN plumbing as its rulestack-managed twin, with policy moved OUT of Azure. It takes NO rulestack, takes a REGION as a real argument, takes NO locks and NO commits, and has SIX force-new fields"]
MODEL["A FIREWALL PICKS EXACTLY ONE POLICY MANAGEMENT MODEL: an Azure Rulestack managed natively in Azure, Palo Alto Panorama, or Strata Cloud Manager. That single choice is why there are SIX firewall resources -- two deployment models times three management models -- and it is what decides whether this rulestack is used at all"]
PANORAMA["Palo Alto Panorama: NOT an Azure resource and NOT a Terraform resource. Where every rule, address object, service and profile lives for a Panorama-managed firewall. Terraform passes one opaque base64 registration blob and can see none of the policy"]
FWVNETPANO["terraform-azurerm-palo-alto-next-generation-firewall-virtual-network-panorama: Panorama policy on the Virtual Network deployment model, which CLOSES the Panorama pair. It needs NO appliance, its network profile has NO force-new fields so the VNet and both subnets change in place, and it is the only firewall here whose DELETE allows 3 hours"]
FWVNETSCM["terraform-azurerm-palo-alto-next-generation-firewall-virtual-network-strata-cloud-manager: policy in Palo Alto SaaS. It carries a managed identity -- a thing only the two Strata variants in this family have -- and its tenant name is validated by a LIVE SUBSCRIPTION-SCOPED API CALL AT PLAN TIME -- so a plan needs real Azure access. Its default billing plan is NOT the stop-sell one"]
FWVHUBSCM["terraform-azurerm-palo-alto-next-generation-firewall-virtual-hub-strata-cloud-manager: the sixth and last firewall, CLOSING THIS FAMILY. It combines the Virtual WAN plumbing -- appliance prerequisite, Azure-assigned subnets, SIX force-new fields -- with Strata policy and its PLAN-TIME tenant check. Every fact it states already had a name elsewhere in the family"]
SCM["Palo Alto Strata Cloud Manager: a SaaS tenant, NOT an Azure resource and NOT a Terraform resource. Where every rule lives for a Strata-managed firewall. Terraform passes only its NAME, and the provider checks that name exists at PLAN time"]
UAI["terraform-azurerm-user-assigned-identity: the identity a Strata-managed firewall can reference. USER-ASSIGNED ONLY -- there is no system-assigned option -- so it is a prerequisite resource, and it is where principal_id and client_id are read from"]
RG -->|"resource_group_name and location. BOTH force-new, along with the name"| RULESTACK
RG -->|"resource_group_name"| VNET
RG -->|"resource_group_name"| VWAN
RG -->|"resource_group_name, force-new. NOTE the firewall takes no location argument at all"| FWVNET
RG -->|"resource_group_name, force-new. It need NOT be the hub's resource group, and no location argument exists here either"| FWVHUB
RG -->|"resource_group_name AND location, both force-new. BOTH Panorama-managed firewalls take a region as an argument, because neither has a rulestack to read one from"| FWVHUBPANO
VWAN -->|"the virtual wan id"| VHUB
RULESTACK -->|"the RULESTACK ID, and a rule is where policy actually lives. A create or update here COMMITS the rulestack"| RULE
RULESTACK -->|"the rulestack id, force-new. All three writes lock AND commit, delete included"| PREFIXLIST
RULESTACK -->|"the rulestack id, force-new. Its create does NOT wait for its own write"| FQDNLIST
RULESTACK -->|"the rulestack id, force-new. Unlike the two lists, this child's DELETE does not commit"| CERTIFICATE
KV -.->|"a VERSIONLESS certificate URI. Under the pinned provider line the validator accepts any nested item"| CERTIFICATE
PREFIXLIST -->|"the prefix list ID, which BOTH a rule's source and its destination can reference"| RULE
FQDNLIST -->|"the fqdn list ID, which ONLY a rule's destination can reference"| RULE
CERTIFICATE -->|"the certificate id, for a rule's inbound SSL inspection"| RULE
CERTIFICATE -->|"the certificate ID, and also the association's own Terraform resource id"| TRUSTASSOC
CERTIFICATE -->|"the certificate ID, same contract as the trust twin"| UNTRUSTASSOC
TRUSTASSOC -.->|"writes securityServices.outboundTrustCertificate ON THE RULESTACK, then commits"| RULESTACK
UNTRUSTASSOC -.->|"writes securityServices.outboundUnTrustCertificate ON THE RULESTACK, then commits"| RULESTACK
RULESTACK -->|"THE PAYOFF EDGE: the rulestack id, NOT force-new so the policy can be swapped in place. It also DECIDES THE FIREWALL'S REGION"| FWVNET
VNET -->|"virtual_network_id, plus the trusted and untrusted subnet ids. Both subnets are OPTIONAL to the provider and needed in practice"| FWVNET
PUBIP -->|"public ip Resource IDs: the addresses presented, the egress NAT addresses, and any DNAT frontend"| FWVNET
RULESTACK -->|"THE SAME PAYOFF EDGE, on the Virtual WAN side: the rulestack id, not force-new, and it decides this firewall's region too"| FWVHUB
VHUB -->|"virtual_hub_id, FORCE-NEW here unlike anything in the VNet variant's profile. No subnet ids are passed: Azure assigns them inside the hub"| FWVHUB
PUBIP -->|"public ip Resource IDs: the addresses presented, the egress NAT addresses, and any DNAT frontend"| FWVHUB
VHUB -->|"virtual_hub_id, force-new and the ONLY input besides the name. The provider takes the resource group from this ID and READS the hub for its location"| APPLIANCE
APPLIANCE -->|"network_virtual_appliance_id: a REQUIRED and FORCE-NEW hard prerequisite. Create the appliance first, and note it needs MICROSOFT.NETWORK rights rather than Cloud NGFW ones"| FWVHUB
VHUB -->|"virtual_hub_id, force-new. THE SAME provider schema helper the rulestack-managed hub firewall uses, so the whole network profile behaves identically"| FWVHUBPANO
APPLIANCE -->|"network_virtual_appliance_id: the SAME required, force-new prerequisite. The deployment model is unchanged by moving policy out of Azure"| FWVHUBPANO
PUBIP -->|"public ip Resource IDs: the addresses presented, the egress NAT addresses, and any DNAT frontend"| FWVHUBPANO
PANORAMA -.->|"a base64-encoded registration config, supplied as ONE OPAQUE ARGUMENT. It carries the credential that authenticates the firewall, and Azure decodes it and reports its parts back as computed attributes"| FWVHUBPANO
RG -->|"resource_group_name AND location, both force-new. Like its Virtual WAN twin and unlike the rulestack-managed firewalls, this one takes a region"| FWVNETPANO
VNET -->|"virtual_network_id plus the trusted and untrusted subnet ids. NONE of them force-new here, so all three change in place -- and both subnets are OPTIONAL to the provider and needed in practice"| FWVNETPANO
PUBIP -->|"public ip Resource IDs: the addresses presented, the egress NAT addresses, and any DNAT frontend"| FWVNETPANO
PANORAMA -.->|"the SAME opaque base64 registration argument as the Virtual WAN variant. No appliance and no rulestack are involved at all"| FWVNETPANO
RG -->|"resource_group_name AND location, both force-new. Every firewall whose policy lives outside Azure takes a region, because none of them has a rulestack to read one from"| FWVNETSCM
VNET -->|"virtual_network_id plus the trusted and untrusted subnet ids. NONE force-new, and both subnets are OPTIONAL to the provider and needed in practice"| FWVNETSCM
PUBIP -->|"public ip Resource IDs: the addresses presented, the egress NAT addresses, and any DNAT frontend"| FWVNETSCM
SCM -.->|"strata_cloud_manager_tenant_name: a NAME, not a Resource ID and not a blob. The provider lists the subscription's available tenants during PLAN and fails the plan if this one is not among them"| FWVNETSCM
UAI -.->|"identity_ids, optional. What the identity GRANTS is undocumented -- the provider's own docs for this block are copied from an unrelated resource -- so this suite attaches none by default"| FWVNETSCM
RG -->|"resource_group_name AND location, both force-new. It need NOT be the hub's resource group"| FWVHUBSCM
VHUB -->|"virtual_hub_id, FORCE-NEW. The SAME provider schema helper the other two Virtual WAN firewalls use, so the whole network profile behaves identically"| FWVHUBSCM
APPLIANCE -->|"network_virtual_appliance_id: the SAME required, force-new prerequisite, needing MICROSOFT.NETWORK rights rather than Cloud NGFW ones"| FWVHUBSCM
PUBIP -->|"public ip Resource IDs: the addresses presented, the egress NAT addresses, and any DNAT frontend"| FWVHUBSCM
SCM -.->|"strata_cloud_manager_tenant_name, checked against the subscription's available tenants during PLAN -- the same live call as its Virtual Network twin"| FWVHUBSCM
UAI -.->|"identity_ids, optional and of undocumented purpose. The only two firewalls here that can take an identity are the two Strata variants"| FWVHUBSCM
MODEL -.->|"choosing Azure Rulestack is what makes this rulestack relevant"| FWVNET
MODEL -.->|"choosing Azure Rulestack, on the Virtual WAN deployment model"| FWVHUB
MODEL -.->|"choosing Panorama on the Virtual WAN deployment model: the rulestack and every module above it become irrelevant"| FWVHUBPANO
MODEL -.->|"choosing Panorama on the Virtual Network deployment model: with both Panorama firewalls authored, this branch of the family is complete"| FWVNETPANO
MODEL -.->|"choosing Strata Cloud Manager on the Virtual Network deployment model: the rulestack branch becomes irrelevant, and policy leaves Azure entirely"| FWVNETSCM
MODEL -.->|"choosing Strata Cloud Manager on the Virtual WAN deployment model. With this authored, ALL SIX firewall resources and every rulestack child are modules -- the family is COMPLETE"| FWVHUBSCM
classDef me fill:#0078D4,stroke:#004578,color:#ffffff
classDef keystone fill:#004578,stroke:#002438,color:#ffffff
classDef sibling fill:#F3F6F9,stroke:#8A9BA8,color:#1B1F23
class RULESTACK,RULE,PREFIXLIST,FQDNLIST,CERTIFICATE,TRUSTASSOC,UNTRUSTASSOC,FWVNET,FWVHUB,APPLIANCE,FWVHUBPANO,FWVNETPANO,FWVNETSCM,FWVHUBSCM me
class MODEL keystone
class RG,VNET,VWAN,VHUB,PUBIP,KV,PANORAMA,SCM,UAI sibling
flowchart TB
IN_PARENT["rulestack_id: the parent rulestack. FORCE-NEW, and the ONLY force-new argument on this resource"]
IN_NAME["name: alphanumerics and dashes, validated by the RULE's validator rather than one of its own. NOT force-new and NOT updatable -- see the trap below"]
IN_PREFIXES["prefix_list: at least one entry, every entry validated with IsCIDR. The literal any is REJECTED here even though a rule accepts it, and a bare IP needs an explicit /32"]
IN_DOCS["audit_comment and description: free-form, both optional, and both sent differently on create than on update when emptied"]
TRAP["THE RENAME TRAP: name is required, not force-new, never sent on update, and restored from the Resource ID on read. So a rename plans, applies, changes nothing, and comes back on the next plan forever. Replace the resource instead"]
THIS["azurerm_palo_alto_local_rulestack_prefix_list.this"]
WRITES["CREATE, UPDATE and DELETE each LOCK the parent rulestack and then COMMIT it. Reads do neither. So a for_each serializes and pays a commit per entry -- and a DELETE takes effect immediately, unlike a rule's"]
INERT["A prefix list filters nothing on its own. It matters only when a rule references its ID, and nothing here can see whether one does"]
OUT_ID["id, name, rulestack_id, prefix_list, audit_comment and description as applied"]
OUT_PARENT["rulestack_name, resource_group_name and rulestack_path_within_the_subscription, parsed from the parent id"]
OUT_SHAPE["prefix_count, distinct_prefix_count, duplicate_prefix_count and prefixes_sorted for comparison"]
OUT_WIDTH["covers_every_ipv4_address, covers_every_ipv6_address, broadest_prefix_length and narrowest_prefix_length"]
OUT_KIND["single_address_prefixes, every_prefix_is_a_single_address, the IPv6 trio, and is_undocumented"]
OUT_CONST["ten constants, led by the rename trap and the commit-on-delete contrast with a rule"]
IN_PARENT --> THIS
IN_NAME --> THIS
IN_PREFIXES --> THIS
IN_DOCS --> THIS
IN_NAME -.->|"the trap this module reports"| TRAP
THIS --> WRITES
THIS -.-> INERT
THIS --> OUT_ID
THIS --> OUT_PARENT
THIS --> OUT_SHAPE
THIS --> OUT_WIDTH
THIS --> OUT_KIND
THIS --> OUT_CONST
classDef me fill:#0078D4,stroke:#004578,color:#ffffff
classDef keystone fill:#004578,stroke:#002438,color:#ffffff
classDef sibling fill:#F3F6F9,stroke:#8A9BA8,color:#1B1F23
class THIS keystone
class OUT_ID,OUT_PARENT,OUT_SHAPE,OUT_WIDTH,OUT_KIND,OUT_CONST me
class IN_PARENT,IN_NAME,IN_PREFIXES,IN_DOCS,TRAP,WRITES,INERT sibling
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_palo_alto_local_rulestack_prefix_list |
1 (this) |
One force-new argument: rulestack_id. The name is immutable in practice but not marked so |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None here. The caller configures the provider, its authentication, and its mandatory features {} block |
| ARM resource type | PaloAltoNetworks.Cloudngfw/localRulestacks/prefixlists |
Schema notes that bite
- π΄
nameCANNOT BE CHANGED, and nothing enforces that. It is required and not force-new, so a change does not trigger replacement. The update path never sends it. The read path restores it from the Resource ID, which never changes. Net effect: a rename plans, applies cleanly, changes nothing, and reappears on every subsequent plan. Useterraform apply -replace=...instead. - β Create, update AND delete each lock the parent rulestack and then commit it. All three. So a removed prefix list takes effect immediately β the sharp edge on the sibling rule module does not apply here.
β οΈ rulestack_idis the only force-new argument, so a prefix list cannot be moved between rulestacks.β οΈ The literal"any"is rejected. A rule'ssource/destinationCIDR lists accept it;IsCIDRhere does not. Write0.0.0.0/0.β οΈ A bare IP address is rejected β10.0.0.1fails,10.0.0.1/32passes. The provider's own message does not say so.β οΈ At least one entry is required (MinItems: 1). An empty list is not a way to say "no addresses".β οΈ Duplicates are accepted silently. Harmless in effect, misleading in review; reported asduplicate_prefix_count.β οΈ IPv6 CIDRs pass validation becauseIsCIDRaccepts them. Whether Cloud NGFW treats them identically is not visible from Terraform.β οΈ The name validator is the RULE's β the provider usesLocalRuleStackRuleNamehere rather than a prefix-list-specific one, and it carries aTODO - Check thiscomment in the provider's source. The sibling FQDN list has its own.- βΉοΈ There are no computed attributes. The provider's
Attributes()map is empty, soidis the only thing Azure adds. - βΉοΈ All four timeouts are honored, each declared on its own CRUD function. Read defaults to 5 minutes; the other three to 30, because they wait on a commit.
| Permission | Scope | Why |
|---|---|---|
PaloAltoNetworks.Cloudngfw/localRulestacks/prefixlists/write |
the parent rulestack | Create and update |
PaloAltoNetworks.Cloudngfw/localRulestacks/prefixlists/read |
the parent rulestack | Refresh, and the pre-create existence check |
PaloAltoNetworks.Cloudngfw/localRulestacks/prefixlists/delete |
the parent rulestack | Destroy |
PaloAltoNetworks.Cloudngfw/localRulestacks/commit/action |
the parent rulestack | Required by all three writes, which each commit |
π This permission is quieter than the rule's, and nearly as powerful. Nobody who edits a prefix list appears in a rule's plan, yet adding
0.0.0.0/0to a list that ten rules reference widens all ten at once. Review changes to shared prefix lists the way you would review the rules themselves.
β οΈ commit/actionis not optional here. Unlike the parent rulestack, whose create does not commit, every write on this resource does β so an identity without it fails after the object is written.βΉοΈ No credential is read or emitted. Plan access here is not credential access.
- A Cloud NGFW by Palo Alto Networks Marketplace subscription, and the
PaloAltoNetworks.Cloudngfwresource provider registered. - An existing local rulestack β see
terraform-azurerm-palo-alto-local-rulestack. - Rules that reference this list, for it to affect any traffic β see
terraform-azurerm-palo-alto-local-rulestack-rule. - A firewall referencing the rulestack, for those rules to run.
terraform-azurerm-palo-alto-local-rulestack-prefix-list/
βββ providers.tf # required_version + the pinned azurerm. No provider block.
βββ variables.tf # 6 variables, 9 validations
βββ main.tf # 25 locals + the single keystone resource
βββ outputs.tf # 36 outputs
βββ README.md # this file
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore
provider "azurerm" {
features {}
}
module "corp_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "corp-ranges"
prefix_list = ["10.0.0.0/8", "172.16.0.0/12"]
description = "Corporate address space"
}
β οΈ Pick the name carefully. Changing it later does not work and does not error β see example 3.
Consumes
| Input | Type | Source |
|---|---|---|
rulestack_id |
string |
terraform-azurerm-palo-alto-local-rulestack β id |
name |
string |
the caller. Effectively immutable |
prefix_list |
list(string) |
the caller. CIDR blocks only |
audit_comment, description |
string |
the caller |
Emits
| Output | Description |
|---|---|
id |
The prefix list's Resource ID β what a rule references |
covers_every_ipv4_address |
The widest entry a prefix list can hold |
duplicate_prefix_count |
Repeats, which make a list look larger than it is |
renaming_requires_replacement_and_the_provider_does_not_enforce_it |
Constant true |
deleting_this_takes_effect_immediately_unlike_a_rule |
Constant true |
1 Β· Minimal β one network range
module "corp_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "corp-ranges"
prefix_list = ["10.0.0.0/8"]
}
output "shape" {
value = {
count = module.corp_ranges.prefix_count # 1
broadest = module.corp_ranges.broadest_prefix_length # 8
wide_open = module.corp_ranges.covers_every_ipv4_address # false
}
}βΉοΈ
broadest_prefix_lengthis the smallest mask number, because a smaller mask covers more addresses. A/8is broader than a/24.
2 Β· Several ranges, documented
module "corp_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "corp-ranges"
prefix_list = [
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
]
description = "RFC1918 corporate address space"
audit_comment = "Baseline, raised by CHG-2001"
}
check "shared_objects_are_documented" {
assert {
condition = !module.corp_ranges.is_undocumented
error_message = "A prefix list has neither a description nor an audit comment. Named objects outlive the people who made them."
}
}π‘ Both fields are free-form, unvalidated and cost nothing.
is_undocumentedexists because a named address object with no explanation is the thing nobody later dares to delete.
3 Β· π΄ The rename trap
module "corp_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
# Changing this does NOT work, and does NOT error.
name = "corp-ranges-v2"
prefix_list = ["10.0.0.0/8"]
}
output "read_before_renaming" {
value = module.corp_ranges.renaming_requires_replacement_and_the_provider_does_not_enforce_it
}π΄ Three provider facts combine into one trap.
nameis required and not force-new, so Terraform plans an in-place update rather than a replacement. The update path never sends the name, so Azure renames nothing. The read path sets the name from the Resource ID, which never changes. The apply therefore succeeds, changes nothing, and the same diff returns on every subsequent plan.β The fix is to replace the resource:
terraform apply -replace='module.corp_ranges.azurerm_palo_alto_local_rulestack_prefix_list.this'That is safe here in a way it would not be for a rule: this resource's delete and create both commit, so there is no window where the rulestack is running stale configuration. Any rule referencing the old ID must be updated to the new one in the same apply.
4 Β· The widest possible list, and asserting against it
module "everything" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "everything"
prefix_list = ["0.0.0.0/0"]
}
check "no_wide_open_prefix_lists" {
assert {
condition = !module.everything.covers_every_ipv4_address
error_message = "Prefix list '${module.everything.name}' contains 0.0.0.0/0, so every rule referencing it is unrestricted on that side."
}
}π΄ This is the quiet version of an any-to-any rule. A rule referencing this list looks narrow β it names one prefix list β while matching every address. The rule's own
permits_any_source_to_any_destinationflag will readfalse, because that flag sees the rule's literal CIDRs, not the contents of a referenced list. The two checks are complementary and you want both.βΉοΈ
covers_every_ipv6_addressis separate on purpose: a list can be narrow in one address family and open in the other.
5 Β· The literal `"any"` is rejected here
module "wrong" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "any-attempt"
# REJECTED. A rule accepts "any"; a prefix list does not.
# prefix_list = ["any"]
# This is the equivalent.
prefix_list = ["0.0.0.0/0"]
}
β οΈ Two resources in one family, two different vocabularies for the same idea.azurerm_palo_alto_local_rulestack_ruleacceptscidrs = ["any"]; this resource validates each entry withIsCIDR, which rejects it. Moving a value from one to the other is where this bites.βΉοΈ The module's own validation message names both this case and the bare-IP case, because the provider's
IsCIDRerror names neither.
6 Β· A bare IP address needs its mask
module "jump_hosts" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "jump-hosts"
# WRONG: prefix_list = ["10.1.0.5", "10.1.0.6"]
prefix_list = ["10.1.0.5/32", "10.1.0.6/32"]
}
output "kind_of_list" {
value = {
hosts_only = module.jump_hosts.every_prefix_is_a_single_address # true
hosts = module.jump_hosts.single_address_prefixes
}
}
β οΈ A list made entirely of/32s is an allow-list of individual machines, not of networks. That is a legitimate design and a maintenance liability: it goes stale as hosts are replaced, and nothing in Azure or Terraform will tell you.every_prefix_is_a_single_addressnames the shape so a reviewer can ask how it is kept current.
7 Β· Duplicates are accepted and reported
locals {
# Two sources of truth merged without dedup -- a common accident.
from_cmdb = ["10.6.0.0/16", "172.20.0.0/14"]
from_manual = ["10.6.0.0/16"]
}
module "merged" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "merged-ranges"
prefix_list = concat(local.from_cmdb, local.from_manual)
}
output "merge_quality" {
value = {
entries = module.merged.prefix_count # 3
distinct = module.merged.distinct_prefix_count # 2
duplicates = module.merged.duplicate_prefix_count # 1
}
}βΉοΈ The provider accepts duplicates and they change nothing about what is matched, so this module reports rather than rejects β refusing them would reject legal input. A non-zero
duplicate_prefix_countis almost always a merge accident in the caller's own configuration, anddistinct(...)is the fix.
8 Β· IPv6 entries
module "dual_stack" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "dual-stack"
prefix_list = ["10.5.0.0/16", "2001:db8::/32"]
}
output "address_families" {
value = {
has_v6 = module.dual_stack.contains_ipv6_prefixes # true
v6 = module.dual_stack.ipv6_prefixes
v6_n = module.dual_stack.ipv6_prefix_count # 1
}
}
β οΈ These pass plan-time validation becauseIsCIDRaccepts IPv6. Whether Cloud NGFW treats an IPv6 prefix identically to an IPv4 one inside a rulestack is not something Terraform can see, so this module reports their presence and refuses nothing β rejecting them would refuse input the provider accepts.
9 Β· Comparing two lists for equality
module "list_a" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "list-a"
prefix_list = ["10.0.0.0/8", "172.16.0.0/12"]
}
module "list_b" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "list-b"
prefix_list = ["172.16.0.0/12", "10.0.0.0/8"] # same set, different order
}
output "are_they_the_same" {
value = module.list_a.prefixes_sorted == module.list_b.prefixes_sorted # true
}π‘
prefix_listpreserves the order you supplied;prefixes_sortedexists so two lists holding the same addresses compare equal regardless of order. Useful when consolidating duplicated lists before deleting one.
10 Β· Wiring it into a rule β both sides
module "corp_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "corp-ranges"
prefix_list = ["10.0.0.0/8"]
}
module "datacenter" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "datacenter"
prefix_list = ["10.200.0.0/16"]
}
module "corp_to_datacenter" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-rule.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "corp-to-datacenter"
priority = 1000
action = "Allow"
applications = ["ssl"]
protocol = "application-default"
source_match = { local_rulestack_prefix_list_ids = [module.corp_ranges.id] }
destination_match = { local_rulestack_prefix_list_ids = [module.datacenter.id] }
}βΉοΈ A prefix list can be referenced from BOTH sides of a rule β
local_rulestack_prefix_list_idsexists on the source and the destination. FQDN lists are destination-only.π‘ Wire
module.<x>.id, not a literal ID. That makes the dependency real, so Terraform creates the lists before the rule β and the rule module neither creates nor verifies the lists it references.
11 Β· One list, many rules β the blast radius
module "trusted" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "trusted"
prefix_list = ["10.0.0.0/8"] # widen this and every rule below widens
}
module "rules" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-rule.git?ref=v1.0.0"
for_each = {
web = { priority = 1000, apps = ["web-browsing", "ssl"] }
dns = { priority = 1100, apps = ["dns"] }
ssh = { priority = 1200, apps = ["ssh"] }
}
rulestack_id = module.rulestack.id
name = "allow-${each.key}"
priority = each.value.priority
action = "Allow"
applications = each.value.apps
protocol = "application-default"
source_match = { local_rulestack_prefix_list_ids = [module.trusted.id] }
destination_match = { cidrs = ["any"] }
}π΄ This is the reason to review prefix-list changes as carefully as rule changes. Adding one entry to
trustedwidens three rules in a single apply, and none of those rules appears in the plan as changed. The rules' own breadth flags will not detect it either, because they read each rule's literal CIDRs rather than the contents of a referenced list.
β οΈ Note also that all four resources here write to one rulestack, so they serialize and pay a commit each.
12 Β· A set of lists with `for_each`
locals {
address_groups = {
corp = ["10.0.0.0/8", "172.16.0.0/12"]
datacenter = ["10.200.0.0/16"]
jump-hosts = ["10.1.0.5/32", "10.1.0.6/32"]
partners = ["203.0.113.0/24"]
}
}
module "prefix_lists" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
for_each = local.address_groups
rulestack_id = module.rulestack.id
name = each.key
prefix_list = each.value
description = "Managed by Terraform"
}
output "review" {
value = {
for k, m in module.prefix_lists : k => {
entries = m.prefix_count
broadest = m.broadest_prefix_length
wide_open = m.covers_every_ipv4_address
hosts_only = m.every_prefix_is_a_single_address
}
}
}
β οΈ These do not apply in parallel. Every write locks the parent rulestack, so four lists are created one after another β and each one commits the rulestack, so the commit cost is paid four times.π‘ The
for_eachkeys are the names, so they must satisfy the Palo Alto name rule: alphanumerics and dashes only.jump-hostsworks;jump_hostswould not.
13 Β· Timeouts, and which one is different
module "big_list" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "big-list"
prefix_list = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
timeouts = {
create = "45m"
update = "45m"
delete = "45m"
read = "10m"
}
}βΉοΈ All four are honored β the provider declares each on its own CRUD function in the typed SDK, so none is inert. That is worth stating, because two resources elsewhere in this library declare an update timeout the provider never reads.
π‘ The provider's own defaults are 30 minutes for create, update and delete but only 5 minutes for read β read is a single GET, while the other three wait for a rulestack commit to finish. Raise
readonly if you are seeing read timeouts.
β οΈ Terraform silently discards object keys this type does not declare, so a misspelled key vanishes with no error. There are exactly four.
14 Β· Importing an existing prefix list
import {
to = module.corp_ranges.azurerm_palo_alto_local_rulestack_prefix_list.this
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-netsec-eastus/providers/PaloAltoNetworks.Cloudngfw/localRulestacks/rs-edge-eastus/prefixlists/corp-ranges"
}
module "corp_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "corp-ranges" # must match the ID's last segment exactly
prefix_list = ["10.0.0.0/8"]
}
β οΈ Thenamemust match the last segment of the ID. If it does not, you get the rename trap from example 3 immediately after importing: a permanent diff that never applies.βΉοΈ Creating over an existing prefix list is refused by default β the provider checks for one first. That check is disabled by the caller's
featuresblock flag for skipping import checks, which converts the refusal into a silent overwrite of a live address object.
15 Β· ποΈ End-to-end composition
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-netsec-eastus"
location = "eastus"
}
module "rulestack" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack.git?ref=v1.0.0"
name = "rs-edge-eastus"
resource_group_name = module.rg.name
location = module.rg.location
security_services = {
vulnerability_profile = "BestPractice"
anti_spyware_profile = "BestPractice"
anti_virus_profile = "BestPractice"
url_filtering_profile = "BestPractice"
file_blocking_profile = "BestPractice"
dns_subscription = "BestPractice"
}
}
module "corp_ranges" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-prefix-list.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "corp-ranges"
prefix_list = ["10.20.0.0/16", "10.21.0.0/16"]
description = "Spoke networks permitted outbound"
audit_comment = "Baseline, raised by CHG-2001"
}
module "allow_egress" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-rule.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "allow-egress"
priority = 1000
action = "Allow"
applications = ["web-browsing", "ssl", "dns"]
protocol = "application-default"
source_match = { local_rulestack_prefix_list_ids = [module.corp_ranges.id] }
destination_match = { cidrs = ["any"] }
}
module "deny_everything_else" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-palo-alto-local-rulestack-rule.git?ref=v1.0.0"
rulestack_id = module.rulestack.id
name = "deny-all"
priority = 999999
action = "DenySilent"
applications = ["any"]
protocol = "application-default"
source_match = { cidrs = ["any"] }
destination_match = { cidrs = ["any"] }
}
check "the_shared_list_is_not_wide_open" {
assert {
condition = !module.corp_ranges.covers_every_ipv4_address
error_message = "The prefix list every egress rule references covers all of IPv4."
}
}
output "composition" {
value = {
rulestack = module.rulestack.id
engines_on = module.rulestack.configured_security_engines
prefix_list = module.corp_ranges.id
rules = [module.allow_egress.id, module.deny_everything_else.id]
# All four modules agree on the rulestack they targeted.
all_on_one_rulestack = length(distinct([
module.rulestack.rulestack_path_within_the_subscription,
module.corp_ranges.rulestack_path_within_the_subscription,
module.allow_egress.rulestack_path_within_the_subscription,
module.deny_everything_else.rulestack_path_within_the_subscription,
])) == 1
# The rule's create commits, so this composition is not left staged.
committed_by_the_rules = module.allow_egress.creating_a_rule_commits_the_whole_rulestack
# Two withdrawal paths, two behaviors -- worth carrying into the runbook.
removing_a_prefix_list_is_immediate = module.corp_ranges.deleting_this_takes_effect_immediately_unlike_a_rule
removing_a_rule_is_not = module.allow_egress.deleting_a_rule_does_not_commit
}
}π Four modules, and the composition makes the family's central asymmetry legible. Deleting the prefix list commits and takes effect at once; deleting the rule does not and leaves the firewall enforcing it. Both facts are emitted rather than described, so a runbook can read them instead of remembering them.
β οΈ The egress rule names one prefix list and one literal"any"destination. Wideningcorp_rangeswidens that rule with no change to the rule itself β which is what thecheckblock guards.βΉοΈ
all_on_one_rulestackcompares the family's sharedrulestack_path_within_the_subscriptionoutput across all four modules β a subscription-independent form, so it works the same in every environment.
| Group | Variables |
|---|---|
| Placement | rulestack_id (force-new, the only one) |
| Identity | name (effectively immutable β see Architecture Notes) |
| Contents | prefix_list (at least one CIDR) |
| Documentation | audit_comment, description |
| Tail | timeouts |
βΉοΈ There is no
tagsvariable, because the resource has none. Within this family, only the rule resource exposestags; the rulestack and this prefix list do not. Tag the resource group for anything that must cover all three.
Full schemas
| Variable | Type | Default | Notes |
|---|---|---|---|
rulestack_id |
string |
β | Force-new. Anchored at localRulestacks/<name> |
name |
string |
β | ^[a-zA-Z0-9-]{1,128}$, no leading or trailing dash. Cannot be changed |
prefix_list |
list(string) |
β | At least one entry; every entry a valid CIDR |
audit_comment |
string |
null |
Free-form; not whitespace-only |
description |
string |
null |
Free-form; not whitespace-only |
timeouts |
object |
{} |
All four honored. Read defaults to 5m, the rest to 30m |
9 validations, listed rather than totaled. Two on rulestack_id (non-blank, and an anchored rulestack ID so a child ID is not accepted). Two on name (the character pattern, and the leading/trailing dash rule the provider raises separately). Two on prefix_list (at least one entry, and every entry a valid CIDR β with a message naming the bare-IP and "any" cases the provider's own error does not). One each on audit_comment and description (not whitespace-only). One on timeouts (duration format).
Three things are deliberately NOT validated. Duplicate entries, because the provider accepts them and they change nothing β reported instead. IPv6 entries, because IsCIDR accepts them and the service's treatment is not visible from Terraform. And whether a referenced address space is correct, which no module can know.
And one is validated only as a better explanation. The provider already enforces IsCIDR and a minimum of one entry; this module restates both β not with a stricter rule, but with an error message that names the two rejections a caller actually hits.
37 outputs: 6 passthrough, 20 derived, 11 constant.
| Output | Description |
|---|---|
id |
The prefix list's Resource ID β what a rule references |
name, rulestack_id, prefix_list, audit_comment, description |
The arguments as applied |
rulestack_name, resource_group_name, rulestack_path_within_the_subscription |
Parsed from the parent ID |
prefix_count, distinct_prefix_count, duplicate_prefix_count |
How many entries, and how many are repeats |
prefixes_sorted |
Sorted, so two lists with the same addresses compare equal |
covers_every_ipv4_address |
Contains 0.0.0.0/0 β the widest entry possible |
covers_every_ipv6_address |
Contains ::/0 |
broadest_prefix_length, narrowest_prefix_length |
Smallest and largest mask lengths; null if none parsed |
single_address_prefixes, single_address_prefix_count |
The /32 and /128 entries |
every_prefix_is_a_single_address |
The list is a host allow-list |
contains_ipv6_prefixes, ipv6_prefixes, ipv6_prefix_count |
IPv6 presence and contents |
has_audit_comment, has_description, is_undocumented |
Documentation state |
renaming_requires_replacement_and_the_provider_does_not_enforce_it |
Constant true |
every_write_locks_and_commits_the_parent_rulestack |
Constant true |
deleting_this_takes_effect_immediately_unlike_a_rule |
Constant true |
the_provider_borrows_the_rules_name_validator_for_this_resource |
Constant true |
the_literal_any_is_not_accepted_here |
Constant true |
a_bare_ip_address_is_rejected |
Constant true |
the_existence_check_can_be_disabled_by_a_provider_feature |
Constant true |
this_resource_has_no_tags |
Constant true |
rules_reference_this_by_id_not_by_name |
Constant true |
nothing_here_reports_whether_any_rule_uses_this_list |
Constant true |
editing_name_does_not_repoint_this_resource |
Constant true. name is a segment of the Terraform ID yet is not force-new, so editing it updates the OLD prefix list in place and the read then rewrites name back β the plan never converges |
βΉοΈ Nothing here is sensitive. A prefix list holds network addresses, which are configuration rather than credentials.
A prefix list is how a rulestack stops repeating itself, and that is also its risk. One named object, referenced by many rules, on either side of the match. Change it once and every referencing rule changes with it β in a single apply, with no rule showing as modified in the plan. The rule module's own breadth flags will not catch it either, because they read each rule's literal CIDRs rather than the contents of a referenced list. covers_every_ipv4_address on this side and permits_any_source_to_any_destination on the rule side are complementary, and a review wants both.
The rename trap is the module's headline, and it is built from three separate provider facts. name is required and not marked force-new, so Terraform plans an in-place update rather than a replacement. The update path handles prefix_list, audit_comment and description and never sends the name, so Azure renames nothing. The read path then sets the name from the Resource ID, which never changes. Each fact is defensible on its own; together they produce a change that plans, applies without error, and reappears on every subsequent plan forever. Nothing in the provider or its documentation says so. This module cannot fix it β refusing a rename would invent a constraint the provider does not have β so it names it in the variable's description, in a constant output, in Troubleshooting, and in an example, and it gives the -replace command that does work.
All three writes commit, and that is a genuine contrast with the sibling rule. Create, update and delete each lock the parent rulestack and then commit it; reads do neither. So deleting a prefix list takes effect immediately, where deleting a rule leaves the running firewall enforcing it until something else commits. The contrast is emitted as its own output because a reader arriving from the rule module would otherwise carry the sharper caveat across the family, where it does not apply. The cost of the same fact is that a for_each over several lists serializes and pays a commit each.
The vocabulary differs from a rule's, in one specific and confusing way. A rule's source and destination CIDR lists accept the literal string "any". A prefix list's entries are validated with IsCIDR, which rejects it β the equivalent is the explicit 0.0.0.0/0. Similarly, a bare IP address is refused where 10.0.0.1/32 is accepted. Both are restated by this module's own validation, not to be stricter than the provider but to explain the provider's own rejection, whose message names neither case.
What the module reports rather than rejects, and why. Duplicate entries are accepted by the provider and change nothing about what is matched, so refusing them would reject legal input β duplicate_prefix_count reports them instead, and a non-zero value is nearly always a merge accident. IPv6 CIDRs pass IsCIDR, so they reach Azure; whether Cloud NGFW treats them identically to IPv4 inside a rulestack is not visible from Terraform, so their presence is surfaced and nothing is refused.
The name validator is the rule's, not its own. The provider validates this resource's name with LocalRuleStackRuleName, and that function carries a TODO - Check this comment in the provider's source. The sibling FQDN list has a validator of its own. The rule that actually binds is the shared Palo Alto one β alphanumerics and dashes, 1 to 128 characters, no leading or trailing dash β which matters when a for_each key with an underscore is rejected.
And the module can see nothing about whether this list is used. A prefix list filters nothing on its own, costs nothing, and produces no signal when the last rule referencing it is deleted. Neither Terraform nor Azure will report an orphan. nothing_here_reports_whether_any_rule_uses_this_list says so rather than leaving the silence to be interpreted.
| Concern | This module's default | The caller's opt-out |
|---|---|---|
| List breadth | No default. 0.0.0.0/0 must be typed, and is reported |
β |
The literal "any" |
Rejected, with a message naming the 0.0.0.0/0 equivalent |
β |
| A bare IP | Rejected, with a message naming the /32 fix |
β |
| Duplicates | Reported, not rejected β the provider accepts them | β |
| IPv6 entries | Reported, not rejected β the provider accepts them | β |
| Documentation | Not required, but is_undocumented is emitted |
leave both unset |
| Renaming | Reported as impossible. No module can change it | use -replace |
MinItems and IsCIDR |
Restated only as a better error message, never as a stricter rule | β |
π There is no empty call to make safe here, and that is worth saying plainly.
rulestack_id,nameand at least one CIDR are all required β a prefix list must say what it contains. So this module's secure-by-default work is not in choosing defaults but in making breadth visible: every entry's mask length, whether the set covers everything, whether it is really a host allow-list, and whether the same address appears twice. Those are what acheckblock can assert on.βΉοΈ Note also what a prefix list is not: it grants no access by itself. It becomes an allow or a deny only through the rule that references it, which is where the action lives.
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0 β never a branch. This module is plan-only in this library; a human applies from CI.
π΄ To rename, replace β
terraform apply -replace='module.<x>.azurerm_palo_alto_local_rulestack_prefix_list.this'. Editingnamein place does not work and does not error.β To withdraw, just delete it. The delete commits, so it takes effect at once. Update any rule referencing the ID in the same apply.
| Covered by | What it proves |
|---|---|
terraform validate |
The resource and every expression parse and type-check against the pinned provider schema |
terraform fmt -check |
Canonical formatting |
terraform console with a root-module fixture |
Variable validations actually fire. All 9 rules were driven by deliberately bad inputs, and all 25 locals were driven to more than one value by good ones |
terraform plan (needs credentials) |
Whether the rulestack exists, and whether a prefix list of that name is already there |
| Nothing offline | Whether the addresses are the right addresses, or whether any rule references this list |
| Nothing at all, until you try it | That a rename will not apply β which is why it is documented here |
β οΈ Terraform skips a validation whose referenced variable has already failed, so a short error list is not proof a rule is missing. Fix the first failure and re-run.
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-netsec-eastus/providers/PaloAltoNetworks.Cloudngfw/localRulestacks/rs-edge-eastus/prefixlists/corp-ranges"
name = "corp-ranges"
prefix_list = [
"10.20.0.0/16",
"10.21.0.0/16",
]
rulestack_name = "rs-edge-eastus"
resource_group_name = "rg-netsec-eastus"
rulestack_path_within_the_subscription = "/resourceGroups/rg-netsec-eastus/providers/PaloAltoNetworks.Cloudngfw/localRulestacks/rs-edge-eastus"
prefix_count = 2
distinct_prefix_count = 2
duplicate_prefix_count = 0
prefixes_sorted = [
"10.20.0.0/16",
"10.21.0.0/16",
]
covers_every_ipv4_address = false
covers_every_ipv6_address = false
broadest_prefix_length = 16
narrowest_prefix_length = 16
single_address_prefix_count = 0
every_prefix_is_a_single_address = false
contains_ipv6_prefixes = false
is_undocumented = false
renaming_requires_replacement_and_the_provider_does_not_enforce_it = true
deleting_this_takes_effect_immediately_unlike_a_rule = true
| Symptom | Cause | Fix |
|---|---|---|
A name change plans every time and never sticks |
Not force-new, never sent on update, restored from the ID on read | terraform apply -replace=.... Editing in place cannot work |
expected ... to contain a valid CIDR |
A bare IP address | Add the mask: 10.0.0.1/32 |
The same, on the value any |
A rule accepts "any"; a prefix list does not |
Use 0.0.0.0/0 |
prefix_list must contain at least one CIDR block |
An empty list | The provider enforces a minimum of one |
name may only contain alphanumerics and dashes |
An underscore, often from a for_each key |
Rename the key, or replace(each.key, "_", "-") |
rulestack_id must be a local rulestack Resource ID |
Passed a child ID or a truncated path | It must end at /localRulestacks/<name> |
| A rule does not match traffic you expected | The rule references the list by ID; check you wired the right one | Compare rulestack_path_within_the_subscription across both modules |
| Widening one list changed several rules | That is what a shared prefix list does | Assert on covers_every_ipv4_address; review shared lists like rules |
| Lists apply one at a time, slowly | Every write locks and commits the parent rulestack | Expected. Each list pays its own commit |
A resource with this ID already exists |
A prefix list of that name is already in the rulestack | Import it, or pick another name |
| An overwrite happened with no error | The provider features flag for skipping import checks is on |
That flag turns the refusal into a silent overwrite |
| A deleted prefix list still appears in a rule | The rule was not updated to stop referencing it | Update the rule; the list's own deletion did commit |
| An IPv6 entry applied but seems inert | IsCIDR accepts IPv6; the service's handling is not visible here |
Verify on the firewall side |
azurerm_palo_alto_local_rulestack_prefix_listβ the provider resource- What is Cloud NGFW by Palo Alto Networks? β the Marketplace subscription and the
PaloAltoNetworks.Cloudngfwresource provider - Cloud NGFW by Palo Alto Networks FAQ β what an Azure Rulestack can configure
az palo-alto cloudngfw local-rulestackβ thecommitoperation every write here performsterraform-azurerm-palo-alto-local-rulestackβ the parent, and where the threat-prevention engines liveterraform-azurerm-palo-alto-local-rulestack-ruleβ the rules that reference this list, on either side of the matchterraform-azurerm-palo-alto-local-rulestack-fqdn-listβ the domain equivalent, destination-only, whose entries the provider does not validateterraform-azurerm-palo-alto-local-rulestack-certificateβ the rulestack's certificate object, which unlike this one does not commit on deleteterraform-azurerm-resource-groupβ the resource group- This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."