TFNFR38 - Resource ID Variable Validation
ID: TFNFR38 - Category: Inputs/Outputs - Resource ID Variable Validation
Every input variable (or nested attribute) that holds an Azure ARM resource ID MUST be validated using the AzAPI provider-defined function provider::azapi::parse_resource_id, called with a literal string naming the expected resource type, and wrapped in can(...).
Hand-rolled regex, startswith, length, or split checks MUST NOT be used to validate resource IDs. The provider function knows the canonical ARM ID grammar for every resource type, is fixed in lockstep with the provider, and produces a single consistent error model — including for IDs whose grammar contains anomalies (such as classic resources, extension resources, or scope-based IDs).
This rule covers, but is not limited to:
- Top-level scope variables such as
parent_id(see TFRMFR1). - Variables that reference other Azure resources by ID (e.g.
subnet_resource_id,key_vault_resource_id,workspace_resource_id,private_dns_zone_resource_ids,user_assigned_resource_ids). - Nested attributes inside
object,map(object),set(object), orlist(object)types that hold resource IDs.
Rules
- The resource type passed to
parse_resource_idMUST be a literal string (e.g."Microsoft.Network/virtualNetworks/subnets"). It MUST NOT be a reference to another variable, local, or expression. This keeps eachvalidationblock self-contained and avoids requiring cross-variable validation. - For optional /
nullablevariables, the validation MUST short-circuit onnull(e.g.var.x == null || can(provider::azapi::parse_resource_id("...", var.x))) so that callers omitting the value do not trip validation. - For collection-valued variables (
set(string),list(string),map(string)), the validation MUST iterate the collection withalltrue([for v in ... : can(...)]). - For nested attributes within object types, the validation MUST iterate the parent collection (or reference the object directly) and validate each nested resource ID, again handling
nullfor optional nested attributes. - Where a variable can legitimately hold IDs of more than one resource type (rare — e.g.
marketplace_partner_resource_idin the diagnostic-settings interface), this rule does not apply and the variable SHOULD be left without resource-ID validation rather than validated against a single arbitrary type.
Examples
A required, single-value resource ID:
variable "key_vault_resource_id" {
type = string
nullable = false
validation {
condition = can(provider::azapi::parse_resource_id("Microsoft.KeyVault/vaults", var.key_vault_resource_id))
error_message = "`key_vault_resource_id` must be a valid Azure Key Vault resource ID."
}
description = "The resource ID of the Key Vault that holds the customer-managed key."
}An optional, single-value resource ID:
variable "workspace_resource_id" {
type = string
default = null
nullable = true
validation {
condition = var.workspace_resource_id == null || can(provider::azapi::parse_resource_id("Microsoft.OperationalInsights/workspaces", var.workspace_resource_id))
error_message = "`workspace_resource_id` must be a valid Log Analytics workspace resource ID, or `null`."
}
description = "The resource ID of the Log Analytics workspace to send diagnostics to."
}A collection of resource IDs:
variable "user_assigned_resource_ids" {
type = set(string)
default = []
nullable = false
validation {
condition = alltrue([
for id in var.user_assigned_resource_ids :
can(provider::azapi::parse_resource_id("Microsoft.ManagedIdentity/userAssignedIdentities", id))
])
error_message = "Each entry in `user_assigned_resource_ids` must be a valid user-assigned managed identity resource ID."
}
description = "A set of user-assigned managed identity resource IDs to attach to the resource."
}A nested resource ID inside a map(object(...)):
variable "private_endpoints" {
type = map(object({
subnet_resource_id = string
private_dns_zone_resource_ids = optional(set(string), [])
# ...other attributes...
}))
default = {}
nullable = false
validation {
condition = alltrue([
for _, v in var.private_endpoints :
can(provider::azapi::parse_resource_id("Microsoft.Network/virtualNetworks/subnets", v.subnet_resource_id))
])
error_message = "Each `private_endpoints[*].subnet_resource_id` must be a valid subnet resource ID."
}
validation {
condition = alltrue(flatten([
for _, v in var.private_endpoints : [
for id in v.private_dns_zone_resource_ids :
can(provider::azapi::parse_resource_id("Microsoft.Network/privateDnsZones", id))
]
]))
error_message = "Each entry in `private_endpoints[*].private_dns_zone_resource_ids` must be a valid private DNS zone resource ID."
}
}Notes
- The rule applies regardless of whether the resource ID is required or optional, single-valued or collection-valued, top-level or nested.
parse_resource_iderrors when (a) the input is not a well-formed ARM ID, or (b) the input does not parse as the supplied resource type. Wrapping incan(...)converts both failure modes into a single boolean suitable for avalidationblock’scondition.- This rule supersedes any older guidance suggesting
startswith(var.x, "/")or hand-written regex for resource ID validation.