Azure Verified Modules (Community Fork)
Glossary GitHub GitHub Issues Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

Last updated: 19 Apr 2024

Composition

While this page describes and summarizes important aspects of the composition of AVM modules, it may not reference All of the shared and language specific requirements.

Therefore, this guide MUST be used in conjunction with the Shared Specification and the Terraform specific specifications. ALL AVM modules (Resource and Pattern modules) MUST meet the respective requirements described in these specifications!

Before jumping on implementing your contribution, please review the AVM Module specifications, in particular the Shared and the Terraform specific pages, to make sure your contribution complies with the AVM module’s design and principles.



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 template repo here.

  • tests/ - (for unit tests and additional tests if required - e.g. tflint etc.)
    • unit/ - (optional, may use further sub-directories if required)
  • modules/ - (for sub-modules only if used)
  • examples/ - (all examples must deploy without successfully without requiring input - these are customer facing)
    • defaults - (minimum/required parameters/variables only, heavy reliance on the default values for other parameters/variables)
    • <other folders for examples as required>
  • /... - (Module files that live in the root of module directory)
    • _header.md - (required for documentation generation)
    • _footer.md - (required for documentation generation)
    • main.tf
    • locals.tf
    • variables.tf
    • outputs.tf
    • terraform.tf
    • locals.version.tf.json - (required for telemetry, should match the upcoming release tag)
    • README.md (autogenerated)
    • main.resource1.tf (If a larger module you may chose to use dot notation for each resource)
    • locals.resource1.tf



Directory and File Structure

/ root
├───.github/
│   ├───actions/
│   │   ├───avmfix/
│   │   │   └───action.yml
│   │   ├───docs-check/
│   │   │   └───action.yml
│   │   ├───e2e-getexamples/
│   │   │   └───action.yml
│   │   ├───e2e-testexamples/
│   │   │   └───action.yml
│   │   ├───linting/
│   │   │   └───action.yml
│   │   └───version-check/
│   │       └───action.yml
│   ├───policies/
│   │   ├───avmrequiredfiles.yml
│   │   └───branchprotection.yml
│   ├───workflows/
│   │   ├───e2e.yml
│   │   ├───linting.yml
│   │   └───version-check.yml
│   ├───CODEOWNERS
│   └───dependabot.yml
├───.vscode/
│   ├───extensions.json
│   └───settings.json
├───examples/
│   ├───default/
│   │   ├───README.md
│   │   ├───_footer.md
│   │   ├───_header.md
│   │   ├───main.tf
│   │   └───variables.tf
│   ├───.terraform-docs.yml
│   └───README.md
├───modules/
│   └───README.md
├───tests/
│   └───README.md
├───.gitignore
├───.terraform-docs.yml
├───CODE_OF_CONDUCT.md
├───LICENSE
├───Makefile
├───README.md
├───SECURITY.md
├───SUPPORT.md
├───_footer.md
├───_header.md
├───avm
├───avm.bat
├───locals.telemetry.tf
├───locals.tf
├───locals.version.tf.json
├───main.privateendpoint.tf
├───main.telemetry.tf
├───main.tf
├───outputs.tf
├───terraform.tf
└───variables.tf



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 within both the Shared and the Terraform specific pages.

See examples in specifications SNFR14 and TFFR14.



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.

See examples in specifications SFR1 and RMFR1.



Outputs

Make sure to review all specifications of Category: Outputs within both the Shared and the Terraform specific pages.

See examples in specification RMFR7 and TFFR2.



Interfaces

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 Shared Interfaces page.




Telemetry

To meet SFR3 & SFR4. We have provided the sample code below, however it is already included in the template repository.

locals {
  # This is the unique id for the module that is supplied by the AVM team.
  # TODO: change this to the PUID for the module. See https://azure.github.io/Azure-Verified-Modules/specs/shared/#id-sfr3---category-telemetry---deploymentusage-telemetry
  telem_puid = "46d3xgtf"

  # TODO: change this to the name of the module. See https://azure.github.io/Azure-Verified-Modules/specs/shared/#id-sfr3---category-telemetry---deploymentusage-telemetry
  module_name = "CHANGEME"

  # Should be either `res` or `ptn`
  module_type = "res"

  # This is an empty ARM deployment template.
  telem_arm_template_content = <<TEMPLATE
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {},
  "variables": {},
  "resources": [],
  "outputs": {
    "telemetry": {
      "type": "String",
      "value": "For more information, see https://aka.ms/avm/TelemetryInfo"
    }
  }
}
TEMPLATE

  # This constructs the ARM deployment name that is used for the telemetry.
  # We shouldn't ever hit the 64 character limit but use substr just in case.
  telem_arm_deployment_name = substr(
    format(
      "%s.%s.%s.v%s.%s",
      local.telem_puid,
      local.module_type,
      substr(local.module_name, 0, 30),
      replace(local.module_version, ".", "-"),
      local.telem_random_hex
    ),
    0,
    64
  )
  telem_random_hex = can(random_id.telem[0].hex) ? random_id.telem[0].hex : ""
}

resource "random_id" "telemetry" {
  count       = local.enable_telemetry ? 1 : 0
  byte_length = 4
}

# This is the module telemetry deployment that is only created if telemetry is enabled.
# It is deployed to the resource's resource group.
resource "azurerm_resource_group_template_deployment" "telemetry" {
  count               = var.enable_telemetry ? 1 : 0
  name                = local.telem_arm_deployment_name
  resource_group_name = var.resource_group_name
  deployment_mode     = "Incremental"
  location            = var.location
  template_content    = local.telem_arm_template_content
}

In addition, you should use a locals.version.tf.json file to store the module version in a machine readable format:

Do not include the v prefix in the module_version value.
{
  "locals": {
    "module_version": "1.0.0"
  }
}



Eventual Consistency

When creating modules, it is important to understand that the Azure Resource Manager (ARM) API is sometimes eventually consistent. This means that when you create a resource, it may not be available immediately. A good example of this is data plane role assignments. When you create such a role assignment, it may take some time for the role assignment to be available. We can use an optional time_sleep resource to wait for the role assignment to be available before creating resources that depend on it.

# In variables.tf...
variable "wait_for_rbac_before_foo_operations" {
  type = object({
    create  = optional(string, "30s")
    destroy = optional(string, "0s")
  })
  default     = {}
  description = <<DESCRIPTION
This variable controls the amount of time to wait before performing foo operations.
It only applies when `var.role_assignments` and `var.foo` are both set.
This is useful when you are creating role assignments on the bar resource and immediately creating foo resources in it.
The default is 30 seconds for create and 0 seconds for destroy.
DESCRIPTION
}

# In main.tf...
resource "time_sleep" "wait_for_rbac_before_foo_operations" {
  count = length(var.role_assignments) > 0 && length(var.foo) > 0 ? 1 : 0
  depends_on = [
    azurerm_role_assignment.this
  ]
  create_duration  = var.wait_for_rbac_before_foo_operations.create
  destroy_duration = var.wait_for_rbac_before_foo_operations.destroy

  # This ensures that the sleep is re-created when the role assignments change.
  triggers = {
    role_assignments = jsonencode(var.role_assignments)
  }
}

resource "azurerm_foo" "this" {
  for_each = var.foo
  depends_on = [
    time_sleep.wait_for_rbac_before_foo_operations
  ]
  # ...
}