Azure Verified Modules (AVM) requirements and best practices for developing certified Azure Terraform modules. Use when creating or reviewing Azure modules that need AVM certification.
Guides creation and review of Azure Terraform modules to meet Azure Verified Modules certification requirements.
npx claudepluginhub jadecli/jadecli-claude-pluginsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This guide covers the mandatory requirements for Azure Verified Modules certification. These requirements ensure consistency, quality, and maintainability across Azure Terraform modules.
References:
Severity: MUST | Requirement: TFFR1
When building Resource or Pattern modules, module owners MAY cross-reference other modules. However:
source = "Azure/xxx/azurerm" with version = "1.2.3"git::https://xxx.yyy/xxx.git or github.com/xxx/yyy)Severity: MUST | Requirement: TFFR3
Authors MUST only use the following Azure providers:
| Provider | Min Version | Max Version |
|---|---|---|
| azapi | >= 2.0 | < 3.0 |
| azurerm | >= 4.0 | < 5.0 |
Requirements:
required_providers block to enforce provider versions~>)Example:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
azapi = {
source = "Azure/azapi"
version = "~> 2.0"
}
}
}
Severity: MUST | Requirement: TFNFR4
MUST use lower snake_casing for:
Example: snake_casing_example
Severity: SHOULD | Requirement: TFNFR6
Severity: MUST | Requirement: TFNFR7
count for conditional resource creationmap(xxx) or set(xxx) as resource's for_each collectionExample:
resource "azurerm_subnet" "pair" {
for_each = var.subnet_map # map(string)
name = "${each.value}-pair"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
Severity: SHOULD | Requirement: TFNFR8
Order within resource/data blocks:
Meta-arguments (top):
providercountfor_eachArguments/blocks (middle, alphabetical):
Meta-arguments (bottom):
depends_onlifecycle (with sub-order: create_before_destroy, ignore_changes, prevent_destroy)Separate sections with blank lines.
Severity: SHOULD | Requirement: TFNFR9
Order within module blocks:
Top meta-arguments:
sourceversioncountfor_eachArguments (alphabetical):
Bottom meta-arguments:
depends_onprovidersSeverity: MUST | Requirement: TFNFR10
The ignore_changes attribute MUST NOT be enclosed in double quotes.
Good:
lifecycle {
ignore_changes = [tags]
}
Bad:
lifecycle {
ignore_changes = ["tags"]
}
Severity: SHOULD | Requirement: TFNFR11
For parameters requiring conditional resource creation, wrap with object type to avoid "known after apply" issues during plan stage.
Recommended:
variable "security_group" {
type = object({
id = string
})
default = null
}
Severity: MUST | Requirement: TFNFR12
Nested blocks under conditions MUST use this pattern:
dynamic "identity" {
for_each = <condition> ? [<some_item>] : []
content {
# block content
}
}
Severity: SHOULD | Requirement: TFNFR13
Good:
coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
Bad:
var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name
Severity: MUST | Requirement: TFNFR27
provider MUST NOT be declared in modules (except for configuration_aliases)provider blocks in modules MUST only use aliasSeverity: MUST | Requirement: TFNFR14
Module owners MUST NOT add variables like enabled or module_depends_on to control entire module operation. Boolean feature toggles for specific resources are acceptable.
Severity: SHOULD | Requirement: TFNFR15
Variables SHOULD follow this order:
Severity: SHOULD | Requirement: TFNFR16
xxx_enabled instead of xxx_disabledSeverity: SHOULD | Requirement: TFNFR17
description SHOULD precisely describe the parameter's purpose and expected data typeobject types, use HEREDOC formatSeverity: MUST | Requirement: TFNFR18
type MUST be defined for every variabletype SHOULD be as precise as possibleany MAY only be used with adequate reasonsbool instead of string/number for true/false valuesobject instead of map(any)Severity: SHOULD | Requirement: TFNFR19
If a variable's type is object and contains sensitive fields, the entire variable SHOULD be sensitive = true, or extract sensitive fields into separate variables.
Severity: SHOULD | Requirement: TFNFR20
Nullable SHOULD be set to false for collection values (sets, maps, lists) when using them in loops. For scalar values, null may have semantic meaning.
Severity: MUST | Requirement: TFNFR21
nullable = true MUST be avoided unless there's a specific semantic need for null values.
Severity: MUST | Requirement: TFNFR22
sensitive = false MUST be avoided (this is the default).
Severity: MUST | Requirement: TFNFR23
A default value MUST NOT be set for sensitive inputs (e.g., default passwords).
Severity: MUST | Requirement: TFNFR24
deprecated_variables.tfDEPRECATED at the beginning of descriptionSeverity: SHOULD | Requirement: TFFR2
Authors SHOULD NOT output entire resource objects as these may contain sensitive data and the schema can change with API or provider versions.
Best Practices:
name)sensitive = true for sensitive attributesfor_each, output computed attributes in a map structureExamples:
# Single resource computed attribute
output "foo" {
description = "MyResource foo attribute"
value = azurerm_resource_myresource.foo
}
# for_each resources
output "childresource_foos" {
description = "MyResource children's foo attributes"
value = {
for key, value in azurerm_resource_mychildresource : key => value.foo
}
}
# Sensitive output
output "bar" {
description = "MyResource bar attribute"
value = azurerm_resource_myresource.bar
sensitive = true
}
Severity: MUST | Requirement: TFNFR29
Outputs containing confidential data MUST be declared with sensitive = true.
Severity: MUST | Requirement: TFNFR30
deprecated_outputs.tfoutputs.tfSeverity: MAY | Requirement: TFNFR31
locals.tf SHOULD only contain locals blockslocals blocks next to resources for advanced scenariosSeverity: MUST | Requirement: TFNFR32
Expressions in locals blocks MUST be arranged alphabetically.
Severity: SHOULD | Requirement: TFNFR33
Use precise types (e.g., number for age, not string).
Severity: MUST | Requirement: TFNFR25
terraform.tf requirements:
terraform blockrequired_version~> #.# or >= #.#.#, < #.#.# formatExample:
terraform {
required_version = "~> 1.6"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
Severity: MUST | Requirement: TFNFR26
terraform block MUST contain required_providers blocksource and versionsource MUST be in format namespace/nameversion MUST include minimum and maximum major version constraints~> #.# or >= #.#.#, < #.#.# formatSeverity: MUST | Requirement: TFNFR5
Required testing tools for AVM:
terraform validate/fmt/test)Severity: SHOULD | Requirement: TFNFR36
For robust testing, prevent_deletion_if_contains_resources SHOULD be explicitly set to false in test provider configurations.
Severity: MUST | Requirement: TFNFR2
.terraform-docs.yml file MUST be present in the module rootSeverity: MUST | Requirement: TFNFR34
New resources added in minor/patch versions MUST have a toggle variable to avoid creation by default:
variable "create_route_table" {
type = bool
default = false
nullable = false
}
resource "azurerm_route_table" "this" {
count = var.create_route_table ? 1 : 0
# ...
}
Severity: MUST | Requirement: TFNFR35
Breaking changes requiring caution:
Resource blocks:
dynamicmoved blockscount to for_each or vice versaVariable/Output blocks:
typedefault valuesnullable to falsesensitive from false to truedefaultvaluesensitive valueSeverity: MUST | Requirement: TFNFR3
Module owners MUST set branch protection policies on the default branch (typically main):
Use this checklist when developing or reviewing Azure Verified Modules:
.terraform-docs.yml present in module rootfor_each uses map() or set() with static keysignore_changes not quotedcoalesce() or try() used for default valuesenabled or module_depends_on variablesany)nullable = falsesensitive = false declarationsdeprecated_variables.tfsensitive = truedeprecated_outputs.tfterraform.tf has version constraints (~> format)required_providers block present with all providersprovider declarations in module (except aliases)Based on: Azure Verified Modules - Terraform Requirements