| name | avm-tf-codestyle |
| description | Use for AVM Terraform file layout, HCL style, variables, outputs, validation, lifecycle syntax, provider requirements, and Avm.Authoring formatting. |
AVM Terraform Code Style
Fetch the current Terraform non-functional specifications through https://azure.github.io/Azure-Verified-Modules/llms.txt. Current module examples can lag the specification and are not authoritative.
Standard file layout
TFNFR39 applies to the root module and every submodule:
| File | Requirement | Contents |
|---|
terraform.tf | MUST | The single terraform {} block with required Terraform and provider versions. |
variables.tf | MUST | Variable blocks. |
outputs.tf | MUST | Output blocks. |
main.tf | MUST | Primary resource, data, and module blocks. |
locals.tf | Required when locals exist | Local values only. |
Large modules may use main.<topic>.tf, variables.<topic>.tf, outputs.<topic>.tf, and locals.<topic>.tf. Topic names are snake case and each file contains only the block kind indicated by its prefix.
Do not add root-level providers.tf, module.tf, or everything.tf. Reusable AVM modules declare provider requirements but never provider configurations.
Each module and submodule also includes _header.md and _footer.md; README.md is generated.
Provider requirements
TFNFR25 requires a minimum and maximum Terraform CLI constraint, while TFFR3 requires the AzAPI provider range. Use the governance-managed baseline; a compliant current shape is:
terraform {
required_version = ">= 1.9, < 2.0"
required_providers {
azapi = {
source = "Azure/azapi"
version = "~> 2.12"
}
}
}
Do not raise required_version only because non-empty ignore_body_changes needs Terraform 1.11. Emit null when that interface is unused so earlier supported Terraform versions remain compatible.
Naming and declarations
- Use snake case for Terraform identifiers.
- Name the primary AzAPI resource
this; name satellite resources after their purpose.
- Give every variable and output a precise type and description.
- Mark secret variables and outputs sensitive and follow the current ephemeral-value requirements.
- Default collections to
{} or [] with nullable = false.
- Use
optional(...) for non-required object attributes and give defaults that match the documentation.
- Keep variable, local, resource, module, and output ordering consistent with the transforms applied by
avm transform.
- Prefer discrete computed outputs over whole-resource outputs.
Example:
variable "private_endpoints" {
type = map(object({
name = optional(string)
subnet_resource_id = string
}))
default = {}
nullable = false
validation {
condition = alltrue([
for endpoint in values(var.private_endpoints) :
can(provider::azapi::parse_resource_id("Microsoft.Network/virtualNetworks/subnets", endpoint.subnet_resource_id))
])
error_message = "Each private endpoint subnet must be a valid subnet resource ID."
}
description = "A map of private endpoints to create."
}
TFNFR38 requires provider::azapi::parse_resource_id with a literal expected resource type for ARM resource IDs. Short-circuit optional values and iterate collections or nested attributes. Do not use regex, startswith, length, or split as substitutes, except for the prescribed generic check on an extension-resource module's polymorphic parent_id under TFRMFR1.
AzAPI block requirements
Every AzAPI resource must:
- source
type from the deterministic field in var.resource_types;
- declare
response_export_values;
- declare
replace_triggers_refs;
- assign
retry = var.retry;
- emit
timeouts through a dynamic block; and
- assign the per-resource
ignore_body_changes list, collapsing empty to null.
See avm-tf-azapi for the complete pattern.
Lifecycle syntax
Terraform lifecycle references are expressions, not strings:
lifecycle {
ignore_changes = [tags]
}
Use static lifecycle.ignore_changes for compile-time-static references. Use the consumer-configurable AzAPI ignore_body_changes interface when body paths are dynamic. Body paths are strings because they are passed to the provider:
ignore_body_changes = length(var.ignore_body_changes.example_widgets) > 0 ? var.ignore_body_changes.example_widgets : null
Generated and managed files
- Edit
_header.md or _footer.md, not README.md.
- Do not hand-edit managed telemetry or provider-version content that
avm sync or avm transform owns.
- Keep comments rare and limited to non-obvious constraints or documented exceptions.
Formatting and validation
Use the PowerShell module rather than individual binaries:
avm transform
avm format
avm docs
avm pre-commit
Review and commit all resulting changes, then run:
avm pr-check
avm pr-check requires a clean Git worktree. Do not use Make, the old repository launcher, a container, Porch, or a separately installed formatter to approximate these checks.