--- name: avm-tf-submodules description: Use for AVM Terraform ARM subresources implemented as local submodules, including cardinality, parent_id, resource_types, retry, timeouts, ignore_body_changes, outputs, files, and tests. --- # AVM Terraform Submodules TFRMNFR1 requires each ARM subresource to be implemented as a full local submodule under `modules//`. Read TFRMNFR1 together with TFRMFR1, TFRMNFR2, TFFR6-TFFR8, TFNFR38, and TFNFR39 through . Every new submodule that deploys an Azure resource MUST use AzAPI for every control-plane and supported direct Azure operation. Do not declare or configure `hashicorp/azurerm`, and do not create any `azurerm_*` resource or data source for convenience or supporting infrastructure in a submodule or its examples, tests, fixtures, or setup Terraform. ## Cardinality The parent owns `for_each` or `count`; the child primary resource owns one instance: ```hcl 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 ignore_body_changes = var.ignore_body_changes.example_widgets_parts } ``` ```hcl # modules/part/main.tf resource "azapi_resource" "this" { type = var.resource_types.example_widgets_parts name = var.name parent_id = var.parent_id body = { properties = var.properties } ignore_body_changes = length(var.ignore_body_changes.example_widgets_parts) > 0 ? var.ignore_body_changes.example_widgets_parts : null response_export_values = [] retry = var.retry dynamic "timeouts" { for_each = var.timeouts == null ? [] : [var.timeouts] content { create = timeouts.value.create read = timeouts.value.read update = timeouts.value.update delete = timeouts.value.delete } } } ``` Do not add `count` or `for_each` to the child's primary resource. ## Required files Every submodule follows TFNFR39 and the applicable documentation, telemetry, and testing requirements: ```text modules/part/ _footer.md _header.md main.tf main.telemetry.tf outputs.tf README.md # generated terraform.tf variables.tf locals.tf # when locals exist tests/ unit/ integration/ ``` Additional files use canonical prefixes such as `main.role_assignments.tf`. The submodule declares every provider it consumes in its own `terraform.tf`; AzAPI is required. Each permitted `azurerm_*` resource or data-source block must independently implement one specific unsupported data-plane/non-ARM operation, document the exact block and AzAPI gap with an upstream AzAPI issue or pull request, and be replaced when support ships. One valid block does not authorize another. ## Parent ID Each submodule exposes required, non-null `parent_id` and assigns it to its primary resource: ```hcl variable "parent_id" { type = string nullable = false description = "The fully-qualified ARM resource ID of the existing widget that will contain the part." validation { condition = can(provider::azapi::parse_resource_id("Microsoft.Example/widgets", var.parent_id)) error_message = "`parent_id` must be a valid widget resource ID." } } ``` The parent normally passes `azapi_resource.this.id`. Do not replace this contract with `resource_group_name`, subscription IDs, or data-source reconstruction. ## `resource_types` The child owns its tested API-version default: ```hcl # modules/part/variables.tf variable "resource_types" { type = object({ example_widgets_parts = optional(string, "Microsoft.Example/widgets/parts@2024-01-01") }) default = {} nullable = false description = < part.resource_id } description = "A map of part resource IDs keyed by the input map." } ``` Prefer discrete outputs over whole-resource output objects. ## Documentation and tests - Author `_header.md` and `_footer.md`; generate each `README.md` with `avm docs` or `avm pre-commit`. - Add provider-mocked unit tests for child logic and parent aggregation. - Add real-Azure integration coverage where ARM behavior matters. - Exercise representative child instances through an E2E example. - Verify parent and child resource IDs, nested interface propagation, and idempotency. ## Migration warning Extracting an existing root collection into a `for_each` submodule changes addresses from `resource.type["key"]` to `module.child["key"].resource.this`. A generic reusable moved block cannot preserve arbitrary consumer keys across that resource-to-module boundary. Prefer an in-place provider migration first, or publish explicit state migration steps and classify the extraction as breaking when required. See `avm-tf-migration`.