Attaches one named IP configuration of a network interface to a load balancer's inbound NAT rule β forwarding one frontend port straight to this one machine. Targets
hashicorp/azurerm ~> 4.0.
- β‘οΈ Attaches one named IP configuration to a load balancer's inbound NAT rule.
- π― Modifies exactly that configuration β the sibling application-security-group association writes itself into every one instead.
- π§Ύ Emits a ready-to-paste
import_idwhose first half is an IP configuration ID, not the interface's. - πͺ Names what the change does: a NAT rule forwards one frontend port to one machine, so this is a direct route rather than a share of balanced traffic.
β οΈ Names what it does not decide: whether anything can traverse that route is set by network security group rules this module cannot see.
π‘ Why it matters: Azure has no association object here. The provider reads the network interface, attaches the rule to one IP configuration, and writes the interface back β so the permission required is write on the interface, a concurrent writer can silently drop the attachment, and the composite Terraform ID is not the shape most people guess.
If this module saved you time:
- β Star the repository β it genuinely helps other people find it.
- πΌ Connect on LinkedIn β linkedin.com/in/microsoftexpert
- β Buy me a coffee β buymeacoffee.com/microsoftexpert
flowchart TB
NIC["terraform-azurerm-network-interface"]
LB["terraform-azurerm-load-balancer -- owns the inbound NAT rule"]
ASSOC["terraform-azurerm-network-interface-nat-rule-association"]
IPCFG["ONE named ip configuration -- not all of them"]
POOLA["terraform-azurerm-network-interface-backend-address-pool-association -- balances instead of forwarding"]
NSGA["terraform-azurerm-network-interface-security-group-association -- decides whether anything gets through"]
NIC -->|"id to network_interface_id -- and this interface is what gets WRITTEN"| ASSOC
LB -->|"inbound nat rule id to nat_rule_id"| ASSOC
ASSOC -->|"attaches the rule to the configuration named in ip_configuration_name"| IPCFG
IPCFG -->|"one frontend port now forwards directly to THIS machine"| LB
POOLA -.->|"same shape, different target on the same load balancer"| ASSOC
NSGA -.->|"its rules decide whether the forwarded port is reachable at all"| NIC
classDef self fill:#0078D4,stroke:#004578,color:#ffffff
classDef keystone fill:#004578,stroke:#002B4A,color:#ffffff
classDef ext fill:#F3F2F1,stroke:#8A8886,color:#201F1E
class ASSOC self
class NIC keystone
class LB,IPCFG,POOLA,NSGA ext
Two edges matter beyond the obvious. The loop back to the interface is why the required permission is write on the whole interface. And the dotted edge to the security group association is the one people forget: a NAT rule creates a route, and the rule set attached to the interface decides whether anything can use it.
flowchart TB
IN1["network_interface_id -- force-new, and THE OBJECT THAT IS WRITTEN"]
IN2["ip_configuration_name -- a NAME, and only THIS configuration is attached"]
IN3["nat_rule_id -- an INBOUND NAT RULE, not a backend address pool"]
RES["azurerm_network_interface_nat_rule_association.this -- a rewrite, not a resource"]
OUT1["a SYNTHETIC id whose FIRST HALF IS AN IP CONFIGURATION ID"]
OUT2["import_id and ip_configuration_id, composed before the resource exists"]
OUT3["spans_subscriptions and spans_resource_groups -- the region is NOT checkable"]
OUT4["posture -- one frontend port to ONE machine, and a missing nic FAILS LOUDLY"]
IN1 --> RES
IN2 --> RES
IN3 --> RES
RES --> OUT1
RES --> OUT2
RES --> OUT3
RES --> OUT4
classDef self fill:#0078D4,stroke:#004578,color:#ffffff
classDef ext fill:#F3F2F1,stroke:#8A8886,color:#201F1E
class RES self
class IN1,IN2,IN3,OUT1,OUT2,OUT3,OUT4 ext
| Resource | Name | Cardinality |
|---|---|---|
azurerm_network_interface_nat_rule_association |
this |
single β an attachment inside one IP configuration, not an ARM object |
All three arguments are force-new, so there is no update function and the timeouts type carries three keys.
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, including authentication |
- π΄ Terraform's ID is a composite whose FIRST HALF IS AN IP CONFIGURATION ID, not the interface's:
{networkInterfaceId}/ipConfigurations/{ipConfigurationName}|{natRuleId}. The provider's documentation confirms the format and says it is specific to Terraform. - π΄ This is not a resource in Azure. The provider rewrites the whole network interface, so the permission required is write on the interface.
- π΄ A missing network interface FAILS the create, loudly. The sibling application-security-group association returns success in the same situation, having done nothing. Siblings in this directory disagree, and nothing in either schema reveals it.
- π΄ A missing IP configuration also fails the create. It is not created for you, and the check happens at apply, because nothing in a Resource ID reveals which configurations an interface has.
- π΄ A NAT rule targets ONE backend at a time. Attaching a second IP configuration to a rule that already has one is refused by Azure β not by the provider and not by this module β so that failure also arrives at apply.
- π All three arguments are force-new, so moving to a different rule is a destroy and a create, with a window in between during which the port forwards nowhere.
- π The read clears the ID by THREE routes: interface gone, IP configuration gone, or the rule no longer attached.
- π‘
ip_configuration_nameis checked only for non-emptiness β no pattern, no length limit. - π‘ The target must be an inbound NAT RULE, not a backend address pool. They differ by one path segment on the same load balancer.
- π‘ The provider locks only the network interface β not the load balancer, and not the rule.
- β The import guard's error names the COMPOSITE ID β the address you actually need.
- β
No
tags, no sensitive field, no credential.
| Permission | Scope | Why |
|---|---|---|
Microsoft.Network/networkInterfaces/read |
the network interface | Read the current IP configurations. |
Microsoft.Network/networkInterfaces/write |
the network interface | Create and delete β both rewrite the interface. |
Microsoft.Network/loadBalancers/inboundNatRules/join/action |
the inbound NAT rule | Attaching an IP configuration to the rule. |
Microsoft.Network/loadBalancers/read |
the load balancer | May be in another subscription. |
π This opens a direct route to one machine. An inbound NAT rule forwards a specific frontend port to a specific backend, so attaching it makes exactly this interface reachable on that port β typically for administrative access such as RDP or SSH. Removing the association closes the route just as directly.
β οΈ Whether anything can traverse the route is decided elsewhere. The network security group rules applying to this interface β and to its subnet β permit or deny the forwarded traffic, and this module can see neither. A NAT rule pointed at a machine with a permissive rule set is a path from the load balancer's frontend to that machine's port. Review both together.
βΉοΈ There is no narrow permission for the write.
networkInterfaces/writeis the right to change any property of the interface, so whoever can attach a NAT rule can also change its subnet or its security group.
- The
Microsoft.Networkresource provider registered in the target subscription. - π΄ The network interface must exist, and the named IP configuration must exist on it. Both are checked at apply, and both fail loudly.
- π΄ The NAT rule must not already have a backend attached β one rule, one backend. Refused by Azure at apply.
- The load balancer and the interface mutually reachable β same region and virtual network. Neither region is derivable from a Resource ID.
- A frontend IP configuration and port on the rule, or the route leads nowhere useful.
terraform-azurerm-network-interface-nat-rule-association/
βββ providers.tf # required_version >= 1.12.0, azurerm ~> 4.0, no provider block
βββ variables.tf # 4 inputs, 4 validations -- the interface, the configuration name, the rule
βββ main.tf # one keystone `this`; ID parsing from the END; the composed import address
βββ outputs.tf # 32 outputs -- the synthetic id, the import id, then the reachability facts
βββ README.md # this file
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore
provider "azurerm" {
features {}
}
module "rdp_rule_attachment" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
ip_configuration_name = "internal"
nat_rule_id = var.rdp_nat_rule_id
}
β οΈ That makes this machine reachable on the rule's frontend port. See example 2.
| Input | Type | Source |
|---|---|---|
network_interface_id |
string |
terraform-azurerm-network-interface β id |
ip_configuration_name |
string |
the caller β a name, not an ID |
nat_rule_id |
string |
terraform-azurerm-load-balancer β an inbound NAT rule ID |
timeouts |
object(...) |
the caller β three keys |
| Output | Description | Consumed by |
|---|---|---|
id |
Synthetic β the first half is an IP configuration ID | reporting only |
import_id |
The exact terraform import address |
operations |
ip_configuration_id |
A real Resource ID for the configuration modified | reporting |
this_opens_a_direct_path_to_this_one_machine |
Constant true |
security review |
1 Β· The smallest real call
module "rdp_rule_attachment" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
ip_configuration_name = "internal"
nat_rule_id = var.rdp_nat_rule_id
}βΉοΈ Three arguments, all force-new. There is no update function, and
fields_that_can_change_after_creationreturns an empty list.
β οΈ ip_configuration_nameis a name β the name of a configuration that already exists on the interface. It is not an ID, and it is not created for you.
2 Β· A rule forwards one port to one machine
# On the load balancer: one frontend port, one backend.
# frontend_port = 50001 -> backend_port = 3389
module "rdp_rule_attachment" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
ip_configuration_name = "internal"
nat_rule_id = var.rdp_nat_rule_id
}π΄ This is the line that makes the machine reachable on that port. Unlike a backend address pool, which spreads traffic across whichever machines belong to it, an inbound NAT rule creates a direct route to exactly this interface β which is why it is usually reached for when someone needs administrative access to one instance.
β οΈ Removing the association closes the route just as directly. A plan line reading "1 to destroy" here is a machine losing its forwarded port.
π‘
this_opens_a_direct_path_to_this_one_machineis a constanttrueso the point appears in the plan, where the load balancer's rule definition does not.
3 Β· The route exists; the rules decide whether it works
module "rdp_rule_attachment" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
ip_configuration_name = "internal"
nat_rule_id = var.rdp_nat_rule_id
}
# The rule set that decides whether anything reaches the forwarded port.
module "nic_security_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-security-group-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
network_security_group_id = module.app_nsg.id
}π A NAT rule creates a path; it permits nothing. What actually traverses the forwarded port is decided by the network security group rules applying to this interface β and to its subnet, which is evaluated as well. This module can see neither.
β οΈ Read the two together. A NAT rule for RDP pointed at a machine whose rule set allows3389from anywhere is a route from the load balancer's public frontend to that machine's console.
π‘ The two modules are shown side by side because that is the only honest way to read the change. In a real configuration they are frequently in different files.
4 Β· Only the named configuration is attached
module "app_nic" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface.git?ref=v1.0.0"
name = "nic-app01"
resource_group_name = module.rg.name
location = module.rg.location
ip_configurations = {
internal = {
subnet_id = module.app_vnet.subnet_ids["snet-app"]
private_ip_address_allocation = "Dynamic"
primary = true
}
management = {
subnet_id = module.app_vnet.subnet_ids["snet-mgmt"]
private_ip_address_allocation = "Dynamic"
}
}
}
module "rdp_rule_attachment" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
ip_configuration_name = "management" # ONLY this one gets the forwarded port
nat_rule_id = var.rdp_nat_rule_id
}π‘ This is the structural difference from the sibling application-security-group association, which has no
ip_configuration_nameargument and writes itself into every configuration on the interface.
π On a machine with a service address and a management address, naming the management configuration is exactly the point β the forwarded port reaches the address intended for it and not the other.
βΉοΈ
only_the_named_ip_configuration_is_modifiedis a constanttrue.
5 Β· The import address, and the half people get wrong
output "import_command" {
value = "terraform import module.rdp_rule_attachment.azurerm_network_interface_nat_rule_association.this '${module.rdp_rule_attachment.import_id}'"
}
output "the_configuration_being_modified" {
# A REAL Azure Resource ID -- the interface's, with /ipConfigurations/<name>.
value = module.rdp_rule_attachment.ip_configuration_id
}π΄ The composite ID's first half is an IP CONFIGURATION ID, not the network interface's. The full shape is
{networkInterfaceId}/ipConfigurations/{ipConfigurationName}|{natRuleId}β confirmed by the provider's own import documentation.
π‘ Both
import_idandip_configuration_idare emitted, and both derive from the inputs β so they are available before the resource exists, which is exactly when you need them if you are adopting an attachment someone made in the portal.
β οΈ Quote it in a shell. The pipe separator is a metacharacter.
6 Β· A NAT rule takes one backend, and this is not the check that stops you
module "rdp_first_machine" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = var.first_nic_id
ip_configuration_name = "internal"
nat_rule_id = var.rdp_nat_rule_id
}
# WRONG -- the same rule cannot serve a second machine.
module "rdp_second_machine" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = var.second_nic_id
ip_configuration_name = "internal"
nat_rule_id = var.rdp_nat_rule_id # <-- the SAME rule
}π΄ An inbound NAT rule targets one backend at a time, and the second attachment is refused by Azure β not by the provider, and not by this module. The failure arrives at apply.
π‘ This module does not invent a check for it: knowing whether the rule already has a backend would mean reading the rule's current state, which is not derivable from an ID. Refusing on a guess would reject legal configurations.
βΉοΈ For several machines, use a NAT rule per machine, or an inbound NAT pool on the load balancer, which allocates a port range across a scale set.
7 Β· A missing interface fails here, and does not fail one sibling
module "rdp_rule_attachment" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = var.nic_id_that_may_not_exist
ip_configuration_name = "internal"
nat_rule_id = var.rdp_nat_rule_id
}π΄ This resource's create ERRORS if the interface does not exist, with
<interface> was not found.
β οΈ The application-security-group association does the opposite: its create logs the fact, clears the resource ID, and returns SUCCESS β a green apply that made no attachment at all. The two live in the same provider directory and share helper functions.
π‘ Neither behaviour is derivable from the schema; both were read from the provider's source. Referencing the interface through a module output rather than a variable is worth doing regardless β Terraform's dependency graph then guarantees it exists.
8 Β· A misspelled configuration name fails at apply
module "rdp_rule_attachment" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
ip_configuration_name = "managment" # missing an e
nat_rule_id = var.rdp_nat_rule_id
}π΄ The create fails with
IP Configuration "managment" was not found for <interface>. The configuration is not created for you, and the failure is not silent.
β οΈ But it happens at APPLY, not at plan. Nothing in a Resource ID reveals which configurations an interface has, so no amount of validation here could catch it. The provider's only check on this argument is that the string is non-empty.
π‘ This module adds one check the provider does not: a value beginning with
/is refused, because a Resource ID in a name field can only be a mistake. Anything else is left alone.
9 Β· A concurrent writer closes the route
module "rdp_rule_attachment" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
ip_configuration_name = "internal"
nat_rule_id = var.rdp_nat_rule_id
}π΄ The provider locks the network interface while it reads, modifies and writes β inside one Terraform process. A second apply, a portal edit, a CLI call or a parallel pipeline reads the same interface, writes back its own version, and one side's change disappears. Neither reports an error.
β οΈ Here that loss means the forwarded port quietly stops reaching the machine, and stays that way until the next refresh notices.
π‘ The mitigation is ownership rather than retries: one pipeline owns a network interface's attachments.
10 Β· Three ways this disappears from state
output "detectable_drift" {
# EMPTY, and not by oversight.
value = module.rdp_rule_attachment.fields_azure_returns_on_read
}βΉοΈ The refresh removes this resource from state if the interface is gone, if the named IP configuration is gone, or if the rule is simply no longer attached to that configuration. All three look identical from the outside.
β οΈ The following plan then proposes to create it again, which reads like a first-time create rather than like something took it away.
π‘
fields_azure_returns_on_readis empty because the read establishes presence and then sets the arguments from the composite ID rather than from the API response. With three force-new arguments and no others, there is nothing a change could be.
11 Β· What NOT to do
# WRONG -- a backend address pool where an inbound NAT rule belongs.
nat_rule_id = var.load_balancer_backend_pool_idβΉοΈ Refused at
terraform plan, offline, with a message naming the module to use instead:terraform-azurerm-network-interface-backend-address-pool-association. The two differ by one path segment on the same load balancer.
# WRONG -- an ID that stops at the load balancer.
nat_rule_id = var.load_balancer_idβΉοΈ Refused. The pattern requires the
/inboundNatRules/<rule>segments.
# WRONG -- an IP configuration's Resource ID where the INTERFACE's belongs.
network_interface_id = var.nic_ip_configuration_idβΉοΈ Refused. Pass the interface's ID and name the configuration separately.
# WRONG -- a Resource ID where a NAME belongs.
ip_configuration_name = var.nic_ip_configuration_idβΉοΈ Refused by this module, which goes one step further than the provider here.
12 Β· ποΈ 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-app-eastus"
location = "eastus"
tags = {
environment = "production"
workload = "line-of-business-app"
}
}
module "app_vnet" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-network.git?ref=v1.0.0"
name = "vnet-app-eastus"
resource_group_name = module.rg.name
location = module.rg.location
address_space = ["10.130.0.0/16"]
subnets = {
snet-app = {
address_prefixes = ["10.130.1.0/24"]
}
}
tags = module.rg.tags
}
module "app_nic" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface.git?ref=v1.0.0"
name = "nic-app01"
resource_group_name = module.rg.name
location = module.rg.location
ip_configurations = {
internal = {
subnet_id = module.app_vnet.subnet_ids["snet-app"]
private_ip_address_allocation = "Dynamic"
primary = true
}
}
tags = module.rg.tags
}
# The rule set that decides whether the forwarded port is usable at all.
module "app_nsg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-security-group.git?ref=v1.0.0"
name = "nsg-app"
resource_group_name = module.rg.name
location = module.rg.location
tags = module.rg.tags
}
module "nic_security_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-security-group-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
network_security_group_id = module.app_nsg.id
}
module "rdp_rule_attachment" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface-nat-rule-association.git?ref=v1.0.0"
network_interface_id = module.app_nic.id
ip_configuration_name = "internal"
nat_rule_id = var.rdp_nat_rule_id
}
output "rdp_rule_attachment" {
value = {
terraform_id = module.rdp_rule_attachment.id
import_address = module.rdp_rule_attachment.import_id
configuration_id = module.rdp_rule_attachment.ip_configuration_id
interface = module.rdp_rule_attachment.network_interface_name
load_balancer = module.rdp_rule_attachment.load_balancer_name
rule = module.rdp_rule_attachment.nat_rule_name
cross_sub = module.rdp_rule_attachment.spans_subscriptions
opens_a_route = module.rdp_rule_attachment.this_opens_a_direct_path_to_this_one_machine
}
}π The security group association is in the composition even though this module never references it, and that is the point: a NAT rule creates a route, and the rule set decides whether anything can use it. Leaving it out would misrepresent the change.
π‘ Referencing the interface through
module.app_nic.idrather than a variable is deliberate β Terraform's dependency graph then guarantees the interface exists before either association is attempted.
β οΈ module.rdp_rule_attachment.idappears in the output because it is Terraform's handle, not because it can be passed to anything expecting a Resource ID.configuration_idis the only real Azure Resource ID of the three.
Required: network_interface_id, ip_configuration_name, nat_rule_id
Optional: timeouts
There is no tags variable β the resource exposes none. Tag the interface or the load balancer.
Full input schemas
variable "network_interface_id" {
type = string
# REQUIRED, FORCE-NEW. Anchored to .../networkInterfaces/<name> at both ends.
# THIS IS THE OBJECT THAT GETS WRITTEN, and the provider locks it by name.
# A MISSING INTERFACE FAILS THE CREATE -- unlike the sibling ASG association.
}
variable "ip_configuration_name" {
type = string
# REQUIRED, FORCE-NEW. The provider checks only that it is non-empty.
# ONLY THIS CONFIGURATION IS MODIFIED. A name that does not exist on the
# interface fails the create AT APPLY -- no plan can catch it.
# This module additionally refuses a value beginning with "/".
}
variable "nat_rule_id" {
type = string
# REQUIRED, FORCE-NEW. A load balancer INBOUND NAT RULE:
# .../Microsoft.Network/loadBalancers/<lb>/inboundNatRules/<rule>
# A BACKEND ADDRESS POOL is a different resource and is refused.
# THE RULE TAKES ONE BACKEND -- a second attachment is refused by AZURE.
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
delete = optional(string)
})
default = null
# THREE KEYS. All three arguments are force-new, so there is no update
# operation; Terraform would discard an `update` key silently.
}| Output | Type | Notes |
|---|---|---|
id |
string |
Synthetic β first half is an IP configuration ID |
import_id |
string |
The exact terraform import address |
ip_configuration_id |
string |
A real Azure Resource ID |
network_interface_id / network_interface_name |
string |
The interface that gets written |
network_interface_resource_group_name / network_interface_subscription_id |
string |
Parsed |
ip_configuration_name |
string |
The one configuration modified |
nat_rule_id / nat_rule_name |
string |
The inbound NAT rule |
load_balancer_name |
string |
Parsed from the rule's ID |
nat_rule_resource_group_name / nat_rule_subscription_id |
string |
Where the rule lives |
spans_subscriptions / spans_resource_groups |
bool |
Where the read permission belongs |
cannot_verify_the_interface_and_rule_share_a_region |
bool |
Constant true β a stated limitation |
this_is_not_a_resource_in_azure |
bool |
Constant true |
creating_this_requires_write_on_the_whole_network_interface |
bool |
Constant true |
this_opens_a_direct_path_to_this_one_machine |
bool |
Constant true |
only_the_named_ip_configuration_is_modified |
bool |
Constant true |
a_missing_interface_fails_the_create_loudly |
bool |
Constant true β one sibling does the opposite |
a_missing_ip_configuration_fails_the_create_loudly |
bool |
Constant true |
a_concurrent_writer_can_silently_drop_this_association |
bool |
Constant true |
the_read_clears_the_id_by_three_separate_routes |
bool |
Constant true |
an_import_guard_exists_and_it_names_the_composite_id |
bool |
Constant true |
there_is_no_update_operation_at_all |
bool |
Constant true |
destroying_this_removes_the_attachment_not_the_rule |
bool |
Constant true |
no_secret_is_accepted_or_emitted_by_this_module |
bool |
Constant true |
this_resource_supports_no_azure_resource_tags |
bool |
Constant true |
force_new_fields |
list(string) |
All three arguments |
fields_that_can_change_after_creation |
list(string) |
Empty |
fields_azure_returns_on_read |
list(string) |
Empty β and the description says why |
This is not a resource in Azure, and most of the caveats follow from that. The provider reads the network
interface, attaches the inbound NAT rule to the named IP configuration, and writes the whole interface back,
holding an internal lock on the interface's name. Delete is the same operation in reverse. So the permission
required is networkInterfaces/write β the right to change any property of that interface β and Azure offers
nothing narrower.
A NAT rule is the pointed sibling of a backend address pool. A pool spreads traffic across whichever machines belong to it; a rule forwards one frontend port to one backend. That is why this association reads as a reachability change rather than a capacity one, and why it is usually reached for when someone needs administrative access to a specific instance.
And it decides nothing about what can traverse the route. The network security group rules applying to the interface β and to its subnet, which is evaluated as well β permit or deny the forwarded traffic. Neither is visible from this module, which is why the composition in example 12 includes a security group association this module never references.
The composite ID is not the shape most people guess. Its first half is an IP configuration Resource ID β
the interface's ID with /ipConfigurations/<name> appended β not the interface's own. Both import_id and
ip_configuration_id are emitted for that reason, and both derive from the inputs so they exist before the
resource does.
Siblings in this provider directory disagree about a missing parent. This resource's create errors when the interface is absent; the application-security-group association's create clears its ID and returns success, having done nothing. Both behaviours were read from the provider's source, and the one that applies here is emitted as a constant so it does not have to be rediscovered.
Two failures land at apply that no plan can catch. A misspelled IP configuration name, because nothing in
a Resource ID reveals which configurations an interface has; and a NAT rule that already has a backend,
because a rule takes one at a time and knowing that would mean reading the rule's current state. This module
adds one refusal the provider does not β an ip_configuration_name beginning with / β and invents nothing
else.
A concurrent writer wins silently, and here that closes a route. The lock lives inside one Terraform process. A second apply, a portal edit or a CLI call rewrites the same interface, one side's change vanishes with no error, and the forwarded port quietly stops reaching the machine.
There is nothing to drift. The read establishes only whether the rule is attached to the named
configuration, then sets the arguments from the composite ID. With three force-new arguments and no others,
there is nothing a change could be β so fields_azure_returns_on_read is deliberately empty with the reason
attached, and the resource can disappear from state by three routes that all look the same.
lifecycle is not valid inside a module block, so a caller cannot add prevent_destroy here.
| Concern | This module's position | What the caller must type to change it |
|---|---|---|
| The import address | Composed and emitted, along with the real IP configuration ID | β |
ip_configuration_name |
Refuses a Resource ID, and nothing more β the provider publishes no pattern | β |
| The target type | Anchored to inbound NAT rules, with the sibling module named in the message | use the pool module for pools |
| One backend per rule | Documented, never enforced β it would mean reading the rule's state | β |
| The security dependency | Emitted as a constant, because the deciding rules are invisible here | read them yourself |
| The region rule | Stated as uncheckable, in contrast to the two comparisons that are made | compare in the calling configuration |
fields_azure_returns_on_read |
Empty, with the reason in the description | β |
tags |
Absent by design β the resource exposes none | tag the interface or the load balancer |
π The question here is not a security setting. It is which address gets the forwarded port, what the rule set allows through it, and who else can write the interface.
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module by tag β ?ref=v1.0.0 β never a branch.
Plan-only: this repository performs no cloud apply. A human applies from CI β knowing that a destroy line closes a route and a create line opens one.
What terraform validate covers, offline and without credentials:
- The anchored network interface ID check, at both ends.
- A non-empty
ip_configuration_name, and a refusal of a Resource ID in that field. - The anchored NAT rule ID check, including refusing a backend address pool.
What only terraform plan exercises (credentials required):
- The import guard, if this rule is already attached to the named configuration.
What only terraform apply reveals:
- Whether the network interface exists β loudly, on this resource.
- Whether the named IP configuration exists on it.
- Whether the NAT rule already has a backend.
- Whether the interface and the load balancer are mutually reachable.
What nothing reveals at all:
- Whether the network security group rules permit traffic on the forwarded port.
- Whether a concurrent writer is editing the same interface.
βΉοΈ
terraform consolefires root-module variable validations, unliketerraform validateon a calling configuration. It is the honest offline harness for the first list and for every derived flag β including both branches of each span flag and the composite ID with a whitespace-padded configuration name.
Outputs:
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app-eastus/providers/Microsoft.Network/networkInterfaces/nic-app01/ipConfigurations/internal|/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app-eastus/providers/Microsoft.Network/loadBalancers/lb-app/inboundNatRules/rdp-01"
ip_configuration_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app-eastus/providers/Microsoft.Network/networkInterfaces/nic-app01/ipConfigurations/internal"
network_interface_name = "nic-app01"
ip_configuration_name = "internal"
load_balancer_name = "lb-app"
nat_rule_name = "rdp-01"
spans_subscriptions = false
spans_resource_groups = false
fields_that_can_change_after_creation = []
fields_azure_returns_on_read = []
βΉοΈ Compare the first two.
idis two IDs with a pipe between them;ip_configuration_idis its first half, and the only one of the two Azure would recognise.
| Symptom | Cause | Fix |
|---|---|---|
must be a network interface Resource ID |
An IP configuration's ID, or a rule's | Pass the interface; name the configuration separately |
must not be empty or whitespace |
A blank configuration name | Non-empty is the provider's only check |
must be a NAME, not a Resource ID |
The configuration's full ID was passed | This module's own refusal |
must be a load balancer INBOUND NAT RULE |
A backend address pool, or an ID stopping at the balancer | The message names the sibling module |
Apply fails <interface> was not found |
The interface does not exist | This resource fails loudly; its ASG sibling would not |
Apply fails IP Configuration ... was not found |
A misspelled or absent configuration name | Checked at apply only |
| Apply fails saying the rule is in use | The NAT rule already has a backend | One rule, one backend. Use another rule or a NAT pool |
| Apply fails with an authorization error | Write on the interface, or join on the rule, is missing | The rule may be in another subscription |
| Apply fails on reachability | The interface and load balancer cannot see each other | Same region and virtual network; not checkable here |
| The attachment vanished | A concurrent writer rewrote the interface | One pipeline should own an interface's attachments |
| The port forwards but nothing connects | The rule set does not permit it | Network security group rules, on the interface and the subnet |
| The wrong address got the port | The wrong configuration was named | Only the named one is attached |
| No drift is ever reported | The read checks presence only | Expected; see fields_azure_returns_on_read |
| A destroy closed an admin route | That is exactly what it does | Expected. The plan says only "1 to destroy" |
| The import command fails to parse | The pipe was not quoted | It is a shell metacharacter; quote import_id |
azurerm_network_interface_nat_rule_associationβ provider reference, including the Terraform-specific ID format- Manage inbound NAT rules β how a rule forwards a port to one backend
- Sibling modules:
terraform-azurerm-network-interface-backend-address-pool-association,terraform-azurerm-network-interface-security-group-association,terraform-azurerm-network-interface-application-security-group-association,terraform-azurerm-network-interface,terraform-azurerm-load-balancer,terraform-azurerm-network-security-group,terraform-azurerm-resource-group - This module's
SCOPE.mdβ the cross-module contract
π "Infrastructure as Code should be standardized, consistent, and secure."