Terraform Composition
Important
AzAPI is the required Azure provider for new AVM Terraform modules. Every new resource, pattern, or utility module MUST use the Azure/azapi provider for every control-plane resource and supported data-plane operation.
AzureRM is permitted only for a specific unsupported data-plane/non-ARM API operation under the narrow TFFR3 exception. The exception applies only to that operation; supporting control-plane resources in the module, submodules, examples, end-to-end tests, Terraform tests, fixtures, and documentation snippets MUST use AzAPI.
This guide MUST be used in conjunction with the Terraform specifications. All AVM modules MUST meet the applicable requirements in those specifications.
Repositories
Each Terraform AVM module will have its own GitHub Repository in the Azure GitHub Organization as per SNFR19.
This repo will be created by the Module Owners and the AVM Core team collaboratively, including the configuration of permissions as per SNFR9
Directory and File Structure
Below is the directory and file structure expected for each AVM Terraform repository/module.
See the Terraform AVM template repository. The azurerm segment in this legacy repository name is not a provider choice; modules created from the template MUST use AzAPI.
tests/- (for unit tests and integration tests using Terraform test)unit/- (.tftest.hcl files for required unit testing with Terraform test)setup.ps1- (optional setup hook)
integration/- (optional .tftest.hcl files for integration testing with Terraform test)setup.ps1- (optional setup hook)
modules/- (for sub-modules only if used; each submodule root MUST be a directmodules/<name>/child)examples/- (all examples must deploy successfully without requiring input and use AzAPI for supporting control-plane resources; AzureRM may appear only when exercising the module’s documented unsupported data-plane exception - these are customer facing and run as end-to-end tests)<at least one folder>- (at least one example that uses the variable defaults minimum/required parameters/variables only)pre.ps1- (optional setup hook)post.ps1- (optional cleanup hook)tflint-pre.ps1- (optional setup hook for TFLint).e2eignore- (optional marker that excludes the example from e2e testing)
<other folders for examples as required>- (each example root MUST be a directexamples/<name>/child)
/...- (Module files that live in the root of module directory)_header.md- (required for documentation generation)_footer.md- (required for documentation generation)main.tflocals.tfvariables.tfoutputs.tfterraform.tfREADME.md(autogenerated)main.resource1.tf(If a larger module you may chose to use dot notation for each resource)locals.resource1.tf
See Lifecycle hooks for hook execution, environment, and migration guidance.
Nested Terraform module and example roots are prohibited. Avm.Authoring convention validation enforces the one-layer modules/* and examples/* structure; see TFRMNFR1.
Code Styling
This section points to conventions to be followed when developing a module.
Casing
Use snake_casing as per TFNFR3.
Input Parameters and Variables
Make sure to review all specifications of Category: Inputs/Outputs within the Terraform specification pages.
Resources
Resources are primarily leveraged by resource modules to declare the primary resource of the main resource type deployed by the AVM module.
Make sure to review all specifications covering resource properties and usage.
Outputs
Make sure to review all specifications of Category: Inputs/Outputs within the Terraform specification pages.
Interfaces
Note
This section is only relevant for contributions to resource modules.
To meet RMFR4 and RMFR5 AVM resource modules must leverage consistent interfaces for all the optional features/extension resources supported by the AVM module primary resource.
Please refer to the Terraform Interfaces page.
Mandatory AzAPI patterns
Every Terraform AVM module MUST be built with AzAPI, and every resource module MUST implement the following AzAPI patterns. The cross-references point at the normative specs — this section only summarises them so that nothing here is missed during scaffolding.
| Spec | One-liner |
|---|---|
| TFFR3 | Use Azure/azapi for every control-plane resource and supported data-plane operation in the module, submodules, examples/e2e tests, Terraform tests, fixtures, and documentation snippets. AzureRM is permitted only for a documented data-plane/non-ARM operation that AzAPI cannot implement. |
| TFRMFR1 | Expose the parent scope as a single required parent_id string variable. Do not expose resource_group_name or any other scope-specific input. Validate with provider::azapi::parse_resource_id against the expected parent type. |
| TFRMNFR1 | Implement every ARM subresource as a Terraform submodule under a direct modules/<subresource-singular-name>/ child. Parent modules MUST reference submodules, and submodules MUST be independently consumable. Keep submodule primary resources single-instance only (no count / for_each on azapi_resource.this); cardinality belongs at the module call site. Nested module roots are prohibited. |
| TFRMNFR2 | Name the primary azapi_resource this. Satellite resources MUST be named after what they represent (e.g. azapi_resource.lock, azapi_resource.role_assignment, azapi_resource.diagnostic_setting, azapi_resource.private_endpoint), not this. |
| TFFR4 | Always set response_export_values on every AzAPI resource (use [] when nothing needs exporting). Include any read-only properties the module’s outputs or downstream resources depend on. |
| TFFR5 | Set replace_triggers_refs only when body paths require replacement. The non-empty static list MUST contain valid, unique JMESPath expressions; omit the argument when no paths are needed. name and location are already triggers, so don’t repeat them. |
| TFFR6 | Source the type argument of every AzAPI resource from a single resource_types object variable instead of hard-coding type strings. Use one optional key per resource, defaulted to the tested API version, and cascade the relevant subset to each submodule. |
| TFFR7 | Expose retry and timeouts variables, apply them to every azapi_resource, and cascade them to every submodule. |
| TFFR8 | Expose an ignore_body_changes object variable so consumers can suppress diffs on body paths derived from non-static values. Use one optional list(string) key per resource (same key naming as resource_types), collapse empty lists to null, and cascade the relevant nested slot to each submodule — never the parent’s own paths. |
| TFNFR38 | Validate every variable (or nested attribute) that holds an Azure ARM resource ID using can(provider::azapi::parse_resource_id("Microsoft.X/y", value)). Hand-rolled regex / startswith / length checks MUST NOT be used. |
| TFNFR39 | Use the standard file layout (terraform.tf, variables.tf, outputs.tf, main.tf, locals.tf). Larger modules MAY split main.tf into main.<topic>.tf files. |
The interface schema files under static/includes/interfaces/tf/ are the canonical, copy-pasteable templates for the variables described by these specs. Treat them as authoritative.
Telemetry
To meet the requirements of SFR3 & SFR4, we use the modtm telemetry provider. This lightweight telemetry provider sends telemetry data to Azure Application Insights via a HTTP POST front end service.
The modtm telemetry provider is included in all Terraform modules and enabled by default through main.telemetry.tf, which is generated and maintained by Avm.Authoring. You do not need to change this configuration.
Make sure that the modtm provider is listed under the required_providers section in the module’s terraform.tf file using the following entry. This is also validated by the linter.
terraform {
required_providers {
# .. other required providers as needed
modtm = {
source = "Azure/modtm"
version = "~> 0.3"
}
}
}