tessl install github:hashicorp/agent-skills --skill azure-verified-modulesgithub.com/hashicorp/agent-skills
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.
Review Score
73%
Validation Score
13/16
Implementation Score
65%
Activation Score
75%
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_nameSeverity: 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