TFFR8 - AzAPI - ignore_body_changes variable
ID: TFFR8 - Category: Inputs/Outputs - AzAPI - ignore_body_changes variable
The ignore_body_changes argument of every azapi_resource declared by the module MUST be configurable by the consumer. Authors MUST NOT hard-code an inline list that the consumer cannot override, and MUST NOT omit the argument.
To meet this requirement, every module — including every submodule (see TFRMNFR1) — MUST expose a variable named ignore_body_changes.
ignore_body_changes lets a consumer suppress plan diffs for a set of body paths that are mutated outside Terraform (for example tags applied by Azure Policy, or an autoscaler adjusting a capacity property). It is the supported fallback for lifecycle.ignore_changes when the paths must be derived from variables, locals or other non-static values, which lifecycle blocks cannot accept.
Without this variable a consumer has no way to reach the argument, because lifecycle.ignore_changes cannot be applied to a resource from outside the module that declares it. This is exactly the same problem that TFFR7 solves for retry and timeouts.
Prerequisites
ignore_body_changes is a write-only argument. As a result:
- The module’s
Azure/azapiconstraint inrequired_providersMUST allow v2.12.0 or later, which is the release that introduces the argument (see TFFR3). - A consumer supplying a non-empty value MUST be running Terraform 1.11 or later. Modules MUST NOT raise their
required_versionfloor for this reason alone (see TFNFR25); instead they MUST emitnullwhen the list is empty so that consumers on earlier Terraform versions who do not use the feature are unaffected. See Applying the variable.
Important
Because the value is held in provider-private state, a change to ignore_body_changes only takes effect after an apply. A consumer who adds a path will still see the pending diff for that path in the same plan, and a consumer who removes a path will not see the suppressed diff reappear until the next plan. Module documentation SHOULD call this out.
Variable shape
Unlike retry and timeouts, which are resource-agnostic and therefore cascade unchanged, ignore_body_changes values are dot-notation paths into one specific resource’s body. A path such as properties.addressSpace is meaningful only for the resource that owns it, so passing a parent’s list straight through to a submodule would apply meaningless paths to a different resource.
The variable is therefore scoped per resource and per submodule, using exactly the same shape and key-naming rule as resource_types (TFFR6).
The ignore_body_changes variable MUST:
- Be a single
object({...})(not amap(list(string))) so typos at call sites error at plan time and the full override surface is visible in the variable declaration. - Default the variable itself to
{}and benullable = false, per TFNFR20 and TFNFR21. - Declare one
optional(list(string), [])field for every AzAPI resource the module itself declares, keyed by the snake_case form of the ARM resource type with theMicrosoft.prefix dropped — the identical key used inresource_types(for exampleMicrosoft.Example/widgets→example_widgets). - Declare one nested
optional(object({...}), {})field for every submodule the module instantiates, keyed by that submodule’s primary ARM resource type. The shape of the nested object MUST match that submodule’s ownignore_body_changesvariable exactly, and the parent MUST cascade the slot through unchanged. - Document every field in the variable’s
description, including whatignore_body_changesdoes, that paths use dot notation, and that changes take effect only after an apply.
Module owners MAY ship module-level defaults where the resource is known to be mutated outside Terraform. To do so, supply the default inside the optional(list(string), [...]) wrapper. Consumers MUST still be able to override any individual field, and a module-level default MUST NOT be used to work around a bug that belongs in the module body.
Modules MAY additionally expose per-item overrides on the collection variable that drives a for_each submodule, for cases where individual instances need different paths. Where they do, the per-item value MUST take precedence over the shared slot.
Path syntax
Values are dot-notation paths relative to the resource’s body, for example tags or properties.sku.name. Each element MUST be a non-empty string.
Individual list items MUST NOT be targeted (there is no index syntax) — ignore the entire list property instead.
Authors and consumers MUST understand that an ignored path is not merely hidden from the plan: configuration changes at that path are not sent to Azure until the path is removed from the list.
Applying the variable
ignore_body_changes is an attribute (not a block) on azapi_resource, so the relevant field of the variable is assigned directly. The assignment MUST collapse an empty list to null so that the write-only argument is absent when the feature is unused:
ignore_body_changes = length(var.ignore_body_changes.example_widgets) > 0 ? var.ignore_body_changes.example_widgets : nullThe variable MUST be applied to every azapi_resource (and equivalent AzAPI resources) declared by the module.
Example — root and child
# === root variables.tf ===
variable "ignore_body_changes" {
type = object({
example_widgets = optional(list(string), [])
example_widgets_parts = optional(object({
example_widgets_parts = optional(list(string), [])
}), {})
})
default = {}
nullable = false
}
# === root main.tf ===
resource "azapi_resource" "this" {
type = var.resource_types.example_widgets
name = var.name
parent_id = var.parent_id
body = { /* ... */ }
ignore_body_changes = length(var.ignore_body_changes.example_widgets) > 0 ? var.ignore_body_changes.example_widgets : null
response_export_values = []
}
module "part" {
source = "./modules/part"
for_each = var.parts
name = each.value.name
parent_id = azapi_resource.this.id
resource_types = var.resource_types.example_widgets_parts
retry = var.retry
timeouts = var.timeouts
# Cascade the nested slot through unchanged.
ignore_body_changes = var.ignore_body_changes.example_widgets_parts
}
# === modules/part/variables.tf ===
variable "ignore_body_changes" {
type = object({
example_widgets_parts = optional(list(string), [])
})
default = {}
nullable = false
}
# === modules/part/main.tf ===
resource "azapi_resource" "this" {
type = var.resource_types.example_widgets_parts
name = var.name
parent_id = var.parent_id
body = { /* ... */ }
ignore_body_changes = length(var.ignore_body_changes.example_widgets_parts) > 0 ? var.ignore_body_changes.example_widgets_parts : null
response_export_values = []
}A consumer ignoring tags on the widget, and a policy-managed property on every part, writes:
module "widget" {
source = "Azure/avm-res-example-widget/azure"
ignore_body_changes = {
example_widgets = var.ignore_policy_tags ? ["tags"] : []
example_widgets_parts = {
example_widgets_parts = ["properties.retentionPolicy"]
}
}
# ...other arguments...
}See https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/resource#ignore_body_changes for full semantics.